diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 6a8e079e..42ea200f 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -3,7 +3,7 @@ import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'crypto'; import * as bcrypt from 'bcrypt'; import { User, UserStatus } from '../users/entities/user.entity'; import { TokenBlacklistService } from './services/token-blacklist.service'; @@ -199,8 +199,8 @@ export class AuthService { private async generateTokens(user: User) { const payload = { sub: user.id, email: user.email, role: user.role }; - const accessJti = uuidv4(); - const refreshJti = uuidv4(); + const accessJti = randomUUID(); + const refreshJti = randomUUID(); const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync( diff --git a/src/auth/jwt.strategy.spec.ts b/src/auth/jwt.strategy.spec.ts index 39effd29..c2aef956 100644 --- a/src/auth/jwt.strategy.spec.ts +++ b/src/auth/jwt.strategy.spec.ts @@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { UnauthorizedException } from '@nestjs/common'; import { JwtStrategy, JwtPayload } from './jwt.strategy'; import { User, UserStatus } from '../users/entities/user.entity'; +import { TokenBlacklistService } from './services/token-blacklist.service'; describe('JwtStrategy', () => { let strategy: JwtStrategy; @@ -12,15 +13,26 @@ describe('JwtStrategy', () => { createQueryBuilder: jest.fn(), }; + const mockTokenBlacklistService = { + isBlacklisted: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [JwtStrategy, { provide: getRepositoryToken(User), useValue: mockUserRepo }], + providers: [ + JwtStrategy, + { provide: getRepositoryToken(User), useValue: mockUserRepo }, + { provide: TokenBlacklistService, useValue: mockTokenBlacklistService }, + ], }).compile(); strategy = module.get(JwtStrategy); }); - afterEach(() => jest.clearAllMocks()); + afterEach(() => { + jest.clearAllMocks(); + mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false); + }); it('should be defined', () => { expect(strategy).toBeDefined(); @@ -32,6 +44,7 @@ describe('JwtStrategy', () => { email: 'test@example.com', roles: [], permissions: [], + jti: 'test-jti', }; const mockUser = { @@ -50,6 +63,13 @@ describe('JwtStrategy', () => { ], }; + it('should check the blacklist on every request and throw UnauthorizedException if blacklisted', async () => { + mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true); + + await expect(strategy.validate(payload)).rejects.toThrow(UnauthorizedException); + expect(mockTokenBlacklistService.isBlacklisted).toHaveBeenCalledWith('test-jti'); + }); + it('should successfully validate and return payload with roles and permissions if user is active', async () => { mockUserRepo.findOneBy.mockResolvedValue(mockUser); diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 8de5f2e6..4e6f586c 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -6,12 +6,14 @@ import { Repository } from 'typeorm'; import { User, UserStatus } from '../users/entities/user.entity'; import { isRS256Configured, loadPEMKey } from './config/jwt-config.factory'; import { RolesService } from '../rbac/roles/roles.service'; +import { TokenBlacklistService } from './services/token-blacklist.service'; export interface JwtPayload { sub: string; email: string; roles: string[]; permissions: string[]; + jti: string; } /** @@ -27,6 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { @InjectRepository(User) private readonly userRepository: Repository, private readonly rolesService: RolesService, + private readonly tokenBlacklistService: TokenBlacklistService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), @@ -54,6 +57,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { * @returns The authenticated user with roles and permissions. */ async validate(payload: JwtPayload): Promise { + if (payload.jti) { + const isBlacklisted = await this.tokenBlacklistService.isBlacklisted(payload.jti); + if (isBlacklisted) { + throw new UnauthorizedException('Token has been revoked'); + } + } + const user = await this.userRepository.findOneBy({ id: payload.sub }); if (!user) { throw new Error('User not found');