feat: Add OIDC authentication with Authentik integration

- Add OIDC login flow with Authentik provider
- Implement session-based auth with Redis store
- Add avatar display from OIDC claims
- Fix input field performance with react-textarea-autosize
- Stabilize callbacks to prevent unnecessary re-renders
- Fix history loading to skip empty session files
- Add 2-row default height for input textarea

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 06:07:22 +01:00
parent cfee1711dc
commit 1186cb1b5e
23 changed files with 2884 additions and 87 deletions

146
backend/utils/oidc.js Normal file
View File

@@ -0,0 +1,146 @@
// OIDC Client Wrapper using openid-client
import { Issuer, generators } from 'openid-client';
import { authConfig } from '../config/auth.js';
let oidcClient = null;
let issuer = null;
/**
* Initialize the OIDC client by discovering the issuer
*/
export async function initializeOIDC() {
if (!authConfig.app.authEnabled) {
console.log('[OIDC] Authentication disabled, skipping initialization');
return null;
}
try {
console.log(`[OIDC] Discovering issuer: ${authConfig.oidc.issuer}`);
issuer = await Issuer.discover(authConfig.oidc.issuer);
console.log(`[OIDC] Discovered issuer: ${issuer.issuer}`);
oidcClient = new issuer.Client({
client_id: authConfig.oidc.clientId,
client_secret: authConfig.oidc.clientSecret,
redirect_uris: [authConfig.oidc.redirectUri],
response_types: ['code'],
});
console.log('[OIDC] Client initialized successfully');
return oidcClient;
} catch (error) {
console.error('[OIDC] Failed to initialize client:', error.message);
throw error;
}
}
/**
* Get the initialized OIDC client
*/
export function getClient() {
if (!oidcClient) {
throw new Error('OIDC client not initialized. Call initializeOIDC() first.');
}
return oidcClient;
}
/**
* Generate authorization URL for login
*/
export function getAuthorizationUrl(state, nonce, codeVerifier) {
const client = getClient();
const codeChallenge = generators.codeChallenge(codeVerifier);
return client.authorizationUrl({
scope: authConfig.oidc.scopes.join(' '),
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
}
/**
* Exchange authorization code for tokens
*/
export async function exchangeCode(code, codeVerifier, nonce) {
const client = getClient();
const tokenSet = await client.callback(
authConfig.oidc.redirectUri,
{ code },
{ code_verifier: codeVerifier, nonce }
);
return tokenSet;
}
/**
* Validate and decode ID token claims
*/
export function getIdTokenClaims(tokenSet) {
return tokenSet.claims();
}
/**
* Get user info from the userinfo endpoint
*/
export async function getUserInfo(accessToken) {
const client = getClient();
return await client.userinfo(accessToken);
}
/**
* Refresh access token using refresh token
*/
export async function refreshTokens(refreshToken) {
const client = getClient();
return await client.refresh(refreshToken);
}
/**
* Get end session URL for logout
*/
export function getEndSessionUrl(idTokenHint, postLogoutRedirectUri) {
const client = getClient();
return client.endSessionUrl({
id_token_hint: idTokenHint,
post_logout_redirect_uri: postLogoutRedirectUri,
});
}
/**
* Generate random state for CSRF protection
*/
export function generateState() {
return generators.state();
}
/**
* Generate random nonce for ID token validation
*/
export function generateNonce() {
return generators.nonce();
}
/**
* Generate code verifier for PKCE
*/
export function generateCodeVerifier() {
return generators.codeVerifier();
}
export default {
initializeOIDC,
getClient,
getAuthorizationUrl,
exchangeCode,
getIdTokenClaims,
getUserInfo,
refreshTokens,
getEndSessionUrl,
generateState,
generateNonce,
generateCodeVerifier,
};