import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ConfigService } from '@nestjs/config'; import { Strategy } from 'passport-jwt'; import * as jwksRsa from 'jwks-rsa'; import { Request } from 'express'; import { PrismaClient } from '../prisma/prisma.module'; import { SyncService } from '../sync/sync.service'; import { AuthenticatedUser } from './authenticated-request'; import { resolveOrProvisionAuthentikUser } from './provision-user'; interface AuthentikJwtPayload { sub: string; email?: string; given_name?: string; family_name?: string; } /// Validates access tokens issued by Authentik (resource-server pattern): /// signature is checked against Authentik's JWKS, the local `User` is /// provisioned on first login (JIT), then the local Membership table decides /// what the user may do. Authentik itself is only the identity source, never /// asked for authorization here. @Injectable() export class AuthentikStrategy extends PassportStrategy(Strategy, 'authentik') { constructor( config: ConfigService, private readonly prisma: PrismaClient, private readonly sync: SyncService, ) { const issuerUrl = config.getOrThrow('AUTHENTIK_ISSUER_URL'); super({ jwtFromRequest: (req: Request) => req.headers.authorization?.startsWith('Bearer ') ? req.headers.authorization.slice('Bearer '.length) : null, secretOrKeyProvider: jwksRsa.passportJwtSecret({ jwksUri: `${issuerUrl}/jwks/`, cache: true, rateLimit: true, }), issuer: issuerUrl, algorithms: ['RS256'], }); } async validate(payload: AuthentikJwtPayload): Promise { if (!payload.email) { throw new UnauthorizedException('Authentik token missing email claim'); } const user = await resolveOrProvisionAuthentikUser(this.prisma, this.sync, { sub: payload.sub, email: payload.email, firstName: payload.given_name ?? '', lastName: payload.family_name ?? '', }); return { userId: user.id, authentikSub: user.authentikSub, email: user.email, memberships: user.memberships.map((m) => ({ kcId: m.kcId, gemeindeId: m.gemeindeId, role: m.role, })), }; } }