107 lines
4.4 KiB
JavaScript
107 lines
4.4 KiB
JavaScript
'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;
|