From a341196f37d49048f6a897ec8a1bf47e41512c04 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Fri, 31 May 2024 08:11:11 +0200 Subject: [PATCH] rename entity colum, divide repo and service --- .../src/entities/user-credentials.entity.ts | 2 +- .../src/modules/auth-module/auth.module.ts | 2 + .../repositories/session.repository.ts | 48 ++++++++++++++ .../user-credentials.repository.ts | 6 +- .../auth-module/services/auth.service.ts | 37 ++++++----- .../auth-module/services/session.service.ts | 65 ++++++++++--------- 6 files changed, 110 insertions(+), 50 deletions(-) create mode 100644 backend/src/modules/auth-module/repositories/session.repository.ts diff --git a/backend/src/entities/user-credentials.entity.ts b/backend/src/entities/user-credentials.entity.ts index fad91b1..2f8eff4 100644 --- a/backend/src/entities/user-credentials.entity.ts +++ b/backend/src/entities/user-credentials.entity.ts @@ -18,7 +18,7 @@ export class UserCredentials { public hash: string; @Column({ nullable: true }) - public hashedRt?: string; + public refreshToken?: string; @CreateDateColumn() public createdAt: Date; diff --git a/backend/src/modules/auth-module/auth.module.ts b/backend/src/modules/auth-module/auth.module.ts index 9f440c3..ff76261 100644 --- a/backend/src/modules/auth-module/auth.module.ts +++ b/backend/src/modules/auth-module/auth.module.ts @@ -8,6 +8,7 @@ import { UserModule } from '../user-module/user.module'; import { VerifyModule } from '../verify-module/verify.module'; import { AuthController } from './controller/auth.controller'; +import { SessionRepository } from './repositories/session.repository'; import { UserCredentialsRepository } from './repositories/user-credentials.repository'; import { AuthService } from './services/auth.service'; import { SessionService } from './services/session.service'; @@ -27,6 +28,7 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies'; SessionService, TokenManagementService, UserCredentialsRepository, + SessionRepository, AccessTokenStrategy, RefreshTokenStrategy, ], diff --git a/backend/src/modules/auth-module/repositories/session.repository.ts b/backend/src/modules/auth-module/repositories/session.repository.ts new file mode 100644 index 0000000..9cb7bb3 --- /dev/null +++ b/backend/src/modules/auth-module/repositories/session.repository.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Session } from 'src/entities'; +import { Repository } from 'typeorm'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class SessionRepository { + public constructor( + @InjectRepository(Session) + private sessionRepository: Repository + ) {} + + public async createSession( + userId: string, + userAgent: string + ): Promise { + const sessionId = uuidv4(); + const expirationDate = new Date(); + + expirationDate.setHours(expirationDate.getHours() + 1); + const session = this.sessionRepository.create({ + userCredentials: userId, + sessionId, + expiresAt: expirationDate, + userAgent, + }); + + await this.sessionRepository.save(session); + return session; + } + + public async validateSessionUserAgent( + sessionId: string, + currentUserAgent: string + ): Promise { + const session = await this.sessionRepository.findOne({ + where: { sessionId: sessionId }, + select: ['userAgent'], + }); + + if (!session) { + return false; + } + + return session.userAgent === currentUserAgent; + } +} diff --git a/backend/src/modules/auth-module/repositories/user-credentials.repository.ts b/backend/src/modules/auth-module/repositories/user-credentials.repository.ts index 86e8cae..a2bf7f0 100644 --- a/backend/src/modules/auth-module/repositories/user-credentials.repository.ts +++ b/backend/src/modules/auth-module/repositories/user-credentials.repository.ts @@ -31,11 +31,11 @@ export class UserCredentialsRepository { return this.repository.findOne({ where: { id: userId } }); } - public async updateUserTokenHash( + public async updateUserRefreshToken( userId: string, - hashedRt: string | null + refreshToken: string | null ): Promise { - const result = await this.repository.update(userId, { hashedRt }); + const result = await this.repository.update(userId, { refreshToken }); return result.affected ?? 0; } diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index df0ecef..595c986 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -79,6 +79,8 @@ export class AuthService { throw new ForbiddenException('Access Denied'); } + await this.sessionService.checkSessionLimit(user.id); + const sesseionId = await this.sessionService.createSession( user.id, request.headers['user-agent'] @@ -90,11 +92,10 @@ export class AuthService { } public async logout(userId: string): Promise { - // TODO Check if the user is logged out already - const affected = await this.userCredentialsRepository.updateUserTokenHash( - userId, - null - ); + const affected = + await this.userCredentialsRepository.updateUserRefreshToken(userId, null); + + await this.sessionService.invalidateAllSessionsForUser(userId); return affected > 0; } @@ -118,19 +119,20 @@ export class AuthService { request.headers['user-agent'] ); - // TODO expand session expiration - 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 + decodedToken.email, + false ); return { access_token: newTokens.access_token }; @@ -138,29 +140,33 @@ export class AuthService { private async generateAndPersistTokens( userId: string, - email: string + email: string, + updateRefreshToken: boolean = false ): Promise { const tokens = await this.tokenManagementService.generateTokens( userId, email ); - await this.userCredentialsRepository.updateUserTokenHash( - userId, - tokens.refresh_token - ); + if (updateRefreshToken) { + await this.userCredentialsRepository.updateUserRefreshToken( + userId, + tokens.refresh_token + ); + } + 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.hashedRt) { + if (!user || !user.refreshToken) { throw new Error('No refresh token found'); } const decodedToken = await this.tokenManagementService.verifyRefreshToken( - user.hashedRt + user.refreshToken ); if (decodedToken.exp < Date.now() / 1000) { @@ -170,6 +176,7 @@ export class AuthService { if (decodedToken.sub !== user.id) { throw new Error('Token subject mismatch'); } + return decodedToken; } } diff --git a/backend/src/modules/auth-module/services/session.service.ts b/backend/src/modules/auth-module/services/session.service.ts index 49e14f5..fae9da1 100644 --- a/backend/src/modules/auth-module/services/session.service.ts +++ b/backend/src/modules/auth-module/services/session.service.ts @@ -3,68 +3,71 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Response } from 'express'; import { Session } from 'src/entities'; import { LessThan, Repository } from 'typeorm'; -import { v4 as uuidv4 } from 'uuid'; + +import { SessionRepository } from '../repositories/session.repository'; @Injectable() export class SessionService { public constructor( @InjectRepository(Session) - private sessionRepository: Repository + private sessionRepository: Repository, + private readonly sessionRepository2: SessionRepository ) {} public async createSession( userId: string, userAgent: string ): Promise { - const sessionId = uuidv4(); - const expirationDate = new Date(); - - expirationDate.setHours(expirationDate.getHours() + 1); - - const session = this.sessionRepository.create({ - userCredentials: userId, - sessionId: sessionId, - expiresAt: expirationDate, - userAgent: userAgent, - }); - - await this.sessionRepository.save(session); - return session; + return this.sessionRepository2.createSession(userId, userAgent); } public async validateSessionUserAgent( sessionId: string, currentUserAgent: string ): Promise { - const session = await this.sessionRepository.findOne({ - where: { sessionId: sessionId }, - select: ['userAgent'], - }); + return this.sessionRepository2.validateSessionUserAgent( + sessionId, + currentUserAgent + ); + } - if (!session) { - return false; + public async checkSessionLimit(userId: string): Promise { + const userSessions = await this.sessionRepository + .createQueryBuilder('session') + .leftJoinAndSelect('session.userCredentials', 'userCredentials') + .where('userCredentials.id = :userId', { userId }) + .orderBy('session.expiresAt', 'ASC') + .getMany(); + + if (userSessions.length >= 5) { + await this.sessionRepository.delete(userSessions[0].id); } - - return session.userAgent === currentUserAgent; } - // TODO use method to invalidate session - public async invalidateSession(sessionId: string): Promise { - await this.sessionRepository.delete({ sessionId: sessionId }); - } - - // TODO use method to invalidate all sessions for user public async invalidateAllSessionsForUser(userId: string): Promise { await this.sessionRepository.delete({ userCredentials: userId }); } - // TODO use method to clear expired sessions + // TODO Add cron job to clear expired sessions public async clearExpiredSessions(): Promise { const now = new Date(); await this.sessionRepository.delete({ expiresAt: LessThan(now) }); } + public async extendSessionExpiration(sessionId: string): Promise { + const session = await this.sessionRepository.findOne({ + where: { sessionId }, + }); + + if (session) { + session.expiresAt = new Date( + session.expiresAt.setMinutes(session.expiresAt.getMinutes() + 30) + ); + await this.sessionRepository.save(session); + } + } + public async findSessionBySessionId(sessionId: string): Promise { return this.sessionRepository.findOne({ where: { sessionId: sessionId },