Skip to content

Commit 5094cc4

Browse files
committed
feat: add configurable JWT signing and permission claims
Rebuilds #974 on top of the current wallet-auth/RBAC architecture: centralizes JWT signing config (HS256/RS256 via JWT_ALGORITHM) into a shared factory used by both AuthModule and UsersModule, and adds a permissions claim derived from roles to the access token payload.
1 parent da2571d commit 5094cc4

9 files changed

Lines changed: 76 additions & 27 deletions

File tree

backend/src/auth/auth.module.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Module } from '@nestjs/common';
22
import { JwtModule } from '@nestjs/jwt';
33
import { PassportModule } from '@nestjs/passport';
4-
import { ConfigModule, ConfigService } from '@nestjs/config';
54
import { AuthController } from './auth.controller';
65
import { AuthService } from './auth.service';
76
import { JwtStrategy } from './strategies/jwt.strategy';
87
import { WalletStrategy } from './strategies/wallet.strategy';
98
import { JwtAuthGuard } from './guards/jwt-auth.guard';
109
import { RolesGuard } from './guards/roles.guard';
10+
import { jwtModuleConfig } from '../config/jwt.config.js';
1111

1212
/**
1313
* #971: Self-contained Auth module.
@@ -18,16 +18,7 @@ import { RolesGuard } from './guards/roles.guard';
1818
@Module({
1919
imports: [
2020
PassportModule.register({ defaultStrategy: 'jwt' }),
21-
JwtModule.registerAsync({
22-
imports: [ConfigModule],
23-
useFactory: (configService: ConfigService) => ({
24-
secret: configService.get('JWT_SECRET', 'dev-secret'),
25-
signOptions: {
26-
expiresIn: configService.get('JWT_ACCESS_TTL', '900'),
27-
},
28-
}),
29-
inject: [ConfigService],
30-
}),
21+
JwtModule.registerAsync(jwtModuleConfig),
3122
],
3223
controllers: [AuthController],
3324
providers: [AuthService, JwtStrategy, WalletStrategy, JwtAuthGuard, RolesGuard],

backend/src/auth/auth.service.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { ConfigService } from '@nestjs/config';
44
import { v4 as uuidv4 } from 'uuid';
55
import { JwtAccessTokenPayload } from '../interfaces/jwt-payload.interface';
66
import { WalletStrategy } from '../strategies/wallet.strategy';
7+
import { AuthRole } from '../common/enums/auth-role.enum.js';
8+
import { ROLE_PERMISSIONS } from '../common/constants/role-permissions.constant.js';
79

810
/**
911
* #971-978: Auth service handling wallet login, JWT lifecycle, and session management.
@@ -106,6 +108,7 @@ export class AuthService {
106108
const refreshTtl = parseInt(this.configService.get('JWT_REFRESH_TTL', '604800'), 10); // 7 days
107109

108110
const roles = await this.resolveRoles(walletAddress);
111+
const permissions = this.resolvePermissions(roles);
109112

110113
const accessPayload: JwtAccessTokenPayload = {
111114
sub: walletAddress,
@@ -114,6 +117,7 @@ export class AuthService {
114117
iat: Math.floor(Date.now() / 1000),
115118
exp: Math.floor(Date.now() / 1000) + accessTtl,
116119
roles,
120+
permissions,
117121
};
118122

119123
const accessToken = await this.jwtService.signAsync(accessPayload);
@@ -199,10 +203,23 @@ export class AuthService {
199203
// In production: persist to database via UsersService
200204
}
201205

202-
private async resolveRoles(walletAddress: string): Promise<string[]> {
206+
private async resolveRoles(walletAddress: string): Promise<AuthRole[]> {
203207
// In production: query from database
204208
// Default role for all authenticated users
205-
return ['MENTEE'];
209+
return [AuthRole.MENTEE];
210+
}
211+
212+
/**
213+
* #974: Resolve the union of permissions granted by a set of roles.
214+
*/
215+
private resolvePermissions(roles: AuthRole[]): string[] {
216+
const permissions = new Set<string>();
217+
for (const role of roles) {
218+
for (const permission of ROLE_PERMISSIONS[role] ?? []) {
219+
permissions.add(permission);
220+
}
221+
}
222+
return Array.from(permissions);
206223
}
207224

