Adiciona primeiros Guards de autenticacao

This commit is contained in:
LeoMortari
2025-09-11 18:46:59 -03:00
parent ee1e3bd7f8
commit 13b41d2f52
7 changed files with 83 additions and 18 deletions

View File

@@ -23,6 +23,7 @@
"@nestjs/common": "11.0.1", "@nestjs/common": "11.0.1",
"@nestjs/config": "4.0.2", "@nestjs/config": "4.0.2",
"@nestjs/core": "11.0.1", "@nestjs/core": "11.0.1",
"@nestjs/passport": "11.0.5",
"@nestjs/platform-express": "11.0.1", "@nestjs/platform-express": "11.0.1",
"@prisma/client": "6.14.0", "@prisma/client": "6.14.0",
"axios": "1.12.0", "axios": "1.12.0",
@@ -30,6 +31,9 @@
"class-transformer": "0.5.1", "class-transformer": "0.5.1",
"class-validator": "0.14.2", "class-validator": "0.14.2",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"jwks-rsa": "3.2.0",
"passport": "0.7.0",
"passport-jwt": "4.0.1",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1" "rxjs": "7.8.1"
}, },

View File

@@ -12,7 +12,7 @@ import { LoggerMiddleware } from './middleware/logger.middleware';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot(),
PrismaModule, PrismaModule,
VideosModule, VideosModule,
AuthModule, AuthModule,

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { KeycloakJwtStrategy } from './keycloak.strategy';
@Module({ @Module({
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService], providers: [AuthService, KeycloakJwtStrategy],
exports: [AuthService], exports: [AuthService],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -10,8 +10,7 @@ import LoginResponseDto from './dto/loginResponse.dto';
export class AuthService { export class AuthService {
private readonly keycloakUrl = private readonly keycloakUrl =
'https://auth.clipperia.com.br/realms/clipperia/protocol/openid-connect'; 'https://auth.clipperia.com.br/realms/clipperia/protocol/openid-connect';
private readonly clientId = 'usuarios'; private readonly clientId = 'account';
private readonly clientSecret = process.env.KEYCLOAK_CLIENT_SECRET;
private readonly keycloakApi = axios.create({ private readonly keycloakApi = axios.create({
baseURL: this.keycloakUrl, baseURL: this.keycloakUrl,
@@ -29,10 +28,6 @@ export class AuthService {
password: loginDto.password, password: loginDto.password,
}); });
if (this.clientSecret) {
params.append('client_secret', this.clientSecret);
}
const { data } = await this.keycloakApi.post<LoginResponseDto>( const { data } = await this.keycloakApi.post<LoginResponseDto>(
'/token', '/token',
params, params,
@@ -56,10 +51,6 @@ export class AuthService {
refresh_token: refreshToken, refresh_token: refreshToken,
}); });
if (this.clientSecret) {
data.append('client_secret', this.clientSecret);
}
await this.keycloakApi.post('/logout', data); await this.keycloakApi.post('/logout', data);
} catch (error) { } catch (error) {
if (isAxiosError(error)) { if (isAxiosError(error)) {
@@ -72,8 +63,6 @@ export class AuthService {
} }
async refreshToken(refreshToken: string) { async refreshToken(refreshToken: string) {
console.log(refreshToken);
try { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
client_id: this.clientId, client_id: this.clientId,
@@ -81,10 +70,6 @@ export class AuthService {
refresh_token: refreshToken, refresh_token: refreshToken,
}); });
if (this.clientSecret) {
params.append('client_secret', this.clientSecret);
}
const { data } = await this.keycloakApi.post<LoginResponseDto>( const { data } = await this.keycloakApi.post<LoginResponseDto>(
'/token', '/token',
params, params,

View File

@@ -0,0 +1,18 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import type { JwtPayload } from './keycloak.strategy';
@Injectable()
export class KeycloakAuthGuard extends AuthGuard('jwt') {
handleRequest(err: unknown, user: JwtPayload): JwtPayload {
if (err || !user) {
if (err instanceof UnauthorizedException) {
throw err;
}
throw new UnauthorizedException('Usuário não autenticado');
}
return user;
}
}

View File

@@ -0,0 +1,54 @@
import { Injectable } 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<string, JwtResourceAccessEntry>
| 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() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKeyProvider: jwksRsa.passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri:
'https://auth.clipperia.com.br/realms/clipperia/protocol/openid-connect/certs',
}),
algorithms: ['RS256'],
audience: 'account',
issuer: 'https://auth.clipperia.com.br/realms/clipperia',
ignoreExpiration: false,
});
}
validate(payload: JwtPayload): JwtPayload {
return payload;
}
}

View File

@@ -6,6 +6,7 @@ import {
Delete, Delete,
Body, Body,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { videos, Prisma, video_situation } from 'generated/prisma'; import { videos, Prisma, video_situation } from 'generated/prisma';
@@ -13,12 +14,14 @@ import { VideosService } from './videos.service';
import { VideoResponseDto } from './dto/video-response.dto'; import { VideoResponseDto } from './dto/video-response.dto';
import { PaginatedQueryDto, PaginatedResponse } from '../shared/dto/paginated'; import { PaginatedQueryDto, PaginatedResponse } from '../shared/dto/paginated';
import { EBooleanPipe } from '../shared/pipe'; import { EBooleanPipe } from '../shared/pipe';
import { KeycloakAuthGuard } from '../auth/keycloak-auth.guard';
@Controller('videos') @Controller('videos')
export class VideosController { export class VideosController {
constructor(private readonly videosService: VideosService) {} constructor(private readonly videosService: VideosService) {}
@Get() @Get()
@UseGuards(KeycloakAuthGuard)
async list( async list(
@Query() query: PaginatedQueryDto, @Query() query: PaginatedQueryDto,
@Query('situation') situation?: video_situation, @Query('situation') situation?: video_situation,