diff --git a/backend/src/auth/oidc.ts b/backend/src/auth/oidc.ts index 8e3bfab..3146a64 100644 --- a/backend/src/auth/oidc.ts +++ b/backend/src/auth/oidc.ts @@ -41,11 +41,11 @@ export interface AuthSession { codeVerifier: string; state: string; nonce: string | undefined; - redirectUri?: string; + redirectUri: string; } -export function createAuthSession(redirectUri?: string): AuthSession { - const isNative = !!redirectUri; +export function createAuthSession(redirectUri: string): AuthSession { + const isNative = redirectUri.startsWith('timetracker://'); return { codeVerifier: generators.codeVerifier(), state: generators.state(), @@ -58,25 +58,22 @@ export function createAuthSession(redirectUri?: string): AuthSession { }; } -export function getAuthorizationUrl(session: AuthSession, redirectUri?: string): string { +export function getAuthorizationUrl(session: AuthSession): string { const client = getOIDCClient(); const codeChallenge = generators.codeChallenge(session.codeVerifier); - + const params: Record = { scope: 'openid profile email', state: session.state, code_challenge: codeChallenge, code_challenge_method: 'S256', + redirect_uri: session.redirectUri, }; if (session.nonce) { params.nonce = session.nonce; } - - if (redirectUri) { - params.redirect_uri = redirectUri; - } - + return client.authorizationUrl(params); } @@ -85,9 +82,7 @@ export async function handleCallback( session: AuthSession ): Promise { const client = getOIDCClient(); - - const redirectUri = session.redirectUri || config.oidc.redirectUri; - + const checks: Record = { code_verifier: session.codeVerifier, state: session.state, @@ -98,11 +93,11 @@ export async function handleCallback( } const tokenSet = await client.callback( - redirectUri, + session.redirectUri, params, checks, ); - + return tokenSet; } diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 65c641e..19a75c1 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -9,6 +9,7 @@ import { } from "../auth/oidc"; import { signBackendJwt } from "../auth/jwt"; import { requireAuth, syncUser } from "../middleware/auth"; +import { config } from "../config"; import type { AuthenticatedRequest } from "../types"; import type { AuthSession } from "../auth/oidc"; @@ -49,11 +50,18 @@ router.get("/login", async (req, res) => { try { await ensureOIDC(); - const redirectUri = req.query.redirect_uri as string | undefined; - console.log(`[auth/login] initiated (redirect_uri: ${redirectUri ?? '(web flow)'})`); + const isNativeFlow = !!req.query.redirect_uri; + // For the web flow no redirect_uri is supplied in the request — use the + // backend-configured value so the IDP always receives an explicit URI + // rather than relying on the openid-client library to pick a default. + const redirectUri = isNativeFlow + ? (req.query.redirect_uri as string) + : config.oidc.redirectUri; + + console.log(`[auth/login] initiated (redirect_uri: ${redirectUri})`); const session = createAuthSession(redirectUri); - if (redirectUri) { + if (isNativeFlow) { // Native app flow: store session by state so /auth/token can retrieve it // without relying on the browser cookie jar. storeNativeSession(session); @@ -63,7 +71,7 @@ router.get("/login", async (req, res) => { req.session.oidc = session; } - const authorizationUrl = getAuthorizationUrl(session, redirectUri); + const authorizationUrl = getAuthorizationUrl(session); console.log(`[auth/login] redirecting to IDP`); res.redirect(authorizationUrl); } catch (error) {