Add session abstraction, mobile bearer auth, and pluggable SSO (Google/Discord/OIDC) #24

Merged
whitlocktech merged 1 commits from feature/auth-session-abstraction into main 2026-07-03 15:35:46 +00:00
Member

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:

sessionService.createSession(user, authMethod)   // authMethod ∈ local · totp · mobile · google · discord · oidc

Delivered in three parts, backend-first, each verified before the next.


Part 1 — Session abstraction (backward-compatible refactor)

  • New 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), and providers/ contracts.
  • utils/auth.js becomes a thin compatibility facade re-exporting the same names, so every existing import site (routes, middleware, siteMode) is untouched.
  • The session token already validated from a cookie or Authorization: Bearer, so bearer support was in place before Part 2.
  • Zero behavior change — purely structural.

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.
  • Refresh tokens are stored hashed (never in the clear) in a new mobile_refresh_tokens table and rotated on every refresh — a replayed refresh token is single-use. Logout revokes one or all.
  • Mobile login reuses the web bot-scoring + backoff defenses, with single-request TOTP ({ totpRequired: true } fallback if a 2FA code is missing/invalid).
  • token.signToken gains a backward-compatible expiresIn option for the short access-token TTL.
  • Web login / TOTP / cookie flow: untouched.

Part 3 — Pluggable SSO (Google, Discord, generic OIDC)

  • Shared OAuth2Provider base + 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/…). A registry handles instantiation, config validation, and health.
  • Public discovery GET /api/v1/auth/providers; redirect flow GET /auth/sso/:provider/{start,link,callback}, CSRF-protected with a signed httpOnly transaction cookie + PKCE.
  • Link-only policy (security): an SSO login succeeds only if the external identity is already linked to an existing account (linked by the user from Account). External identities are never auto-provisioned — no one gains admin access without an account you created.
  • OAuth client secrets are encrypted at rest (AES-256-GCM, utils/secretBox.js) and never returned to any client (hasSecret flag only).
  • Admin CRUD /admin/auth/providers (built-ins enable-only/undeletable) and account linking /admin/account/identities. New auth_providers + user_identities tables.
  • SSO logins go through sessionService, so login/activity logging, RBAC (role from the linked user), and bot protection are identical to a local login.

Frontend

  • Login page renders provider buttons from 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).
  • New Authentication admin view (/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.
  • Account page gains a linked-accounts section (link → SSO link redirect, unlink).

Database

Three new tables, all added idempotently to schema.sql (applied by ensureSchema() 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 from JWT_SECRET), APP_BASE_URL (builds the OAuth redirect_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.example updated.

Not included / follow-ups

  • No changes to web login/TOTP/logout, the cookie, or the mobile flow from SSO work.
  • Live end-to-end OAuth smoke (real Google/Discord apps against a running DB) has not been run — needs real credentials and a registered redirect URI (${APP_BASE_URL}/api/v1/auth/sso/:provider/callback).
  • Session revocation for cookie/mobile beyond refresh-token rotation remains a documented stub hook (invalidateSession / invalidateAllUserSessions).

🤖 Generated with Claude Code

## 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: ```js sessionService.createSession(user, authMethod) // authMethod ∈ local · totp · mobile · google · discord · oidc ``` Delivered in three parts, backend-first, each verified before the next. --- ## Part 1 — Session abstraction (backward-compatible refactor) - New `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`), and `providers/` contracts. - `utils/auth.js` becomes a **thin compatibility facade** re-exporting the same names, so every existing import site (routes, middleware, `siteMode`) is untouched. - The session token already validated from a cookie **or** `Authorization: Bearer`, so bearer support was in place before Part 2. - **Zero behavior change** — purely structural. ## 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**. - Refresh tokens are **stored hashed** (never in the clear) in a new `mobile_refresh_tokens` table and **rotated on every refresh** — a replayed refresh token is single-use. Logout revokes one or all. - Mobile login reuses the web bot-scoring + backoff defenses, with **single-request TOTP** (`{ totpRequired: true }` fallback if a 2FA code is missing/invalid). - `token.signToken` gains a backward-compatible `expiresIn` option for the short access-token TTL. - Web login / TOTP / cookie flow: untouched. ## Part 3 — Pluggable SSO (Google, Discord, generic OIDC) - Shared `OAuth2Provider` base + 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/…). A `registry` handles instantiation, config validation, and health. - Public discovery `GET /api/v1/auth/providers`; redirect flow `GET /auth/sso/:provider/{start,link,callback}`, CSRF-protected with a signed httpOnly transaction cookie + **PKCE**. - **Link-only policy (security):** an SSO login succeeds *only* if the external identity is already linked to an existing account (linked by the user from **Account**). External identities are **never auto-provisioned** — no one gains admin access without an account you created. - OAuth **client secrets are encrypted at rest** (AES-256-GCM, `utils/secretBox.js`) and never returned to any client (`hasSecret` flag only). - Admin CRUD `/admin/auth/providers` (built-ins enable-only/undeletable) and account linking `/admin/account/identities`. New `auth_providers` + `user_identities` tables. - SSO logins go through `sessionService`, so login/activity logging, RBAC (role from the linked user), and bot protection are identical to a local login. ## Frontend - **Login page** renders provider buttons from `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`). - New **Authentication** admin view (`/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. - **Account** page gains a linked-accounts section (link → SSO link redirect, unlink). --- ## Database Three new tables, all added idempotently to `schema.sql` (applied by `ensureSchema()` 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 from `JWT_SECRET`), `APP_BASE_URL` (builds the OAuth `redirect_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.example` updated. ## Not included / follow-ups - No changes to web login/TOTP/logout, the cookie, or the mobile flow from SSO work. - **Live end-to-end OAuth smoke** (real Google/Discord apps against a running DB) has not been run — needs real credentials and a registered redirect URI (`${APP_BASE_URL}/api/v1/auth/sso/:provider/callback`). - Session revocation for cookie/mobile beyond refresh-token rotation remains a documented stub hook (`invalidateSession` / `invalidateAllUserSessions`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-07-03 15:33:57 +00:00
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>
whitlocktech approved these changes 2026-07-03 15:35:25 +00:00
whitlocktech merged commit 86e44a94a2 into main 2026-07-03 15:35:46 +00:00
whitlocktech deleted branch feature/auth-session-abstraction 2026-07-03 15:35:47 +00:00
Sign in to join this conversation.
No description provided.