'use strict'; /** * OIDC / authentik authentication routes. * * GET /api/auth/oidc/login — initiates authorization-code + PKCE flow * GET /api/auth/oidc/callback — handles redirect from the identity provider * * Returns 501 if OIDC is not configured or not enabled by admin. * Local auth is unaffected. Tokens are never logged or forwarded to the frontend. */ const express = require('express'); const router = express.Router(); const { createSession, cookieOpts, COOKIE_NAME } = require('../services/authService'); const { getOidcConfig, isOidcLoginActive, createLoginState, consumeLoginState, buildAuthorizationUrl, exchangeAndVerifyTokens, findOrProvisionUser, } = require('../services/oidcService'); const NOT_ACTIVE = { error: 'OIDC authentication is not configured or not enabled on this server' }; // ── GET /api/auth/oidc/login ────────────────────────────────────────────────── // Validates that OIDC is active, generates PKCE + nonce + state, stores in DB, // then redirects the user to the identity provider's authorization endpoint. router.get('/login', async (req, res) => { if (!isOidcLoginActive()) return res.status(501).json(NOT_ACTIVE); const config = getOidcConfig(); if (!config) return res.status(501).json(NOT_ACTIVE); try { const redirectTo = typeof req.query.redirect_to === 'string' ? req.query.redirect_to : null; const state = createLoginState(redirectTo); const authUrl = await buildAuthorizationUrl(config, state); res.redirect(authUrl); } catch (err) { console.error('[oidc] Login initiation error:', err.message); res.status(502).json({ error: 'Failed to reach the identity provider. Please try again.' }); } }); // ── GET /api/auth/oidc/callback ─────────────────────────────────────────────── // Receives code + state from the identity provider after user authentication. // // Full validation via openid-client@5: // • State matches saved state (replay prevention) // • PKCE code_verifier / code_challenge // • JWT signature via JWKS (cryptographic verification) // • Issuer, audience, expiry, nonce claims // // On success: creates a local session, sets cookie, redirects to frontend. // On any failure: redirects with a safe oidc_error query parameter. // Tokens are never logged or forwarded. router.get('/callback', async (req, res) => { if (!isOidcLoginActive()) return res.redirect('/?oidc_error=not_configured'); const config = getOidcConfig(); if (!config) return res.redirect('/?oidc_error=not_configured'); const { code, state: stateId, error } = req.query; // Provider signalled an authorization error — log code only, not description (may have PII) if (error) { console.error('[oidc] Provider error on callback:', String(error).slice(0, 40)); return res.redirect('/?oidc_error=authorization_failed'); } if (!code || !stateId || typeof code !== 'string' || typeof stateId !== 'string') { return res.redirect('/?oidc_error=invalid_callback'); } // Consume the one-time state — validates expiry and prevents replay const savedState = consumeLoginState(stateId); if (!savedState) { return res.redirect('/?oidc_error=invalid_or_expired_state'); } try { // Full token exchange + cryptographic ID token verification via openid-client@5. // Verifies: state, PKCE, JWT signature (JWKS), issuer, audience, expiry, nonce. // Throws on any validation failure. Tokens are never logged. const claims = await exchangeAndVerifyTokens(config, code, stateId, savedState); // Map verified claims to a local user account const user = await findOrProvisionUser(claims, config); // Create a local session — same mechanism as local login const session = await createSession(user.id); if (!session) throw new Error('Failed to create local session after OIDC login'); res.cookie(COOKIE_NAME, session.sessionId, cookieOpts(req)); res.redirect(savedState.redirect_to || '/'); } catch (err) { // Log message only — never log tokens, codes, or ID token contents console.error('[oidc] Callback error:', err.message); const errCode = err.status === 403 ? 'access_denied' : 'authentication_failed'; res.redirect(`/?oidc_error=${errCode}`); } }); module.exports = router;