kaboot/server/src/middleware/auth.ts

84 lines
2.1 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const OIDC_ISSUER = process.env.OIDC_ISSUER || 'http://localhost:9000/application/o/kaboot/';
const OIDC_JWKS_URI = process.env.OIDC_JWKS_URI || 'http://localhost:9000/application/o/kaboot/jwks/';
const OIDC_INTERNAL_JWKS_URI = process.env.OIDC_INTERNAL_JWKS_URI || OIDC_JWKS_URI;
const client = jwksClient({
jwksUri: OIDC_INTERNAL_JWKS_URI,
cache: true,
cacheMaxAge: 600000,
rateLimit: true,
jwksRequestsPerMinute: 10,
});
function getSigningKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback): void {
if (!header.kid) {
callback(new Error('No kid in token header'));
return;
}
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
return;
}
const signingKey = key?.getPublicKey();
callback(null, signingKey);
});
}
export interface AuthenticatedUser {
sub: string;
preferred_username: string;
email?: string;
name?: string;
groups?: string[];
}
export interface AuthenticatedRequest extends Request {
user?: AuthenticatedUser;
}
export function requireAuth(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Missing or invalid authorization header' });
return;
}
const token = authHeader.slice(7);
jwt.verify(
token,
getSigningKey,
{
issuer: OIDC_ISSUER,
algorithms: ['RS256'],
},
(err, decoded) => {
if (err) {
console.error('Token verification failed:', err.message);
res.status(401).json({ error: 'Invalid token', details: err.message });
return;
}
const payload = decoded as jwt.JwtPayload;
req.user = {
sub: payload.sub!,
preferred_username: payload.preferred_username || payload.sub!,
email: payload.email,
name: payload.name,
groups: payload.groups || [],
};
next();
}
);
}