Adiciona primeiros Guards de autenticacao
This commit is contained in:
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
18
src/auth/keycloak-auth.guard.ts
Normal file
18
src/auth/keycloak-auth.guard.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/auth/keycloak.strategy.ts
Normal file
54
src/auth/keycloak.strategy.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user