From 0d084cd5464fd630bc347bec8714883d42b6bc10 Mon Sep 17 00:00:00 2001 From: Simon Franken Date: Wed, 18 Feb 2026 22:45:38 +0100 Subject: [PATCH] update --- backend/src/routes/auth.routes.ts | 56 ++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 0609093..b2a3b6a 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -7,7 +7,8 @@ import { getUserInfo, } from "../auth/oidc"; import { requireAuth, syncUser } from "../middleware/auth"; -import type { AuthenticatedRequest } from "../types"; +import type { AuthenticatedRequest, AuthenticatedUser } from "../types"; +import type { AuthSession } from "../auth/oidc"; const router = Router(); @@ -21,6 +22,26 @@ async function ensureOIDC() { } } +// Short-lived store for native app OIDC sessions, keyed by state. +// Entries are cleaned up after 10 minutes regardless of use. +const nativeOidcSessions = new Map(); +const NATIVE_SESSION_TTL_MS = 10 * 60 * 1000; + +function storeNativeSession(session: AuthSession): void { + nativeOidcSessions.set(session.state, { + session, + expiresAt: Date.now() + NATIVE_SESSION_TTL_MS, + }); +} + +function popNativeSession(state: string): AuthSession | null { + const entry = nativeOidcSessions.get(state); + if (!entry) return null; + nativeOidcSessions.delete(state); + if (Date.now() > entry.expiresAt) return null; + return entry.session; +} + // GET /auth/login - Initiate OIDC login flow router.get("/login", async (req, res) => { try { @@ -28,7 +49,15 @@ router.get("/login", async (req, res) => { const redirectUri = req.query.redirect_uri as string | undefined; const session = createAuthSession(redirectUri); - req.session.oidc = session; + + if (redirectUri) { + // Native app flow: store session by state so /auth/token can retrieve it + // without relying on the browser cookie jar. + storeNativeSession(session); + } else { + // Web flow: store session in the cookie-backed server session as before. + req.session.oidc = session; + } const authorizationUrl = getAuthorizationUrl(session, redirectUri); res.redirect(authorizationUrl); @@ -38,7 +67,7 @@ router.get("/login", async (req, res) => { } }); -// GET /auth/callback - OIDC callback handler +// GET /auth/callback - OIDC callback handler (web frontend only) router.get("/callback", async (req, res) => { try { await ensureOIDC(); @@ -89,9 +118,9 @@ router.get("/me", requireAuth, (req: AuthenticatedRequest, res) => { res.json(req.user); }); -// POST /auth/token - Exchange authorization code for tokens (for native apps) -// The session cookie set during /auth/login is sent automatically by ASWebAuthenticationSession, -// so req.session.oidc contains the original state/nonce for validation. +// POST /auth/token - Exchange authorization code for tokens (native app flow) +// Session state is retrieved from the in-memory store by state value, so no +// session cookie is required from the native client. router.post("/token", async (req, res) => { try { await ensureOIDC(); @@ -103,20 +132,12 @@ router.post("/token", async (req, res) => { return; } - const oidcSession = req.session.oidc; + const oidcSession = popNativeSession(state); if (!oidcSession) { - res.status(400).json({ error: "No active OIDC session. Initiate login first." }); + res.status(400).json({ error: "OIDC session not found or expired. Initiate login again." }); return; } - // Validate that the returned state matches what was stored in the session - if (oidcSession.state !== state) { - res.status(400).json({ error: "State mismatch" }); - return; - } - - // Use the session's own codeVerifier (generated by the backend during /auth/login) - // rather than a client-supplied one, to prevent verifier substitution attacks. const session = { codeVerifier: oidcSession.codeVerifier, state: oidcSession.state, @@ -129,9 +150,6 @@ router.post("/token", async (req, res) => { const user = await getUserInfo(tokenSet); await syncUser(user); - // Clear OIDC session state now that it has been consumed - delete req.session.oidc; - res.json({ access_token: tokenSet.access_token, id_token: tokenSet.id_token,