ARCHWAY ID SSO · spec v1checking…

One account for every Archway Point product. This service knows about accounts and nothing else — products verify tokens locally with the public key, so any number of them share one Archway ID with zero coupling and no shared secrets. The universal sign-in page lives at /; send users to /?redirect=<your-page> and read #archway_token off the fragment when they come back.

Base URL

https://sso.archwaypoint.com        (public, TLS)
http://192.168.3.31                 (rack LAN — VM 102 "archway-sso")

Architecture

Client ── email+password ──────────▶ Archway ID ── issues Ed25519 JWT (7d)
   │                                     │
   └── Authorization: Bearer <jwt> ──▶ Your product's server
                                       verifies LOCALLY via /auth/jwks.json

Node 22, zero-framework node:http, jose, Postgres (2 tables), one container behind Caddy. The Ed25519 keypair is generated on first boot and persisted in Postgres — tokens survive restarts. Public key at /auth/jwks.json (kid: archway-1). Passwords: scrypt (N=16384, r=8, p=1, 16-byte salt), constant-time compare, stored as s1$salt$hash.

HTTP API (JSON · permissive CORS · token rides the header, no cookies)

EndpointBody / headerReturns
POST /auth/register{email, password} (pw ≥ 8){token, account} · 409 if email taken
POST /auth/login{email, password}{token, account} · 401 on mismatch
GET /auth/meAuthorization: Bearer <jwt>{account: {id, email}}
POST /auth/change-password{old, new} + Bearer{ok: true}
GET /auth/jwks.json{keys: [publicJwk]}
GET /auth/health{ok, service: "archway-id"}

Rate limit: 10 register/login attempts per IP per minute (429). Emails lower-cased, unique. Bodies capped at 10 KB.

Token

JWT, alg: EdDSA, kid: archway-1. Claims: sub = account UUID (key your product's data on this, never on email), email, iss: "archway-id", iat, exp (7 days). No refresh tokens in v1 — clients re-login on expiry.

Your own branded login page — the client SDK

Every product can skin sign-in however it likes: the auth endpoints are plain JSON with permissive CORS, so your page calls them directly — no redirect to the hosted page required. Easiest path is the drop-in SDK served right from this host:

<script src="https://sso.archwaypoint.com/archway-id.js"></script>
<script>
  // your own form, your own branding — the SDK is just the wire calls
  ArchwayID.login(email, password)          // or .register(email, password)
    .then(({ account }) => start(account))  // token auto-stored in localStorage
    .catch(err => showError(err.error));    // { status, error } — 401/409/429 pre-worded

  ArchwayID.me().then(a => a && skipLogin(a));       // already signed in?
  fetch("/api/thing", { headers: ArchwayID.authHeader() }); // call YOUR backend
</script>

Also on the SDK: changePassword(oldPw, newPw), token(), signOut(), configure({baseUrl}) for LAN installs, and hostedLoginUrl(returnTo) / consumeRedirectToken() if you'd rather bounce through the hosted page. Prefer raw HTTP? Skip the SDK — the table above is the whole API.

Integrating a new product — the whole recipe

1 · Client: POST /auth/login (or register), keep token, hand it to your backend however you like.

3 · Config: one env var — AUTH_JWKS_URL=https://sso.archwaypoint.com/auth/jwks.json. Retry the JWKS fetch at boot so container ordering never matters.

2 · Server — verify locally, never call Archway ID per request:

import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(
  new URL(process.env.AUTH_JWKS_URL),
  { cooldownDuration: 60_000 });
const { payload } = await jwtVerify(token, jwks,
  { issuer: "archway-id" });
// payload.sub = the account id

Security notes & v2 candidates

Known v1 gaps, accepted for LAN/beta: no refresh/revocation (7-day blast radius on a leaked token), no email verification or password reset, in-memory per-instance rate limiting, * CORS (safe because auth is header-based — no cookies). Before wide public use: password reset via email, per-account lockout, and key rotation (issue archway-2, serve both keys in the JWKS until old tokens expire).

Reference implementation: src/index.ts (245 lines) in the archway-sso repo · consumers: Prometheus (server/src/verify.ts).