mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
The web OIDC flow ends in a session cookie the Bearer-transport native client cannot consume, so on an OIDC_ONLY instance the iOS app had no sign-in. Add a native leg: the app opens the login URL with client=native and its own PKCE S256 challenge inside an in-app browser; the callback resolves identity exactly as before, then mints a one-time handoff code and 302s to the fixed compiled-in scheme healthlog://oidc-callback (or hands back the existing MFA ticket when the account has a second factor). The app exchanges the code plus its PKCE verifier at POST /api/auth/oidc/native/token for the standard native token bundle. The return address is a compile-time constant — no redirect_uri parameter and no env allowlist, so the open-redirect / scheme-hijack class is removed by construction. The native flag and app challenge travel only inside the AES-256-GCM state blob, so a network attacker or the IdP cannot flip a web login into a native one. The handoff code is 256-bit CSPRNG, HMAC-hashed at rest, 90-second TTL, single-use via a guarded consume; a failed PKCE check burns it. A replay of a consumed code revokes exactly the pair the first exchange issued. The token pair never rides a URL; only the opaque code or the MFA ticket do. The SSO login can never satisfy requireFreshMfa step-up — the transport is Bearer and step-up is cookie-only. Adds the OidcNativeHandoff model (migration 0252), the token-exchange route, a daily cleanup sweep, and the OpenAPI entry.
49 lines
2.0 KiB
SQL
49 lines
2.0 KiB
SQL
-- Native OIDC SSO session handoff (iOS#49).
|
|
--
|
|
-- One-time handoff codes bridging the browser-context OIDC callback to the
|
|
-- cookie-less native token exchange. The web OIDC flow ends in a session
|
|
-- cookie a Bearer-transport native client can never consume; this table lets
|
|
-- the callback mint an opaque, PKCE-locked, single-use code that the app
|
|
-- exchanges for the standard native token bundle at
|
|
-- POST /api/auth/oidc/native/token.
|
|
--
|
|
-- The raw code (`hlh_<…>`) rides ONLY the custom-scheme redirect; only its
|
|
-- HMAC-SHA256 hash is stored, mirroring the refresh-token / MFA-challenge
|
|
-- hash-at-rest posture. `issued_refresh_token_hash` records the pair minted at
|
|
-- exchange so a replay (a second presentation after consumption) can revoke
|
|
-- exactly that family member — refresh-token reuse-detection reach-back.
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "oidc_native_handoffs" (
|
|
"id" TEXT NOT NULL,
|
|
"user_id" TEXT NOT NULL,
|
|
"code_hash" TEXT NOT NULL,
|
|
"code_challenge" TEXT NOT NULL,
|
|
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
"consumed_at" TIMESTAMP(3),
|
|
"issued_refresh_token_hash" TEXT,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"ip_address" TEXT,
|
|
"user_agent" TEXT,
|
|
CONSTRAINT "oidc_native_handoffs_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex — the exchange loads a row by the code's hash; unique so a hash
|
|
-- collision can never resolve to two live codes.
|
|
CREATE UNIQUE INDEX "oidc_native_handoffs_code_hash_key"
|
|
ON "oidc_native_handoffs" ("code_hash");
|
|
|
|
-- CreateIndex — per-user audit scans + the FK-cascade delete path.
|
|
CREATE INDEX "oidc_native_handoffs_user_id_idx"
|
|
ON "oidc_native_handoffs" ("user_id");
|
|
|
|
-- CreateIndex — the daily cleanup sweep filters on `expires_at`.
|
|
CREATE INDEX "oidc_native_handoffs_expires_at_idx"
|
|
ON "oidc_native_handoffs" ("expires_at");
|
|
|
|
-- AddForeignKey — deleting a user cascades their handoff rows away.
|
|
ALTER TABLE "oidc_native_handoffs"
|
|
ADD CONSTRAINT "oidc_native_handoffs_user_id_fkey"
|
|
FOREIGN KEY ("user_id") REFERENCES "users" ("id")
|
|
ON DELETE CASCADE ON UPDATE CASCADE;
|