import type { NextFunction, Request, Response } from 'express'; import jwt from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; const CLIENT_ID = process.env.MSAL_CLIENT_ID ?? ''; const TENANT_ID = process.env.MSAL_TENANT_ID ?? ''; export const JWT_SECRET = process.env.JWT_SECRET ?? ''; const jwks = jwksClient({ jwksUri: `https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys`, cache: true, cacheMaxAge: 12 * 60 * 60 * 1000 }); function getKey(header: jwt.JwtHeader, callback: (err: Error | null, key?: string) => void) { if (!header.kid) { callback(new Error('Kein kid im Token-Header')); return; } jwks.getSigningKey(header.kid, (err, key) => { if (err || !key) { callback(err ?? new Error('Kein Signing-Key gefunden')); return; } callback(null, key.getPublicKey()); }); } export interface AuthedRequest extends Request { user?: { email: string; name: string; oid: string }; } // Zwei Anmeldearten werden akzeptiert: // - Microsoft Entra ID (RS256, per JWKS von login.microsoftonline.com verifiziert) // - Lokales Konto (HS256, mit unserem eigenen JWT_SECRET signiert, siehe routes/auth.ts) // Unterschieden wird anhand des "alg" im (unverifizierten) Token-Header. export function requireAuth(req: AuthedRequest, res: Response, next: NextFunction) { const header = req.headers.authorization; if (!header?.startsWith('Bearer ')) { res.status(401).json({ error: 'Kein Token übermittelt.' }); return; } const token = header.slice('Bearer '.length); const unverifiedHeader = jwt.decode(token, { complete: true })?.header; if (unverifiedHeader?.alg === 'HS256') { if (!JWT_SECRET) { res.status(500).json({ error: 'JWT_SECRET ist serverseitig nicht konfiguriert.' }); return; } jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] }, (err, decoded) => { if (err || !decoded || typeof decoded === 'string') { res.status(401).json({ error: 'Token ungültig: ' + (err?.message ?? 'unbekannt') }); return; } const claims = decoded as jwt.JwtPayload & { username?: string; name?: string; oid?: string }; req.user = { email: claims.username ?? '', name: claims.name ?? claims.username ?? '', oid: claims.oid ?? '' }; next(); }); return; } jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => { if (err || !decoded || typeof decoded === 'string') { res.status(401).json({ error: 'Token ungültig: ' + (err?.message ?? 'unbekannt') }); return; } const claims = decoded as jwt.JwtPayload & { tid?: string; aud?: string; preferred_username?: string; upn?: string; name?: string; oid?: string }; if (claims.tid !== TENANT_ID) { res.status(401).json({ error: 'Falscher Tenant.' }); return; } if (claims.aud !== CLIENT_ID) { res.status(401).json({ error: 'Token nicht für diese App ausgestellt.' }); return; } const email = claims.preferred_username ?? claims.upn ?? ''; req.user = { email, name: claims.name ?? email, oid: claims.oid ?? '' }; next(); }); }