Add session abstraction, mobile bearer auth, and pluggable SSO (Google/Discord/OIDC) #24
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/auth-session-abstraction"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Overview
Evolves authentication into a provider-agnostic session layer and builds two new auth surfaces on top of it — mobile bearer tokens and pluggable SSO (built-in Google & Discord + generic OIDC) — as safe, incremental additions. Local username/password + TOTP, cookie sessions, bot-scoring, login backoff, and RBAC are unchanged. Every flow now issues sessions through a single seam:
Delivered in three parts, backend-first, each verified before the next.
Part 1 — Session abstraction (backward-compatible refactor)
server/src/auth/module:token.js(JWT/cookie primitives),session.service.js(create / validate / partial-TOTP / decode / revoke stubs),session.middleware.js(attachSession,requireAuth,requireRole), andproviders/contracts.utils/auth.jsbecomes a thin compatibility facade re-exporting the same names, so every existing import site (routes, middleware,siteMode) is untouched.Authorization: Bearer, so bearer support was in place before Part 2.Part 2 — Mobile / Android bearer auth (additive)
POST /api/v1/auth/mobile/{login,refresh,logout}: a short-lived access token (bearer JWT, validated by the same middleware as the cookie) plus a long-lived refresh token.mobile_refresh_tokenstable and rotated on every refresh — a replayed refresh token is single-use. Logout revokes one or all.{ totpRequired: true }fallback if a 2FA code is missing/invalid).token.signTokengains a backward-compatibleexpiresInoption for the short access-token TTL.Part 3 — Pluggable SSO (Google, Discord, generic OIDC)
OAuth2Providerbase + built-in Google and Discord (endpoints hardcoded — admins configure only client id/secret) + a fully-configurable generic OIDC/OAuth2 provider (Authentik/Keycloak/Okta/Azure AD/Zitadel/…). Aregistryhandles instantiation, config validation, and health.GET /api/v1/auth/providers; redirect flowGET /auth/sso/:provider/{start,link,callback}, CSRF-protected with a signed httpOnly transaction cookie + PKCE.utils/secretBox.js) and never returned to any client (hasSecretflag only)./admin/auth/providers(built-ins enable-only/undeletable) and account linking/admin/account/identities. Newauth_providers+user_identitiestables.sessionService, so login/activity logging, RBAC (role from the linked user), and bot protection are identical to a local login.Frontend
GET /auth/providers(inline SVG brand icons — no binary assets) below the unchanged password form; renders fine with zero providers, and surfaces?sso_error(e.g.not_linked)./admin/auth-providers): tabs Local / Google / Discord / Custom, with each built-in showing its exact callback URL to register and health warnings for enabled-but-incomplete providers.Database
Three new tables, all added idempotently to
schema.sql(applied byensureSchema()on boot; verified created on a live DB):mobile_refresh_tokens— hashed, rotating mobile refresh tokens (FK → users, cascade).auth_providers— SSO provider config; client secret column stored encrypted.user_identities— external→internal account links,UNIQUE(provider, subject).Config (
server/.env.example)New:
SECRET_ENC_KEY(AES key for provider secrets; required in prod, dev-derived fromJWT_SECRET),APP_BASE_URL(builds the OAuthredirect_uri),MOBILE_ACCESS_TTL(15m),MOBILE_REFRESH_TTL_DAYS(30).Testing
83/83 server tests pass. New DB-free suites (mock global
fetch+ stub model objects):session,mobileSession,providers,registry,secretBox,ssoState,ssoCallback(linked-login / not-linked-refusal / link / CSRF-reject). All prior tests remain green. Client builds clean (191 modules); login page verified in preview (zero-provider path + rendered provider buttons, no console errors). README +.env.exampleupdated.Not included / follow-ups
${APP_BASE_URL}/api/v1/auth/sso/:provider/callback).invalidateSession/invalidateAllUserSessions).🤖 Generated with Claude Code
Refactor authentication into a provider-agnostic session layer and build two new auth surfaces on top of it, without changing local password/TOTP behavior. Every flow now issues sessions through sessionService.createSession(user, authMethod). Part 1 — Session abstraction (backward-compatible refactor): - New server/src/auth/: token.js (JWT/cookie primitives), session.service.js (create/validate/partial-TOTP/revoke), session.middleware.js (attachSession/requireAuth/requireRole). utils/auth.js is now a thin compat facade so existing imports are unchanged. Part 2 — Mobile bearer auth (additive): - /api/v1/auth/mobile/{login,refresh,logout}: short-lived access JWT + long-lived refresh token, stored hashed and rotated on use, in a new mobile_refresh_tokens table. Reuses web bot-scoring/backoff; single-request TOTP. token.signToken gains a backward-compatible expiresIn option. Part 3 — Pluggable SSO (Google, Discord, generic OIDC): - OAuth2Provider base + built-in Google/Discord (fixed endpoints) + generic OIDC, a registry with health/validation, PKCE+CSRF transaction state, and discovery (GET /auth/providers), start/link/callback routes. - Link-only policy: SSO signs in only to an already-linked account; external identities are never auto-provisioned. Client secrets encrypted at rest (AES-256-GCM, utils/secretBox.js). Admin CRUD (/admin/auth/providers) and account linking (/admin/account/identities). New auth_providers + user_identities tables. Frontend: - Login page renders provider buttons from /auth/providers (inline SVG icons, graceful with zero providers). New Authentication admin view (Local/Google/Discord/Custom). Account page linked-accounts section. Tests: 83 passing (session, mobile, providers, registry, secretBox, ssoState, ssoCallback) — all DB-free via fetch mocks + model stubs. README + .env.example updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>