Refactored Auth with Sessions #12

Merged
igorpropisnov merged 9 commits from feature/refactor-auth into main 2024-06-06 12:58:52 +02:00
9 changed files with 89 additions and 136 deletions
Showing only changes of commit 2a53e946ea - Show all commits

View File

@ -1,28 +0,0 @@
import { Injectable, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
@Injectable()
export class AccessTokenGuard extends AuthGuard('jwt-access-token') {
public constructor(private readonly reflector: Reflector) {
super();
}
public canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
// Check if the current route is marked as public
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
// Allow access if the route is public, otherwise defer to the standard JWT authentication mechanism
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

View File

@ -9,6 +9,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
import { Request } from 'express'; import { Request } from 'express';
import { SessionGuard } from 'src/modules/session/guard';
import { Public } from 'src/shared/decorator'; import { Public } from 'src/shared/decorator';
import { LocalAuthGuard } from '../guard'; import { LocalAuthGuard } from '../guard';
@ -46,4 +47,10 @@ export class AuthController {
request.user as LoginResponseDto & { userAgent: string } request.user as LoginResponseDto & { userAgent: string }
); );
} }
@UseGuards(SessionGuard)
@Post('logout')
public async logout(@Req() request: Request): Promise<void> {
this.authService.logout(request.sessionID);
}
} }

View File

@ -6,6 +6,7 @@ import {
Injectable, Injectable,
} from '@nestjs/common'; } from '@nestjs/common';
import { UserCredentials } from 'src/entities'; import { UserCredentials } from 'src/entities';
import { SessionService } from 'src/modules/session/services/session.service';
import { EncryptionService } from 'src/shared'; import { EncryptionService } from 'src/shared';
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service'; import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
@ -20,7 +21,8 @@ export class AuthService {
private readonly userCredentialsRepository: UserCredentialsRepository, private readonly userCredentialsRepository: UserCredentialsRepository,
private readonly userDataRepository: UserDataRepository, private readonly userDataRepository: UserDataRepository,
private readonly passwordConfirmationMailService: PasswordConfirmationMailService, private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
private readonly emailVerificationService: EmailVerificationService private readonly emailVerificationService: EmailVerificationService,
private readonly sessionService: SessionService
) {} ) {}
public async signup( public async signup(
@ -73,29 +75,6 @@ export class AuthService {
} }
} }
public async signin(userCredentials: UserCredentialsDto): Promise<void> {
// const user = await this.userCredentialsRepository.findUserByEmail(
// userCredentials.email
// );
// if (!user) {
// throw new ForbiddenException('Access Denied');
// }
// const passwordMatch = await EncryptionService.compareHash(
// userCredentials.password,
// user.hash
// );
// if (!passwordMatch) {
// throw new ForbiddenException('Access Denied');
// }
// await this.sessionService.checkSessionLimit(user.id);
// const sesseionId = await this.sessionService.createSession(
// user.id,
// request.headers['user-agent']
// );
// this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
// return this.generateAndPersistTokens(user.id, user.email, true);
}
public async validateUser( public async validateUser(
email: string, email: string,
password: string password: string
@ -140,84 +119,14 @@ export class AuthService {
return responseData; return responseData;
} }
// public async logout(userId: string): Promise<boolean> { public async logout(sessionId: string): Promise<void> {
// const affected = try {
// await this.userCredentialsRepository.updateUserRefreshToken(userId, null); this.sessionService.deleteSessionBySessionId(sessionId);
} catch (error) {
// await this.sessionService.invalidateAllSessionsForUser(userId); throw new HttpException(
'Fehler beim Logout',
// return affected > 0; HttpStatus.INTERNAL_SERVER_ERROR
// } );
}
// public async refresh(request: Request): Promise<AccessTokenDto> { }
// const sessionId = request.cookies['session_id'];
// if (!sessionId) {
// throw new ForbiddenException('Session ID missing');
// }
// const session: Session =
// await this.sessionService.findSessionBySessionId(sessionId);
// if (!session) {
// throw new ForbiddenException('Invalid session');
// }
// const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
// sessionId,
// request.headers['user-agent']
// );
// if (!isUserAgentValid) {
// throw new ForbiddenException('Invalid session - User agent mismatch');
// }
// await this.sessionService.extendSessionExpiration(sessionId);
// const decodedToken: TokenPayload = await this.validateRefreshToken(
// session.userCredentials['id']
// );
// const newTokens = await this.generateAndPersistTokens(
// decodedToken.sub,
// decodedToken.email,
// false
// );
// return { access_token: newTokens.access_token };
// }
// private async generateAndPersistTokens(
// userId: string,
// email: string
// ): Promise<LoginResponseDto> {
// const tokens = await this.tokenManagementService.generateTokens(
// userId,
// email
// );
// return { access_token: tokens.access_token, email: email, userId: userId };
// }
// private async validateRefreshToken(userId: string): Promise<TokenPayload> {
// const user = await this.userCredentialsRepository.findUserById(userId);
// if (!user || !user.refreshToken) {
// throw new Error('No refresh token found');
// }
// const decodedToken = await this.tokenManagementService.verifyRefreshToken(
// user.refreshToken
// );
// if (decodedToken.exp < Date.now() / 1000) {
// throw new Error('Token expired');
// }
// if (decodedToken.sub !== user.id) {
// throw new Error('Token subject mismatch');
// }
// return decodedToken;
// }
} }

