diff --git a/backend/src/modules/auth-module/common/guards/access-token.guard.ts b/backend/src/modules/auth-module/common/guards/access-token.guard.ts deleted file mode 100644 index df44b08..0000000 --- a/backend/src/modules/auth-module/common/guards/access-token.guard.ts +++ /dev/null @@ -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 | Observable { - // Check if the current route is marked as public - const isPublic = this.reflector.getAllAndOverride('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); - } -} diff --git a/backend/src/modules/auth-module/common/guards/index.ts b/backend/src/modules/auth-module/common/guards/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts index a6fd6f4..ce279fe 100644 --- a/backend/src/modules/auth-module/controller/auth.controller.ts +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; +import { SessionGuard } from 'src/modules/session/guard'; import { Public } from 'src/shared/decorator'; import { LocalAuthGuard } from '../guard'; @@ -46,4 +47,10 @@ export class AuthController { request.user as LoginResponseDto & { userAgent: string } ); } + + @UseGuards(SessionGuard) + @Post('logout') + public async logout(@Req() request: Request): Promise { + this.authService.logout(request.sessionID); + } } diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index 7d161cc..e18dbcd 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -6,6 +6,7 @@ import { Injectable, } from '@nestjs/common'; import { UserCredentials } from 'src/entities'; +import { SessionService } from 'src/modules/session/services/session.service'; import { EncryptionService } from 'src/shared'; import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service'; @@ -20,7 +21,8 @@ export class AuthService { private readonly userCredentialsRepository: UserCredentialsRepository, private readonly userDataRepository: UserDataRepository, private readonly passwordConfirmationMailService: PasswordConfirmationMailService, - private readonly emailVerificationService: EmailVerificationService + private readonly emailVerificationService: EmailVerificationService, + private readonly sessionService: SessionService ) {} public async signup( @@ -73,29 +75,6 @@ export class AuthService { } } - public async signin(userCredentials: UserCredentialsDto): Promise { - // 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( email: string, password: string @@ -140,84 +119,14 @@ export class AuthService { return responseData; } - // public async logout(userId: string): Promise { - // const affected = - // await this.userCredentialsRepository.updateUserRefreshToken(userId, null); - - // await this.sessionService.invalidateAllSessionsForUser(userId); - - // return affected > 0; - // } - - // public async refresh(request: Request): Promise { - // 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 { - // const tokens = await this.tokenManagementService.generateTokens( - // userId, - // email - // ); - - // return { access_token: tokens.access_token, email: email, userId: userId }; - // } - - // private async validateRefreshToken(userId: string): Promise { - // 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; - // } + public async logout(sessionId: string): Promise { + try { + this.sessionService.deleteSessionBySessionId(sessionId); + } catch (error) { + throw new HttpException( + 'Fehler beim Logout', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } } diff --git a/backend/src/modules/session/guard/index.ts b/backend/src/modules/session/guard/index.ts new file mode 100644 index 0000000..9fb91c1 --- /dev/null +++ b/backend/src/modules/session/guard/index.ts @@ -0,0 +1 @@ +export * from './session.guard'; diff --git a/backend/src/modules/session/guard/session.guard.ts b/backend/src/modules/session/guard/session.guard.ts new file mode 100644 index 0000000..06e55e0 --- /dev/null +++ b/backend/src/modules/session/guard/session.guard.ts @@ -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 { + 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; + } +} diff --git a/backend/src/modules/session/repository/session.repository.ts b/backend/src/modules/session/repository/session.repository.ts index f970490..8c93c52 100644 --- a/backend/src/modules/session/repository/session.repository.ts +++ b/backend/src/modules/session/repository/session.repository.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Session } from 'src/entities'; import { Repository } from 'typeorm'; @@ -7,7 +8,8 @@ import { Repository } from 'typeorm'; export class SessionRepository { public constructor( @InjectRepository(Session) - private readonly repository: Repository + private readonly repository: Repository, + private readonly configService: ConfigService ) {} public async findSessionsByUserId(userId: string): Promise { @@ -46,7 +48,16 @@ export class SessionRepository { }); } + public async isSessionExpired(session: Session): Promise { + return session.expiredAt < Date.now(); + } + + public async deleteSessionBySessionId(sessionId: string): Promise { + await this.repository.delete(sessionId); + } + public async enforceSessionLimit(userId: string): Promise { + const sessionLimit = this.configService.get('SESSION_LIMIT'); const sessions = await this.repository .createQueryBuilder('session') .withDeleted() @@ -56,8 +67,11 @@ export class SessionRepository { .orderBy('session.expiredAt', 'ASC') .getMany(); - if (sessions.length > 5) { - const sessionsToDelete = sessions.slice(0, sessions.length - 5); + if (sessions.length > sessionLimit) { + const sessionsToDelete = sessions.slice( + 0, + sessions.length - sessionLimit + ); await this.repository.remove(sessionsToDelete); } diff --git a/backend/src/modules/session/services/session.service.ts b/backend/src/modules/session/services/session.service.ts index 3a92278..0fd0b56 100644 --- a/backend/src/modules/session/services/session.service.ts +++ b/backend/src/modules/session/services/session.service.ts @@ -20,6 +20,14 @@ export class SessionService { return this.sessionRepository.findSessionsByUserId(userId); } + public async isSessioExpired(session: Session): Promise { + return this.sessionRepository.isSessionExpired(session); + } + + public async deleteSessionBySessionId(sessionId: string): Promise { + return this.sessionRepository.deleteSessionBySessionId(sessionId); + } + public async findSessionBySessionId( sessionId: string ): Promise { diff --git a/backend/src/modules/session/session.module.ts b/backend/src/modules/session/session.module.ts index 407c59f..2ef9227 100644 --- a/backend/src/modules/session/session.module.ts +++ b/backend/src/modules/session/session.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Session } from 'src/entities'; +import { SessionGuard } from './guard'; import { SessionRepository } from './repository/session.repository'; import { SessionInitService, SessionSerializerService } from './services'; import { SessionService } from './services/session.service'; @@ -12,9 +13,10 @@ import { SessionService } from './services/session.service'; SessionInitService, SessionSerializerService, SessionRepository, + SessionGuard, SessionService, ], controllers: [], - exports: [SessionService], + exports: [SessionService, SessionGuard], }) export class SessionModule {}