208225
private cleanExpiredNonces(): void {

backend/src/auth/guards/jwt-auth.guard.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
Injectable,
55
UnauthorizedException,
66
} from '@nestjs/common';
7-
import { ConfigService } from '@nestjs/config';
87
import { JwtService } from '@nestjs/jwt';
98
import { Request } from 'express';
109
import { JwtAccessTokenPayload } from '../interfaces/jwt-payload.interface';
@@ -25,7 +24,6 @@ export interface JwtGuardOptions {
2524
export class JwtAuthGuard implements CanActivate {
2625
constructor(
2726
private readonly jwtService: JwtService,
28-
private readonly configService: ConfigService,
2927
private readonly blacklistCheck?: (jti: string) => Promise<boolean>,
3028
) {}
3129

@@ -41,9 +39,8 @@ export class JwtAuthGuard implements CanActivate {
4139
}
4240

4341
try {
44-
const payload = await this.jwtService.verifyAsync<JwtAccessTokenPayload>(token, {
45-
secret: this.configService.get<string>('JWT_SECRET', 'dev-secret'),
46-
});
42+
const payload =
43+
await this.jwtService.verifyAsync<JwtAccessTokenPayload>(token);
4744

4845
// #981: Check Redis blacklist for revoked tokens
4946
if (this.blacklistCheck && payload.jti) {

backend/src/auth/interfaces/jwt-payload.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export interface JwtAccessTokenPayload {
55
iat: number;
66
exp: number;
77
roles?: string[];
8+
permissions?: string[];
89
}

backend/src/auth/strategies/jwt.strategy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
2929
iat: payload.iat,
3030
exp: payload.exp,
3131
roles: payload.roles || [],
32+
permissions: payload.permissions || [],
3233
};
3334
}
3435
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AuthRole } from '../enums/auth-role.enum.js';
2+
import { Permission } from '../enums/permission.enum.js';
3+
4+
export const ROLE_PERMISSIONS: Record<AuthRole, Permission[]> = {
5+
[AuthRole.USER]: [Permission.PROFILE_READ],
6+
[AuthRole.MENTEE]: [Permission.PROFILE_READ, Permission.MENTEE_PROFILE_WRITE],
7+
[AuthRole.MENTOR]: [Permission.PROFILE_READ, Permission.MENTOR_PROFILE_WRITE],
8+
[AuthRole.ADMIN]: [
9+
Permission.PROFILE_READ,
10+
Permission.PROFILE_WRITE,
11+
Permission.MENTOR_PROFILE_WRITE,
12+
Permission.MENTEE_PROFILE_WRITE,
13+
Permission.USER_MANAGE,
14+
],
15+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export enum Permission {
2+
PROFILE_READ = 'profile:read',
3+
PROFILE_WRITE = 'profile:write',
4+
MENTOR_PROFILE_WRITE = 'mentor_profile:write',
5+
MENTEE_PROFILE_WRITE = 'mentee_profile:write',
6+
USER_MANAGE = 'user:manage',
7+
}

backend/src/config/jwt.config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ConfigModule, ConfigService } from '@nestjs/config';
2+
import { JwtModuleOptions } from '@nestjs/jwt';
3+
import { Algorithm } from 'jsonwebtoken';
4+
5+
export const jwtModuleConfig = {
6+
imports: [ConfigModule],
7+
inject: [ConfigService],
8+
useFactory: (config: ConfigService): JwtModuleOptions => {
9+
const algorithm = config.get<Algorithm>('JWT_ALGORITHM', 'HS256');
10+
11+
if (algorithm === 'RS256') {
12+
return {
13+
privateKey: config.get<string>('JWT_PRIVATE_KEY'),
14+
publicKey: config.get<string>('JWT_PUBLIC_KEY'),
15+
signOptions: { algorithm },
16+
verifyOptions: { algorithms: [algorithm] },
17+
};
18+
}
19+
20+
return {
21+
secret: config.get<string>('JWT_SECRET', 'dev-secret'),
22+
signOptions: { algorithm },
23+
verifyOptions: { algorithms: [algorithm] },
24+
};
25+
},
26+
};

backend/src/users/users.module.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Module } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { JwtModule } from '@nestjs/jwt';
4-
import { ConfigModule, ConfigService } from '@nestjs/config';
4+
import { ConfigModule } from '@nestjs/config';
55
import { UsersController } from './users.controller.js';
66
import { UsersService } from './users.service.js';
77
import { User } from './entities/user.entity.js';
@@ -10,18 +10,12 @@ import { MentorProfile } from './entities/mentor-profile.entity.js';
1010
import { MenteeProfile } from './entities/mentee-profile.entity.js';
1111
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard.js';
1212
import { RolesGuard } from '../auth/guards/roles.guard.js';
13+
import { jwtModuleConfig } from '../config/jwt.config.js';
1314

1415
@Module({
1516
imports: [
1617
TypeOrmModule.forFeature([User, Role, MentorProfile, MenteeProfile]),
17-
JwtModule.registerAsync({
18-
imports: [ConfigModule],
19-
inject: [ConfigService],
20-
useFactory: (config: ConfigService) => ({
21-
secret: config.get<string>('JWT_SECRET'),
22-
signOptions: { expiresIn: '1h' },
23-
}),
24-
}),
18+
JwtModule.registerAsync(jwtModuleConfig),
2519
ConfigModule,
2620
],
2721
controllers: [UsersController],

0 commit comments

Comments
 (0)