import { Injectable, UnauthorizedException } 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.NODE_ENV === 'production' ? 'http://keycloak:8080' : '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, }); } validate(payload: JwtPayload): JwtPayload { if (payload.exp < Date.now() / 1000) { throw new UnauthorizedException('Token expirado'); } return payload; } }