Authentication und Authorization sind fundamentale Sicherheitsaspekte jeder modernen Webanwendung. NestJS bietet robuste Tools und Patterns für die Implementierung sicherer Authentifizierungssysteme. In diesem Kapitel behandeln wir moderne Authentifizierungsstrategien, JWT-Implementation, Role-based Access Control und fortgeschrittene Sicherheitskonzepte.
Die Landschaft der Authentifizierung hat sich in den letzten Jahren erheblich weiterentwickelt. Moderne Anwendungen müssen verschiedene Authentifizierungsmethoden unterstützen und gleichzeitig höchste Sicherheitsstandards einhalten.
Authentication (Authentifizierung): “Wer bist du?” - Verifizierung der Identität - Login-Prozess - Credential-Validation
Authorization (Autorisierung): “Was darfst du?” - Zugriffskontrolle - Berechtigung für Ressourcen - Role-based oder Attribute-based Access Control
Passport.js ist die de-facto Standard-Bibliothek für Authentication in Node.js-Anwendungen. NestJS bietet eine nahtlose Integration mit über 500 verfügbaren Strategien.
npm install @nestjs/passport passport
npm install @nestjs/jwt passport-jwt
npm install passport-local
npm install @types/passport-local @types/passport-jwtimport { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
@Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h'),
issuer: configService.get<string>('JWT_ISSUER', 'nestjs-app'),
audience: configService.get<string>('JWT_AUDIENCE', 'nestjs-users'),
},
}),
inject: [ConfigService],
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController],
exports: [AuthService, PassportModule, JwtModule],
})
export class AuthModule {}Die Local Strategy wird für die traditionelle Username/Password-Authentifizierung verwendet.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
import { User } from '../entities/user.entity';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'email', // Standard ist 'username'
passwordField: 'password',
passReqToCallback: false,
});
}
async validate(email: string, password: string): Promise<User> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
if (!user.isActive) {
throw new UnauthorizedException('Account is deactivated');
}
if (user.lockedUntil && user.lockedUntil > new Date()) {
throw new UnauthorizedException('Account is temporarily locked');
}
return user;
}
}Die JWT Strategy validiert Bearer Token und extrahiert Benutzerinformationen.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '../auth.service';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
import { User } from '../entities/user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private authService: AuthService,
private configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
issuer: configService.get<string>('JWT_ISSUER'),
audience: configService.get<string>('JWT_AUDIENCE'),
});
}
async validate(payload: JwtPayload): Promise<User> {
const user = await this.authService.findUserById(payload.sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
if (!user.isActive) {
throw new UnauthorizedException('User account is deactivated');
}
// Prüfung auf Token-Revocation (optional)
if (payload.jti && await this.authService.isTokenRevoked(payload.jti)) {
throw new UnauthorizedException('Token has been revoked');
}
// Prüfung auf Passwort-Änderung
if (payload.iat && user.passwordChangedAt) {
const passwordChangedTimestamp = Math.floor(user.passwordChangedAt.getTime() / 1000);
if (payload.iat < passwordChangedTimestamp) {
throw new UnauthorizedException('Password has been changed. Please login again.');
}
}
return user;
}
}OAuth2-Integration für externe Provider wie Google, GitHub, etc.
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '../auth.service';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(
private authService: AuthService,
private configService: ConfigService,
) {
super({
clientID: configService.get<string>('GOOGLE_CLIENT_ID'),
clientSecret: configService.get<string>('GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get<string>('GOOGLE_CALLBACK_URL'),
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos } = profile;
const user = await this.authService.findOrCreateOAuthUser({
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
avatar: photos[0].value,
provider: 'google',
providerId: profile.id,
accessToken,
refreshToken,
});
done(null, user);
}
}import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-github2';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '../auth.service';
@Injectable()
export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
constructor(
private authService: AuthService,
private configService: ConfigService,
) {
super({
clientID: configService.get<string>('GITHUB_CLIENT_ID'),
clientSecret: configService.get<string>('GITHUB_CLIENT_SECRET'),
callbackURL: configService.get<string>('GITHUB_CALLBACK_URL'),
scope: ['user:email'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
): Promise<any> {
const { username, emails, photos } = profile;
const user = await this.authService.findOrCreateOAuthUser({
email: emails[0].value,
username,
avatar: photos[0].value,
provider: 'github',
providerId: profile.id,
accessToken,
refreshToken,
});
return user;
}
}JSON Web Tokens (JWT) sind der moderne Standard für stateless Authentication in APIs und Single Page Applications.
export interface JwtPayload {
sub: string; // Subject (User ID)
email: string; // User email
roles: string[]; // User roles
permissions: string[]; // User permissions
iat: number; // Issued at
exp: number; // Expiration time
jti?: string; // JWT ID (for revocation)
iss: string; // Issuer
aud: string; // Audience
}
export interface RefreshTokenPayload {
sub: string;
tokenId: string;
iat: number;
exp: number;
}import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';
import { User } from '../entities/user.entity';
import { JwtPayload, RefreshTokenPayload } from './interfaces/jwt-payload.interface';
import * as bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.usersService.findByEmail(email, true);
if (!user) {
// Simulate password check to prevent timing attacks
await bcrypt.compare(password, '$2b$12$dummy.hash.to.prevent.timing.attacks');
return null;
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
// Log failed login attempt
await this.logFailedLoginAttempt(user.id, email);
return null;
}
// Reset failed login attempts on successful validation
await this.resetFailedLoginAttempts(user.id);
const { password: _, ...result } = user;
return result as User;
}
async login(user: User): Promise<{
accessToken: string;
refreshToken: string;
expiresIn: number;
user: Partial<User>;
}> {
const tokenId = uuidv4();
const payload: JwtPayload = {
sub: user.id,
email: user.email,
roles: user.roles || [],
permissions: await this.getUserPermissions(user),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + this.getAccessTokenTTL(),
jti: tokenId,
iss: this.configService.get<string>('JWT_ISSUER'),
aud: this.configService.get<string>('JWT_AUDIENCE'),
};
const refreshPayload: RefreshTokenPayload = {
sub: user.id,
tokenId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + this.getRefreshTokenTTL(),
};
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload),
this.jwtService.signAsync(refreshPayload, {
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN', '7d'),
}),
]);
// Store refresh token
await this.storeRefreshToken(user.id, tokenId, refreshToken);
// Log successful login
await this.logSuccessfulLogin(user.id);
return {
accessToken,
refreshToken,
expiresIn: this.getAccessTokenTTL(),
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
roles: user.roles,
},
};
}
async refreshTokens(refreshToken: string): Promise<{
accessToken: string;
refreshToken: string;
expiresIn: number;
}> {
try {
const payload = await this.jwtService.verifyAsync<RefreshTokenPayload>(
refreshToken,
{
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
},
);
// Validate stored refresh token
const storedToken = await this.getStoredRefreshToken(payload.sub, payload.tokenId);
if (!storedToken || storedToken !== refreshToken) {
throw new UnauthorizedException('Invalid refresh token');
}
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
// Generate new tokens
const newTokenId = uuidv4();
const newPayload: JwtPayload = {
sub: user.id,
email: user.email,
roles: user.roles || [],
permissions: await this.getUserPermissions(user),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + this.getAccessTokenTTL(),
jti: newTokenId,
iss: this.configService.get<string>('JWT_ISSUER'),
aud: this.configService.get<string>('JWT_AUDIENCE'),
};
const newRefreshPayload: RefreshTokenPayload = {
sub: user.id,
tokenId: newTokenId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + this.getRefreshTokenTTL(),
};
const [newAccessToken, newRefreshToken] = await Promise.all([
this.jwtService.signAsync(newPayload),
this.jwtService.signAsync(newRefreshPayload, {
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN', '7d'),
}),
]);
// Replace old refresh token
await this.replaceRefreshToken(
payload.sub,
payload.tokenId,
newTokenId,
newRefreshToken,
);
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresIn: this.getAccessTokenTTL(),
};
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
private getAccessTokenTTL(): number {
return parseInt(this.configService.get<string>('JWT_ACCESS_TTL', '3600')); // 1 hour
}
private getRefreshTokenTTL(): number {
return parseInt(this.configService.get<string>('JWT_REFRESH_TTL', '604800')); // 7 days
}
private async getUserPermissions(user: User): Promise<string[]> {
// Implement based on your role/permission system
const permissions: string[] = [];
if (user.roles?.includes('admin')) {
permissions.push('*'); // All permissions
} else if (user.roles?.includes('user')) {
permissions.push('read:own', 'write:own');
}
return permissions;
}
private async storeRefreshToken(
userId: string,
tokenId: string,
token: string,
): Promise<void> {
// Implementation depends on your storage solution (Redis, Database, etc.)
// Example with Redis:
// await this.redis.setex(`refresh_token:${userId}:${tokenId}`, this.getRefreshTokenTTL(), token);
}
private async getStoredRefreshToken(
userId: string,
tokenId: string,
): Promise<string | null> {
// Implementation depends on your storage solution
// Example with Redis:
// return await this.redis.get(`refresh_token:${userId}:${tokenId}`);
return null; // Placeholder
}
private async replaceRefreshToken(
userId: string,
oldTokenId: string,
newTokenId: string,
newToken: string,
): Promise<void> {
// Remove old token and store new one
// await this.redis.del(`refresh_token:${userId}:${oldTokenId}`);
// await this.redis.setex(`refresh_token:${userId}:${newTokenId}`, this.getRefreshTokenTTL(), newToken);
}
private async logFailedLoginAttempt(userId: string, email: string): Promise<void> {
// Implement failed login attempt tracking
console.log(`Failed login attempt for user ${email}`);
}
private async resetFailedLoginAttempts(userId: string): Promise<void> {
// Reset failed login attempt counter
console.log(`Reset failed login attempts for user ${userId}`);
}
private async logSuccessfulLogin(userId: string): Promise<void> {
// Log successful login
console.log(`Successful login for user ${userId}`);
}
}import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check if route is marked as public
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('JWT_SECRET'),
issuer: this.configService.get<string>('JWT_ISSUER'),
audience: this.configService.get<string>('JWT_AUDIENCE'),
});
// Additional validation
await this.validateTokenPayload(payload);
request.user = payload;
return true;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new UnauthorizedException('Token has expired');
} else if (error.name === 'JsonWebTokenError') {
throw new UnauthorizedException('Invalid token');
} else {
throw new UnauthorizedException('Token validation failed');
}
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
private async validateTokenPayload(payload: any): Promise<void> {
// Additional custom validation
if (!payload.sub || !payload.email) {
throw new UnauthorizedException('Invalid token payload');
}
// Check if token is revoked (if you implement token revocation)
if (payload.jti && await this.isTokenRevoked(payload.jti)) {
throw new UnauthorizedException('Token has been revoked');
}
}
private async isTokenRevoked(jti: string): Promise<boolean> {
// Implement token revocation check
// Example with Redis:
// return await this.redis.exists(`revoked_token:${jti}`);
return false; // Placeholder
}
}import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from './decorators/public.decorator';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Public()
@Post('login')
async login(@Body() loginDto: { email: string; password: string }) {
const user = await this.authService.validateUser(loginDto.email, loginDto.password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return this.authService.login(user);
}
@Public()
@Post('refresh')
async refresh(@Body() refreshDto: { refreshToken: string }) {
if (!refreshDto.refreshToken) {
throw new UnauthorizedException('Refresh token is required');
}
return this.authService.refreshTokens(refreshDto.refreshToken);
}
@Post('logout')
async logout(@Body() logoutDto: { refreshToken: string }) {
if (logoutDto.refreshToken) {
await this.authService.revokeRefreshToken(logoutDto.refreshToken);
}
return { message: 'Logged out successfully' };
}
@Post('logout-all')
async logoutAll(@Req() req: any) {
const userId = req.user.sub;
await this.authService.revokeAllRefreshTokens(userId);
return { message: 'Logged out from all devices' };
}
}RBAC ist ein bewährtes Modell für die Zugriffskontrolle in Unternehmensanwendungen.
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
@Entity('roles')
export class Role {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
name: string;
@Column({ nullable: true })
description: string;
@Column({ default: true })
isActive: boolean;
@ManyToMany(() => Permission, permission => permission.roles)
@JoinTable({
name: 'role_permissions',
joinColumn: { name: 'roleId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'permissionId', referencedColumnName: 'id' },
})
permissions: Permission[];
}
@Entity('permissions')
export class Permission {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
name: string;
@Column({ nullable: true })
description: string;
@Column()
resource: string; // e.g., 'users', 'posts', 'orders'
@Column()
action: string; // e.g., 'create', 'read', 'update', 'delete'
@ManyToMany(() => Role, role => role.permissions)
roles: Role[];
}
// Updated User Entity
@Entity('users')
export class User extends BaseEntity {
// ... other properties
@ManyToMany(() => Role, role => role.users)
@JoinTable({
name: 'user_roles',
joinColumn: { name: 'userId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'roleId', referencedColumnName: 'id' },
})
roles: Role[];
// Helper method to get all permissions
getAllPermissions(): string[] {
const permissions = new Set<string>();
this.roles?.forEach(role => {
role.permissions?.forEach(permission => {
permissions.add(`${permission.action}:${permission.resource}`);
});
});
return Array.from(permissions);
}
// Helper method to check permission
hasPermission(action: string, resource: string): boolean {
return this.getAllPermissions().includes(`${action}:${resource}`) ||
this.getAllPermissions().includes('*'); // Super admin
}
// Helper method to check role
hasRole(roleName: string): boolean {
return this.roles?.some(role => role.name === roleName) || false;
}
}@Injectable()
export class RbacService {
constructor(
@InjectRepository(Role)
private roleRepository: Repository<Role>,
@InjectRepository(Permission)
private permissionRepository: Repository<Permission>,
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async createRole(name: string, description?: string): Promise<Role> {
const role = this.roleRepository.create({ name, description });
return this.roleRepository.save(role);
}
async createPermission(
name: string,
resource: string,
action: string,
description?: string,
): Promise<Permission> {
const permission = this.permissionRepository.create({
name,
resource,
action,
description,
});
return this.permissionRepository.save(permission);
}
async assignRoleToUser(userId: string, roleId: string): Promise<void> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});
const role = await this.roleRepository.findOne({
where: { id: roleId },
});
if (!user || !role) {
throw new NotFoundException('User or role not found');
}
if (!user.roles) {
user.roles = [];
}
if (!user.roles.find(r => r.id === roleId)) {
user.roles.push(role);
await this.userRepository.save(user);
}
}
async removeRoleFromUser(userId: string, roleId: string): Promise<void> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});
if (!user) {
throw new NotFoundException('User not found');
}
user.roles = user.roles?.filter(role => role.id !== roleId) || [];
await this.userRepository.save(user);
}
async assignPermissionToRole(roleId: string, permissionId: string): Promise<void> {
const role = await this.roleRepository.findOne({
where: { id: roleId },
relations: ['permissions'],
});
const permission = await this.permissionRepository.findOne({
where: { id: permissionId },
});
if (!role || !permission) {
throw new NotFoundException('Role or permission not found');
}
if (!role.permissions) {
role.permissions = [];
}
if (!role.permissions.find(p => p.id === permissionId)) {
role.permissions.push(permission);
await this.roleRepository.save(role);
}
}
async getUserPermissions(userId: string): Promise<string[]> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles', 'roles.permissions'],
});
if (!user) {
return [];
}
return user.getAllPermissions();
}
async hasPermission(
userId: string,
action: string,
resource: string,
): Promise<boolean> {
const permissions = await this.getUserPermissions(userId);
return permissions.includes(`${action}:${resource}`) ||
permissions.includes('*');
}
async hasRole(userId: string, roleName: string): Promise<boolean> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});
return user?.hasRole(roleName) || false;
}
// Seed default roles and permissions
async seedDefaultRolesAndPermissions(): Promise<void> {
// Create default permissions
const permissions = [
{ name: 'Create Users', resource: 'users', action: 'create' },
{ name: 'Read Users', resource: 'users', action: 'read' },
{ name: 'Update Users', resource: 'users', action: 'update' },
{ name: 'Delete Users', resource: 'users', action: 'delete' },
{ name: 'Create Posts', resource: 'posts', action: 'create' },
{ name: 'Read Posts', resource: 'posts', action: 'read' },
{ name: 'Update Posts', resource: 'posts', action: 'update' },
{ name: 'Delete Posts', resource: 'posts', action: 'delete' },
{ name: 'Admin Access', resource: '*', action: '*' },
];
const createdPermissions = await Promise.all(
permissions.map(async (perm) => {
const existing = await this.permissionRepository.findOne({
where: { name: perm.name },
});
if (!existing) {
return this.createPermission(perm.name, perm.resource, perm.action);
}
return existing;
}),
);
// Create default roles
const adminRole = await this.roleRepository.findOne({ where: { name: 'admin' } }) ||
await this.createRole('admin', 'Administrator role');
const userRole = await this.roleRepository.findOne({ where: { name: 'user' } }) ||
await this.createRole('user', 'Regular user role');
// Assign permissions to roles
adminRole.permissions = createdPermissions;
await this.roleRepository.save(adminRole);
userRole.permissions = createdPermissions.filter(p =>
p.resource === 'posts' && ['create', 'read', 'update'].includes(p.action)
);
await this.roleRepository.save(userRole);
}
}Guards in NestJS sind der primäre Mechanismus für die Implementierung von Authorization-Logic.
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new ForbiddenException('User not found in request');
}
const hasRole = requiredRoles.some(role => user.roles?.includes(role));
if (!hasRole) {
throw new ForbiddenException(`Insufficient permissions. Required roles: ${requiredRoles.join(', ')}`);
}
return true;
}
}import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RbacService } from '../rbac/rbac.service';
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(
private reflector: Reflector,
private rbacService: RbacService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermissions = this.reflector.getAllAndOverride<{
action: string;
resource: string;
}[]>(PERMISSIONS_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredPermissions) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new ForbiddenException('User not found in request');
}
// Check all required permissions
for (const permission of requiredPermissions) {
const hasPermission = await this.rbacService.hasPermission(
user.sub,
permission.action,
permission.resource,
);
if (!hasPermission) {
throw new ForbiddenException(
`Insufficient permissions. Required: ${permission.action}:${permission.resource}`,
);
}
}
return true;
}
}import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RESOURCE_OWNER_KEY } from '../decorators/resource-owner.decorator';
@Injectable()
export class ResourceOwnerGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const resourceOwnerConfig = this.reflector.get<{
userIdParam: string;
allowRoles?: string[];
}>(RESOURCE_OWNER_KEY, context.getHandler());
if (!resourceOwnerConfig) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const resourceUserId = request.params[resourceOwnerConfig.userIdParam];
if (!user) {
throw new ForbiddenException('User not found in request');
}
// Allow if user is the resource owner
if (user.sub === resourceUserId) {
return true;
}
// Allow if user has one of the allowed roles
if (resourceOwnerConfig.allowRoles) {
const hasAllowedRole = resourceOwnerConfig.allowRoles.some(role =>
user.roles?.includes(role),
);
if (hasAllowedRole) {
return true;
}
}
throw new ForbiddenException('Access denied. You can only access your own resources.');
}
}// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: Array<{ action: string; resource: string }>) =>
SetMetadata(PERMISSIONS_KEY, permissions);
// resource-owner.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const RESOURCE_OWNER_KEY = 'resource_owner';
export const ResourceOwner = (userIdParam: string, allowRoles?: string[]) =>
SetMetadata(RESOURCE_OWNER_KEY, { userIdParam, allowRoles });
// public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
@RequirePermissions({ action: 'read', resource: 'users' })
async findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Get(':id')
@ResourceOwner('id', ['admin'])
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Post()
@Roles('admin')
@RequirePermissions({ action: 'create', resource: 'users' })
async create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
@Put(':id')
@ResourceOwner('id', ['admin'])
@RequirePermissions({ action: 'update', resource: 'users' })
async update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@Roles('admin')
@RequirePermissions({ action: 'delete', resource: 'users' })
async remove(@Param('id') id: string): Promise<void> {
return this.usersService.remove(id);
}
}Obwohl JWT stateless ist, gibt es Szenarien, in denen Session Management erforderlich ist.
import { Injectable } from '@nestjs/common';
import { PassportSerializer } from '@nestjs/passport';
import { UsersService } from '../users/users.service';
import { User } from '../entities/user.entity';
@Injectable()
export class SessionSerializer extends PassportSerializer {
constructor(private usersService: UsersService) {
super();
}
serializeUser(user: User, done: (err: Error, user: any) => void): any {
done(null, { id: user.id, email: user.email });
}
async deserializeUser(
payload: { id: string; email: string },
done: (err: Error, payload: string) => void,
): Promise<any> {
const user = await this.usersService.findById(payload.id);
done(null, user);
}
}import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as session from 'express-session';
import * as connectRedis from 'connect-redis';
import { Redis } from 'ioredis';
@Module({
providers: [
{
provide: 'SESSION_OPTIONS',
useFactory: (configService: ConfigService) => {
const RedisStore = connectRedis(session);
const redisClient = new Redis({
host: configService.get('REDIS_HOST'),
port: configService.get('REDIS_PORT'),
password: configService.get('REDIS_PASSWORD'),
});
return {
store: new RedisStore({ client: redisClient }),
secret: configService.get('SESSION_SECRET'),
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24, // 1 day
httpOnly: true,
secure: configService.get('NODE_ENV') === 'production',
sameSite: 'strict',
},
};
},
inject: [ConfigService],
},
],
exports: ['SESSION_OPTIONS'],
})
export class SessionModule {}MFA fügt eine zusätzliche Sicherheitsebene hinzu und ist für moderne Anwendungen unerlässlich.
import { Injectable } from '@nestjs/common';
import * as speakeasy from 'speakeasy';
import * as QRCode from 'qrcode';
import { UsersService } from '../users/users.service';
@Injectable()
export class MfaService {
constructor(private usersService: UsersService) {}
async generateSecret(userId: string): Promise<{
secret: string;
qrCodeUrl: string;
manualEntryKey: string;
}> {
const user = await this.usersService.findById(userId);
const secret = speakeasy.generateSecret({
name: `${user.email}`,
issuer: 'YourApp',
length: 32,
});
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
// Store the secret (temporarily) - user needs to verify before it's saved permanently
await this.usersService.setTempMfaSecret(userId, secret.base32);
return {
secret: secret.base32,
qrCodeUrl,
manualEntryKey: secret.base32,
};
}
async verifyAndEnableMfa(
userId: string,
token: string,
): Promise<{ success: boolean; backupCodes: string[] }> {
const user = await this.usersService.findById(userId);
if (!user.tempMfaSecret) {
throw new BadRequestException('No MFA setup in progress');
}
const verified = speakeasy.totp.verify({
secret: user.tempMfaSecret,
encoding: 'base32',
token,
window: 2, // Allow some time drift
});
if (!verified) {
throw new BadRequestException('Invalid verification code');
}
// Generate backup codes
const backupCodes = this.generateBackupCodes();
// Save MFA settings
await this.usersService.enableMfa(userId, user.tempMfaSecret, backupCodes);
return {
success: true,
backupCodes,
};
}
async verifyMfaToken(userId: string, token: string): Promise<boolean> {
const user = await this.usersService.findById(userId);
if (!user.mfaSecret) {
return false;
}
// Check if it's a backup code
if (await this.verifyBackupCode(userId, token)) {
return true;
}
// Verify TOTP token
return speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token,
window: 2,
});
}
async disableMfa(userId: string, token: string): Promise<void> {
if (!await this.verifyMfaToken(userId, token)) {
throw new BadRequestException('Invalid verification code');
}
await this.usersService.disableMfa(userId);
}
private generateBackupCodes(): string[] {
const codes: string[] = [];
for (let i = 0; i < 10; i++) {
// Generate 8-character backup codes
const code = Math.random().toString(36).substring(2, 10).toUpperCase();
codes.push(code);
}
return codes;
}
private async verifyBackupCode(userId: string, code: string): Promise<boolean> {
const user = await this.usersService.findById(userId);
if (!user.mfaBackupCodes?.includes(code)) {
return false;
}
// Remove used backup code
await this.usersService.removeUsedBackupCode(userId, code);
return true;
}
}import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { SKIP_MFA_KEY } from '../decorators/skip-mfa.decorator';
@Injectable()
export class MfaGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Check if MFA should be skipped for this route
const skipMfa = this.reflector.getAllAndOverride<boolean>(SKIP_MFA_KEY, [
context.getHandler(),
context.getClass(),
]);
if (skipMfa) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new UnauthorizedException('User not authenticated');
}
// If user has MFA enabled, check if they've completed MFA for this session
if (user.mfaEnabled && !user.mfaVerified) {
throw new UnauthorizedException('MFA verification required');
}
return true;
}
}
// Decorator to skip MFA for certain routes
export const SKIP_MFA_KEY = 'skipMfa';
export const SkipMfa = () => SetMetadata(SKIP_MFA_KEY, true);@Controller('auth/mfa')
@UseGuards(JwtAuthGuard)
export class MfaController {
constructor(
private mfaService: MfaService,
private authService: AuthService,
) {}
@Post('setup')
async setupMfa(@Req() req: any) {
const userId = req.user.sub;
return this.mfaService.generateSecret(userId);
}
@Post('verify-setup')
async verifySetup(
@Req() req: any,
@Body() verifyDto: { token: string },
) {
const userId = req.user.sub;
return this.mfaService.verifyAndEnableMfa(userId, verifyDto.token);
}
@Post('verify')
@SkipMfa()
async verifyMfa(
@Req() req: any,
@Body() verifyDto: { token: string },
) {
const userId = req.user.sub;
const isValid = await this.mfaService.verifyMfaToken(userId, verifyDto.token);
if (!isValid) {
throw new UnauthorizedException('Invalid MFA token');
}
// Mark MFA as verified for this session
const updatedToken = await this.authService.markMfaAsVerified(userId);
return {
success: true,
accessToken: updatedToken,
};
}
@Post('disable')
async disableMfa(
@Req() req: any,
@Body() disableDto: { token: string },
) {
const userId = req.user.sub;
await this.mfaService.disableMfa(userId, disableDto.token);
return { success: true, message: 'MFA disabled successfully' };
}
@Get('backup-codes')
async generateBackupCodes(@Req() req: any) {
const userId = req.user.sub;
// Regenerate backup codes (invalidate old ones)
return this.mfaService.regenerateBackupCodes(userId);
}
}// config/security.config.ts
export const securityConfig = () => ({
jwt: {
secret: process.env.JWT_SECRET || (() => {
throw new Error('JWT_SECRET is required');
})(),
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
refreshSecret: process.env.JWT_REFRESH_SECRET || (() => {
throw new Error('JWT_REFRESH_SECRET is required');
})(),
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
},
bcrypt: {
saltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS || '12'),
},
rateLimit: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX || '100'), // limit each IP to 100 requests per windowMs
},
session: {
secret: process.env.SESSION_SECRET || (() => {
throw new Error('SESSION_SECRET is required');
})(),
maxAge: parseInt(process.env.SESSION_MAX_AGE || '86400000'), // 24 hours
},
mfa: {
issuer: process.env.MFA_ISSUER || 'YourApp',
windowSize: parseInt(process.env.MFA_WINDOW_SIZE || '2'),
},
});@Injectable()
export class PasswordService {
private readonly saltRounds = 12;
// Password complexity requirements
private readonly passwordRequirements = {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
prohibitCommonPasswords: true,
};
async hashPassword(password: string): Promise<string> {
// Validate password strength
this.validatePasswordStrength(password);
return bcrypt.hash(password, this.saltRounds);
}
async verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
validatePasswordStrength(password: string): void {
const errors: string[] = [];
if (password.length < this.passwordRequirements.minLength) {
errors.push(`Password must be at least ${this.passwordRequirements.minLength} characters long`);
}
if (password.length > this.passwordRequirements.maxLength) {
errors.push(`Password must be no more than ${this.passwordRequirements.maxLength} characters long`);
}
if (this.passwordRequirements.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (this.passwordRequirements.requireLowercase && !/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (this.passwordRequirements.requireNumbers && !/\d/.test(password)) {
errors.push('Password must contain at least one number');
}
if (this.passwordRequirements.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('Password must contain at least one special character');
}
if (this.passwordRequirements.prohibitCommonPasswords && this.isCommonPassword(password)) {
errors.push('Password is too common. Please choose a more unique password');
}
if (errors.length > 0) {
throw new BadRequestException(`Password validation failed: ${errors.join(', ')}`);
}
}
private isCommonPassword(password: string): boolean {
const commonPasswords = [
'password', '123456', '123456789', 'qwerty', 'abc123',
'password123', 'admin', 'letmein', 'welcome', 'monkey',
];
return commonPasswords.includes(password.toLowerCase());
}
generateSecurePassword(length: number = 16): string {
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
let password = '';
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
}@Injectable()
export class AccountSecurityService {
constructor(
private usersService: UsersService,
private configService: ConfigService,
) {}
async lockAccountAfterFailedAttempts(userId: string): Promise<void> {
const user = await this.usersService.findById(userId);
const maxAttempts = this.configService.get<number>('MAX_LOGIN_ATTEMPTS', 5);
const lockoutDuration = this.configService.get<number>('LOCKOUT_DURATION_MS', 30 * 60 * 1000); // 30 minutes
user.failedLoginAttempts = (user.failedLoginAttempts || 0) + 1;
if (user.failedLoginAttempts >= maxAttempts) {
user.lockedUntil = new Date(Date.now() + lockoutDuration);
// Send security alert email
await this.sendSecurityAlert(user.email, 'account_locked');
}
await this.usersService.save(user);
}
async resetFailedLoginAttempts(userId: string): Promise<void> {
const user = await this.usersService.findById(userId);
user.failedLoginAttempts = 0;
user.lockedUntil = null;
await this.usersService.save(user);
}
async detectSuspiciousActivity(userId: string, ipAddress: string, userAgent: string): Promise<void> {
const user = await this.usersService.findById(userId);
const lastLogin = user.lastLoginAt;
const lastIp = user.lastLoginIp;
// Check for unusual login patterns
if (lastLogin && lastIp && lastIp !== ipAddress) {
const timeDiff = Date.now() - lastLogin.getTime();
const minTimeForLocationChange = 30 * 60 * 1000; // 30 minutes
if (timeDiff < minTimeForLocationChange) {
// Potential suspicious activity
await this.sendSecurityAlert(user.email, 'suspicious_login', {
ipAddress,
userAgent,
previousIp: lastIp,
});
}
}
// Update login information
user.lastLoginAt = new Date();
user.lastLoginIp = ipAddress;
user.lastUserAgent = userAgent;
await this.usersService.save(user);
}
async requirePasswordChange(userId: string, reason: string): Promise<void> {
const user = await this.usersService.findById(userId);
user.mustChangePassword = true;
user.passwordChangeReason = reason;
await this.usersService.save(user);
await this.sendSecurityAlert(user.email, 'password_change_required', { reason });
}
async logSecurityEvent(
userId: string,
eventType: string,
details: Record<string, any>,
): Promise<void> {
// Log to security audit system
console.log(`Security Event: ${eventType}`, {
userId,
timestamp: new Date().toISOString(),
details,
});
// You might want to store this in a dedicated security events table
}
private async sendSecurityAlert(
email: string,
alertType: string,
data?: Record<string, any>,
): Promise<void> {
// Implement email service integration
console.log(`Security Alert: ${alertType} sent to ${email}`, data);
}
}import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as helmet from 'helmet';
import * as csurf from 'csurf';
import * as rateLimit from 'express-rate-limit';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
}));
// Rate limiting
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
}));
// CSRF protection (for session-based auth)
if (process.env.ENABLE_CSRF === 'true') {
app.use(csurf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
},
}));
}
// CORS configuration
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
});
await app.listen(3000);
}
bootstrap();Die Implementierung einer robusten Authentication und Authorization ist fundamental für die Sicherheit moderner NestJS-Anwendungen. Durch die Kombination von JWT, RBAC, MFA und zusätzlichen Sicherheitsmaßnahmen können Sie ein hochsicheres System entwickeln, das den aktuellen Best Practices entspricht.