View File

@ -0,0 +1 @@
export * from './session.guard';

View File

@ -0,0 +1,40 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { SessionService } from '../services/session.service';
@Injectable()
export class SessionGuard implements CanActivate {
public constructor(private readonly sessionService: SessionService) {}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const sessionId = request.session.id;
const currentAgent = request.headers['user-agent'];
const session = await this.sessionService.findSessionBySessionId(sessionId);
if (!session) {
throw new UnauthorizedException('Session not found.');
}
const isExpired = await this.sessionService.isSessioExpired(session);
if (isExpired) {
throw new UnauthorizedException('Session expired.');
}
const userAgentInSession = JSON.parse(session.json).passport.user
.userAgent as string;
if (userAgentInSession !== currentAgent) {
throw new UnauthorizedException('User agent mismatch.');
}
return true;
}
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Session } from 'src/entities'; import { Session } from 'src/entities';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -7,7 +8,8 @@ import { Repository } from 'typeorm';
export class SessionRepository { export class SessionRepository {
public constructor( public constructor(
@InjectRepository(Session) @InjectRepository(Session)
private readonly repository: Repository<Session> private readonly repository: Repository<Session>,
private readonly configService: ConfigService
) {} ) {}
public async findSessionsByUserId(userId: string): Promise<Session[]> { public async findSessionsByUserId(userId: string): Promise<Session[]> {
@ -46,7 +48,16 @@ export class SessionRepository {
}); });
} }
public async isSessionExpired(session: Session): Promise<boolean> {
return session.expiredAt < Date.now();
}
public async deleteSessionBySessionId(sessionId: string): Promise<void> {
await this.repository.delete(sessionId);
}
public async enforceSessionLimit(userId: string): Promise<void> { public async enforceSessionLimit(userId: string): Promise<void> {
const sessionLimit = this.configService.get<number>('SESSION_LIMIT');
const sessions = await this.repository const sessions = await this.repository
.createQueryBuilder('session') .createQueryBuilder('session')
.withDeleted() .withDeleted()
@ -56,8 +67,11 @@ export class SessionRepository {
.orderBy('session.expiredAt', 'ASC') .orderBy('session.expiredAt', 'ASC')
.getMany(); .getMany();
if (sessions.length > 5) { if (sessions.length > sessionLimit) {
const sessionsToDelete = sessions.slice(0, sessions.length - 5); const sessionsToDelete = sessions.slice(
0,
sessions.length - sessionLimit
);
await this.repository.remove(sessionsToDelete); await this.repository.remove(sessionsToDelete);
} }

View File

@ -20,6 +20,14 @@ export class SessionService {
return this.sessionRepository.findSessionsByUserId(userId); return this.sessionRepository.findSessionsByUserId(userId);
} }
public async isSessioExpired(session: Session): Promise<boolean> {
return this.sessionRepository.isSessionExpired(session);
}
public async deleteSessionBySessionId(sessionId: string): Promise<void> {
return this.sessionRepository.deleteSessionBySessionId(sessionId);
}
public async findSessionBySessionId( public async findSessionBySessionId(
sessionId: string sessionId: string
): Promise<Session | null> { ): Promise<Session | null> {

View File

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { Session } from 'src/entities'; import { Session } from 'src/entities';
import { SessionGuard } from './guard';
import { SessionRepository } from './repository/session.repository'; import { SessionRepository } from './repository/session.repository';
import { SessionInitService, SessionSerializerService } from './services'; import { SessionInitService, SessionSerializerService } from './services';
import { SessionService } from './services/session.service'; import { SessionService } from './services/session.service';
@ -12,9 +13,10 @@ import { SessionService } from './services/session.service';
SessionInitService, SessionInitService,
SessionSerializerService, SessionSerializerService,
SessionRepository, SessionRepository,
SessionGuard,
SessionService, SessionService,
], ],
controllers: [], controllers: [],
exports: [SessionService], exports: [SessionService, SessionGuard],
}) })
export class SessionModule {} export class SessionModule {}