Skip to content

Commit 93d6b21

Browse files
Merge pull request #940 from Agbasimere/fix/802-user-status-check
fix(auth): reject token refresh and access validation for non-active users
2 parents 07bf990 + e0e51f0 commit 93d6b21

5 files changed

Lines changed: 157 additions & 3 deletions

File tree

package-lock.json

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auth/auth.service.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,20 @@ describe('AuthService', () => {
134134
expect(mockUserRepo.update).toHaveBeenCalledWith('user-1', { refreshToken: null });
135135
});
136136

137+
it('throws UnauthorizedException when the user status is SUSPENDED', async () => {
138+
mockJwtService.verify.mockReturnValue(validDecoded);
139+
mockUserRepo.findOneBy.mockResolvedValue(makeUser({ status: UserStatus.SUSPENDED }));
140+
141+
await expect(service.refreshTokens('token')).rejects.toThrow(UnauthorizedException);
142+
});
143+
144+
it('throws UnauthorizedException when the user status is INACTIVE', async () => {
145+
mockJwtService.verify.mockReturnValue(validDecoded);
146+
mockUserRepo.findOneBy.mockResolvedValue(makeUser({ status: UserStatus.INACTIVE }));
147+
148+
await expect(service.refreshTokens('token')).rejects.toThrow(UnauthorizedException);
149+
});
150+
137151
it('issues new tokens when the refresh token is valid and not blacklisted', async () => {
138152
mockJwtService.verify.mockReturnValue(validDecoded);
139153
mockUserRepo.findOneBy.mockResolvedValue(makeUser());

src/auth/auth.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
44
import { Repository } from 'typeorm';
55
import { v4 as uuidv4 } from 'uuid';
66
import * as bcrypt from 'bcrypt';
7-
import { User } from '../users/entities/user.entity';
7+
import { User, UserStatus } from '../users/entities/user.entity';
88
import { TokenBlacklistService } from './services/token-blacklist.service';
99

1010
@Injectable()
@@ -51,6 +51,10 @@ export class AuthService {
5151
throw new UnauthorizedException('Access Denied');
5252
}
5353

54+
if (user.status !== UserStatus.ACTIVE) {
55+
throw new UnauthorizedException('User is not active');
56+
}
57+
5458
const refreshTokenMatches = await bcrypt.compare(refreshToken, user.refreshToken);
5559
if (!refreshTokenMatches) {
5660
throw new UnauthorizedException('Access Denied');

src/auth/jwt.strategy.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { getRepositoryToken } from '@nestjs/typeorm';
3+
import { UnauthorizedException } from '@nestjs/common';
4+
import { JwtStrategy, JwtPayload } from './jwt.strategy';
5+
import { User, UserStatus } from '../users/entities/user.entity';
6+
7+
describe('JwtStrategy', () => {
8+
let strategy: JwtStrategy;
9+
10+
const mockUserRepo = {
11+
findOneBy: jest.fn(),
12+
createQueryBuilder: jest.fn(),
13+
};
14+
15+
beforeEach(async () => {
16+
const module: TestingModule = await Test.createTestingModule({
17+
providers: [JwtStrategy, { provide: getRepositoryToken(User), useValue: mockUserRepo }],
18+
}).compile();
19+
20+
strategy = module.get<JwtStrategy>(JwtStrategy);
21+
});
22+
23+
afterEach(() => jest.clearAllMocks());
24+
25+
it('should be defined', () => {
26+
expect(strategy).toBeDefined();
27+
});
28+
29+
describe('validate', () => {
30+
const payload: JwtPayload = {
31+
sub: 'user-1',
32+
email: 'test@example.com',
33+
roles: [],
34+
permissions: [],
35+
};
36+
37+
const mockUser = {
38+
id: 'user-1',
39+
email: 'test@example.com',
40+
status: UserStatus.ACTIVE,
41+
};
42+
43+
const mockUserWithRolesAndPermissions = {
44+
...mockUser,
45+
roles: [
46+
{
47+
name: 'student',
48+
permissions: [{ resource: 'course', action: 'read' }],
49+
},
50+
],
51+
};
52+
53+
it('should successfully validate and return payload with roles and permissions if user is active', async () => {
54+
mockUserRepo.findOneBy.mockResolvedValue(mockUser);
55+
56+
const mockQueryBuilder = {
57+
leftJoinAndSelect: jest.fn().mockReturnThis(),
58+
where: jest.fn().mockReturnThis(),
59+
getOne: jest.fn().mockResolvedValue(mockUserWithRolesAndPermissions),
60+
};
61+
mockUserRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);
62+
63+
const result = await strategy.validate(payload);
64+
65+
expect(mockUserRepo.findOneBy).toHaveBeenCalledWith({ id: 'user-1' });
66+
expect(result).toEqual({
67+
sub: 'user-1',
68+
email: 'test@example.com',
69+
roles: ['student'],
70+
permissions: ['course:read'],
71+
});
72+
});
73+
74+
it('should throw UnauthorizedException if the user is suspended', async () => {
75+
mockUserRepo.findOneBy.mockResolvedValue({
76+
...mockUser,
77+
status: UserStatus.SUSPENDED,
78+
});
79+
80+
await expect(strategy.validate(payload)).rejects.toThrow(UnauthorizedException);
81+
});
82+
83+
it('should throw UnauthorizedException if the user is inactive', async () => {
84+
mockUserRepo.findOneBy.mockResolvedValue({
85+
...mockUser,
86+
status: UserStatus.INACTIVE,
87+
});
88+
89+
await expect(strategy.validate(payload)).rejects.toThrow(UnauthorizedException);
90+
});
91+
92+
it('should throw an error if the user is not found', async () => {
93+
mockUserRepo.findOneBy.mockResolvedValue(null);
94+
95+
await expect(strategy.validate(payload)).rejects.toThrow(Error);
96+
});
97+
});
98+
});

src/auth/jwt.strategy.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Injectable, UnauthorizedException } from '@nestjs/common';
22
import { PassportStrategy } from '@nestjs/passport';
33
import { ExtractJwt, Strategy } from 'passport-jwt';
44
import { InjectRepository } from '@nestjs/typeorm';
55
import { Repository } from 'typeorm';
6-
import { User } from '../users/entities/user.entity';
6+
import { User, UserStatus } from '../users/entities/user.entity';
77

88
export interface JwtPayload {
99
sub: string;
@@ -39,6 +39,10 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
3939
throw new Error('User not found');
4040
}
4141

42+
if (user.status !== UserStatus.ACTIVE) {
43+
throw new UnauthorizedException('User is not active');
44+
}
45+
4246
// Fetch roles and permissions for the user
4347
const userWithRolesAndPermissions = await this.userRepository
4448
.createQueryBuilder('user')

0 commit comments

Comments
 (0)