feat(auth): native SSO authorization bridge for the Android app
Add a Mobile SSO Authorization Bridge so the native app can "Sign in with Google/Discord" without shipping any OAuth secret. It EXTENDS the existing /auth/sso/* redirect flow (same PKCE-vs-IdP, link-only + opt-in provisioning, TOTP gate) and terminates in the existing mobile bearer tokens — not a parallel auth path. - Schema: mobile_auth_sessions + mobile_auth_codes (short-lived, self-pruning; authorization code stored hash-only, PKCE challenge is a hash by construction). - GET /auth/mobile/sso/start: validate provider enabled + redirect_uri by EXACT allowlist match (never prefix), seed a bridge session, reuse the SSO redirect tagged mode:'mobile' (new redirectToIdp helper extracted from beginFlow). - SSO callback + finishSsoTotp gain a mode:'mobile' branch: mint a single-use, hashed, PKCE-bound code and redirect to the fixed app callback (code + echoed state, never a token) instead of setting a cookie. 2FA keeps full parity via the existing web TOTP form (now carrying the bridge session). - POST /auth/mobile/sso/exchange: verify Layer-B PKCE (before burning the code), single-use consume, then issue the SAME pair as /auth/mobile/login. - Discovery reuses GET /auth/providers; refresh/logout reuse /auth/mobile/*. - Rate limits: /start per-IP+provider, /exchange per-IP. Boot-time + opportunistic prune of both tables (no cron, mirrors revoked_sessions). - Redirect allowlist is MOBILE_AUTH_REDIRECT_URIS (default the one fixed runicgateway://auth/callback); App Link URIs can be appended per shard later. - Swagger regenerated; 39 tests (model single-use/gating + full controller matrix: bad/expired/reused code, PKCE mismatch, disabled provider, redirect allowlist, TOTP-through-bridge). Full suite green (271). Refs docs/website/BACKEND_DESIGN.md, docs/android/PLAN.md §9 (M9). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -197,6 +197,41 @@ CREATE TABLE IF NOT EXISTS mobile_refresh_tokens (
|
||||
INDEX idx_mrt_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Mobile SSO authorization bridge (M9). Two short-lived, self-pruning tables that
|
||||
-- bridge a browser SSO redirect flow to the native app. They carry the app↔website
|
||||
-- PKCE + CSRF state (a SECOND PKCE layer, distinct from the website↔IdP PKCE the
|
||||
-- sso_tx cookie already carries) and the one-time code the app trades for bearer
|
||||
-- tokens. No secret is stored in the clear: code_challenge is a hash by construction
|
||||
-- and the authorization code is stored as a sha256 hash only (same pattern as
|
||||
-- mobile_refresh_tokens / user_invites / password_resets). See docs BACKEND_DESIGN §3/§4.
|
||||
CREATE TABLE IF NOT EXISTS mobile_auth_sessions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id CHAR(36) NOT NULL UNIQUE, -- uuid; carried inside the signed sso_tx (mode 'mobile')
|
||||
provider VARCHAR(40) NOT NULL, -- provider id, validated enabled at /start
|
||||
code_challenge VARCHAR(255) NOT NULL, -- app-supplied PKCE S256 challenge (base64url)
|
||||
redirect_uri VARCHAR(255) NOT NULL, -- app callback; EXACT-match against the allowlist
|
||||
state VARCHAR(255) NOT NULL, -- app-generated opaque CSRF value, echoed to the app
|
||||
status ENUM('pending','completed','consumed') NOT NULL DEFAULT 'pending',
|
||||
user_id INT NULL, -- set once SSO resolves the account
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL, -- ~10 min (one redirect round-trip incl. TOTP)
|
||||
used_at DATETIME NULL, -- stamped at exchange
|
||||
CONSTRAINT fk_mas_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_mas_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mobile_auth_codes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
code_hash CHAR(64) NOT NULL UNIQUE, -- sha256 hex of the opaque >=128-bit code
|
||||
user_id INT NOT NULL,
|
||||
session_id CHAR(36) NOT NULL, -- owning mobile_auth_sessions.session_id (ties code→PKCE challenge)
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL, -- very short (~5 min)
|
||||
used_at DATETIME NULL, -- set on first successful exchange (single use)
|
||||
CONSTRAINT fk_mac_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_mac_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Denylist of revoked web/cookie session tokens, keyed on the JWT `jti` minted
|
||||
-- per session in createSession. A single logout adds this session's jti here;
|
||||
-- requireAuth rejects any token whose jti is present. Rows self-expire: expires_at
|
||||
|
||||
Reference in New Issue
Block a user