import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, } from '@nestjs/common'; import { Request } from 'express'; import { createRemoteJWKSet, jwtVerify } from 'jose'; import { IdentityData } from '../interfaces/identity-data'; import * as jwt from 'jsonwebtoken'; /** * JwtAuthGuard verifies JWTs issued by the external Identity Provider (Authentik) * using JWKS (RS256). It also accepts internal RayLab JWTs signed with a local secret. */ @Injectable() export class JwtAuthGuard implements CanActivate { private jwks: ReturnType | null = null; constructor() {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const authHeader = request.headers.authorization; if (!authHeader) { throw new UnauthorizedException('Authorization header is missing.'); } const [type, token] = authHeader.split(' '); if (type !== 'Bearer' || !token) { throw new UnauthorizedException('Invalid authorization header.'); } const jwksUri = process.env.AUTHENTIK_JWKS_URI; // First try verifying with external JWKS (Authentik) if (jwksUri) { try { if (!this.jwks) this.jwks = createRemoteJWKSet(new URL(jwksUri)); const { payload } = await jwtVerify(token, this.jwks, { issuer: process.env.AUTHENTIK_ISSUER, audience: process.env.AUTHENTIK_AUDIENCE, }); const identity = new IdentityData( payload.sub as string, (payload as any).preferred_username as string | undefined, (payload as any).email as string | undefined, payload as Record, ); request.identity = identity; return true; } catch (err) { // ignore and try internal verification } } // Fallback: verify with internal symmetric secret const secret = process.env.RAYLAB_JWT_SECRET; if (!secret) { throw new UnauthorizedException('Invalid or expired token.'); } try { const payload = jwt.verify(token, secret) as any; const identity = new IdentityData( payload.sub as string, payload.preferred_username as string | undefined, payload.email as string | undefined, payload as Record, ); request.identity = identity; return true; } catch (err: any) { throw new UnauthorizedException('Invalid or expired token.'); } } }