ARCHWAY ID SSO · spec v2checking…

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 ── access JWT (15 min) + refresh
   │                                     │
   └── Authorization: Bearer <jwt> ──▶ Your product's server
                                       verifies LOCALLY via /auth/jwks.json (checks aud!)

Node 22, zero-framework node:http, jose, Postgres (6 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, client_id?} (pw ≥ 8){token, refresh, expires_in, account} · 409 taken
POST /auth/login{email, password, client_id?}same · 401 on mismatch
POST /auth/refresh{refresh}same — refresh is ROTATED, store the new one
POST /auth/logout{refresh}{ok}
POST /auth/logout-allBearer{ok} — every session
GET /auth/meBearer{account: {id, email, email_verified}}
POST /auth/change-password{old, new} + Bearer{ok} — revokes all sessions
POST /auth/verify/requestBearer{ok} (re-send, rate limited)
POST /auth/verify/confirm{token}{ok, account} (single-use, 24h)
POST /auth/reset/request{email}{ok} ALWAYS — never leaks existence
POST /auth/reset/confirm{token, password}{ok} (single-use, 1h) — revokes all sessions
POST /auth/admin/revoke{sub} + Bearer ADMIN_KEY{ok} — ban support
GET /auth/jwks.json—{keys: [publicJwk]}
GET /auth/health—{ok, service, db, key} (readiness)

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

Tokens

Access token: JWT, alg: EdDSA, kid: archway-1, 15 minutes. Claims: sub = account UUID (immutable, never reused — key your data on this), aud = the client_id it was minted for, email, email_verified, sid, jti, iss: "archway-id", iat, exp.

Refresh token: opaque r1.<sid>.<secret>, ~30 days, stored hashed, rotated on every /auth/refresh — replaying a rotated-away token revokes the whole session family (theft response).

Products MUST verify aud. A token minted for another product is not a credential here:
jwtVerify(token, jwks, { issuer: "archway-id", audience: "yourproduct", clockTolerance: 60 })
Registered clients: sso (default), prometheus, gamersfreedom, archwaypoint-admin (OIDC, public + PKCE). Check email_verified before linking accounts by email.

OIDC — "Sign in with Archway ID" for standard tooling

Anything that speaks OAuth2/OIDC (Pelican OAuth schema, Laravel Socialite, NextAuth, passport) integrates with config only:

Discovery:  https://sso.archwaypoint.com/.well-known/openid-configuration
Authorize:  GET  /auth/authorize    (response_type=code, PKCE S256 REQUIRED)
Token:      POST /auth/token        (form-encoded or JSON; code + code_verifier)
Userinfo:   GET  /auth/userinfo     (Bearer) -> {sub, email, email_verified}
JWKS:       /auth/jwks.json         (EdDSA archway-1 + RS256 archway-rsa-1)

id_tokens are RS256 (iss: https://sso.archwaypoint.com, 1h). Exact-match redirect allowlist per client, single-use 60s codes, S256 mandatory, state echoed. Register clients (admin): POST /auth/admin/clients {client_id, name, redirect_uris[], client_secret?}. Consent happens on the hosted /oauth.html page.

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 & roadmap

v2 shipped: email verification, password reset (always-ok, hashed single-use tokens, revokes sessions), 15-min access + rotating refresh with reuse revocation, required aud, admin revoke-by-sub, readiness health. Still open: per-account lockout, account status in claims, key rotation, audit log, email-change flow, TOTP for staff, OIDC provider. Tokens live in localStorage — the 15-minute TTL bounds XSS damage, but keep your pages clean.

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