import { Injectable, UnauthorizedException, Logger } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import * as jwksRsa from 'jwks-rsa'; export type JwtAudience = string | string[] | undefined; export interface JwtRealmAccess { roles: string[]; } export interface JwtResourceAccessEntry { roles: string[]; } export type JwtResourceAccess = | Record | undefined; export interface JwtPayload { sub: string; email?: string; preferred_username?: string; given_name?: string; family_name?: string; scope?: string; realm_access?: JwtRealmAccess; resource_access?: JwtResourceAccess; iat: number; exp: number; iss: string; aud?: JwtAudience; [claim: string]: unknown; } @Injectable() export class KeycloakJwtStrategy extends PassportStrategy(Strategy, 'jwt') { constructor() { const baseUrl = process.env.KEYCLOAK_URL ?? 'https://auth.clipperia.com.br'; super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKeyProvider: jwksRsa.passportJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: `${baseUrl}/realms/clipperia/protocol/openid-connect/certs`, }), algorithms: ['RS256'], issuer: `${baseUrl}/realms/clipperia`, ignoreExpiration: false, }); } private readonly logger = new Logger(KeycloakJwtStrategy.name); validate(payload: JwtPayload): JwtPayload { try { // Basic JWT info this.logger.verbose('=== JWT Validation Start ==='); // Token metadata this.logger.verbose(`Subject (sub): ${payload.sub}`); this.logger.verbose(`Issuer (iss): ${payload.iss}`); this.logger.verbose(`Audience (aud): ${JSON.stringify(payload.aud)}`); this.logger.verbose( `Issued At (iat): ${new Date(payload.iat * 1000).toISOString()}`, ); this.logger.verbose( `Expiration (exp): ${new Date(payload.exp * 1000).toISOString()}`, ); // User info this.logger.verbose('--- User Info ---'); this.logger.verbose(`Email: ${payload.email || 'N/A'}`); this.logger.verbose(`Username: ${payload.preferred_username || 'N/A'}`); this.logger.verbose( `Name: ${payload.given_name || ''} ${payload.family_name || ''}`.trim() || 'N/A', ); // Realm roles this.logger.verbose('--- Realm Access ---'); if (payload.realm_access?.roles?.length) { payload.realm_access.roles.forEach((role: string) => { this.logger.verbose(`- ${role}`); }); } else { this.logger.verbose('No realm roles found'); } // Resource access this.logger.verbose('--- Resource Access ---'); if (payload.resource_access) { Object.entries(payload.resource_access).forEach(([resource, data]) => { if (data?.roles?.length) { this.logger.verbose(`${resource} roles:`); data.roles.forEach((role: string) => { this.logger.verbose(` - ${role}`); }); } }); } else { this.logger.verbose('No resource access found'); } // Token expiration check const now = Math.floor(Date.now() / 1000); if (payload.exp < now) { const minutesAgo = Math.round((now - payload.exp) / 60); this.logger.warn(`Token expired ${minutesAgo} minutes ago`); throw new UnauthorizedException('Token expirado'); } this.logger.verbose('=== JWT Validation Successful ==='); return payload; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.stack : String(error); this.logger.error('JWT Validation Error:', errorMessage); throw error; } return payload; } }