260223:1415 20260223 nextJS & nestJS Best pratices
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s
This commit is contained in:
146
.agents/skills/nestjs-best-practices/rules/security-auth-jwt.md
Normal file
146
.agents/skills/nestjs-best-practices/rules/security-auth-jwt.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
title: Implement Secure JWT Authentication
|
||||
impact: CRITICAL
|
||||
impactDescription: Essential for secure APIs
|
||||
tags: security, jwt, authentication, tokens
|
||||
---
|
||||
|
||||
## Implement Secure JWT Authentication
|
||||
|
||||
Use `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads.
|
||||
|
||||
**Incorrect (insecure JWT implementation):**
|
||||
|
||||
```typescript
|
||||
// Hardcode secrets
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.register({
|
||||
secret: 'my-secret-key', // Exposed in code
|
||||
signOptions: { expiresIn: '7d' }, // Too long
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
// Store sensitive data in JWT
|
||||
async login(user: User): Promise<{ accessToken: string }> {
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
password: user.password, // NEVER include password!
|
||||
ssn: user.ssn, // NEVER include sensitive data!
|
||||
isAdmin: user.isAdmin, // Can be tampered if not verified
|
||||
};
|
||||
return { accessToken: this.jwtService.sign(payload) };
|
||||
}
|
||||
|
||||
// Skip token validation
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: 'my-secret',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any): Promise<any> {
|
||||
return payload; // No validation of user existence
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (secure JWT with refresh tokens):**
|
||||
|
||||
```typescript
|
||||
// Secure JWT configuration
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
expiresIn: '15m', // Short-lived access tokens
|
||||
issuer: config.get<string>('JWT_ISSUER'),
|
||||
audience: config.get<string>('JWT_AUDIENCE'),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
// Minimal JWT payload
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
async login(user: User): Promise<TokenResponse> {
|
||||
// Only include necessary, non-sensitive data
|
||||
const payload: JwtPayload = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
roles: user.roles,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
const accessToken = this.jwtService.sign(payload);
|
||||
const refreshToken = await this.createRefreshToken(user.id);
|
||||
|
||||
return { accessToken, refreshToken, expiresIn: 900 };
|
||||
}
|
||||
|
||||
private async createRefreshToken(userId: string): Promise<string> {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
const hashedToken = await bcrypt.hash(token, 10);
|
||||
|
||||
await this.refreshTokenRepo.save({
|
||||
userId,
|
||||
token: hashedToken,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
// Proper JWT strategy with validation
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private usersService: UsersService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: config.get<string>('JWT_SECRET'),
|
||||
ignoreExpiration: false,
|
||||
issuer: config.get<string>('JWT_ISSUER'),
|
||||
audience: config.get<string>('JWT_AUDIENCE'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<User> {
|
||||
// Verify user still exists and is active
|
||||
const user = await this.usersService.findById(payload.sub);
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException('User not found or inactive');
|
||||
}
|
||||
|
||||
// Verify token wasn't issued before password change
|
||||
if (user.passwordChangedAt) {
|
||||
const tokenIssuedAt = new Date(payload.iat * 1000);
|
||||
if (tokenIssuedAt < user.passwordChangedAt) {
|
||||
throw new UnauthorizedException('Token invalidated by password change');
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)
|
||||
Reference in New Issue
Block a user