mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(auth): native OIDC SSO session handoff for the iOS client
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.
This commit is contained in:
1 parent
74891e95f4
commit
e6d9cc229a
16 files changed
+900
-70
No files matched your search
@@ -1562,6 +1562,38 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/oidc/native/token:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Exchange a native OIDC handoff code for the token bundle
|
||||
description: >-
|
||||
The cookie-less native leg of the OIDC SSO flow. The iOS app opens `GET
|
||||
/api/auth/oidc/login?client=native&code_challenge=<S256>` inside an `ASWebAuthenticationSession`; the callback
|
||||
returns a one-time handoff code on `healthlog://oidc-callback?code=hlh_…` (or an `mfa_ticket` when the account
|
||||
has a second factor, completed at /api/auth/mfa/verify). This endpoint exchanges the code + its PKCE
|
||||
`codeVerifier` for the SAME native bundle password login issues.
|
||||
|
||||
|
||||
Requires the native transport (no cookie, non-browser UA) — a browser is rejected. The code is single-use and
|
||||
expires in ~90 seconds; a replay of a consumed code revokes the pair the first exchange issued. A single generic
|
||||
401 covers every invalid/expired/used/PKCE-mismatch case.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OidcNativeTokenRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Handoff accepted — native access + refresh bundle.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OidcNativeTokenResponse"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/me/mfa/totp/setup:
|
||||
post:
|
||||
tags:
|
||||
@@ -11015,6 +11047,21 @@ components:
|
||||
- mfaTicket
|
||||
- code
|
||||
description: Complete a second-factor login challenge and receive the same token bundle the password path issues.
|
||||
OidcNativeTokenRequest:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
pattern: ^hlh_[A-Za-z0-9_-]{43}$
|
||||
description: One-time handoff code from the healthlog://oidc-callback?code= redirect.
|
||||
codeVerifier:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9\-._~]{43,128}$
|
||||
description: The app's PKCE code_verifier (RFC 7636).
|
||||
required:
|
||||
- code
|
||||
- codeVerifier
|
||||
description: Exchange a one-time native OIDC handoff code (+ its PKCE verifier) for the standard native token bundle.
|
||||
MfaDisableRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -14874,6 +14921,23 @@ components:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
OidcNativeTokenResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/AccessRefreshBundle"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
TotpSetupEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user