import { createRemoteJWKSet, jwtVerify } from 'jose' let jwks function authConfig() { const issuer = process.env.KEYCLOAK_ISSUER?.replace(/\/$/, '') const clientId = process.env.KEYCLOAK_CLIENT_ID return { issuer, clientId } } export function publicAuthConfig() { const { issuer, clientId } = authConfig() return { authRequired: process.env.AUTH_REQUIRED !== 'false', issuer: issuer || '', clientId: clientId || '' } } export async function requireAuth(req, res, next) { if (process.env.AUTH_REQUIRED === 'false') { req.user = { sub: 'local-dev', preferred_username: 'local-dev' } return next() } try { const { issuer, clientId } = authConfig() if (!issuer || !clientId) throw new Error('Keycloak is not configured') const header = req.headers.authorization || '' if (!header.startsWith('Bearer ')) return res.status(401).json({ error: 'missing_bearer_token' }) jwks ||= createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`)) const { payload } = await jwtVerify(header.slice(7), jwks, { issuer }) const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud].filter(Boolean) if (payload.azp !== clientId && !aud.includes(clientId)) { return res.status(403).json({ error: 'wrong_client' }) } req.user = payload next() } catch (error) { res.status(401).json({ error: 'invalid_token', detail: error.message }) } }