diff --git a/apps/api-gateway/common/interface.ts b/apps/api-gateway/common/interface.ts index d3869d75f..c1791888b 100644 --- a/apps/api-gateway/common/interface.ts +++ b/apps/api-gateway/common/interface.ts @@ -1,3 +1,5 @@ +import { Prisma } from '@prisma/client'; + export interface ResponseType { statusCode: number; message: string; @@ -6,7 +8,21 @@ export interface ResponseType { } export interface ExceptionResponse { - message: string | string[] - error: string - statusCode: number + message: string | string[]; + error: string; + statusCode: number; +} + +export interface ISession { + id?: string; + sessionToken?: string; + userId?: string; + expires?: number; + refreshToken?: string; + keycloakUserId?: string; + type?: string; + accountId?: string; + sessionType?: string; + expiresAt?: Date; + clientInfo?: Prisma.JsonValue | null; } diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts index e5f9860f1..5120fd914 100644 --- a/apps/api-gateway/src/authz/jwt.strategy.ts +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -31,11 +31,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) { // Todo: We need to add this logic in seprate jwt gurd to handle the token expiration functionality. // eslint-disable-next-line @typescript-eslint/no-explicit-any const decodedToken: any = jwt.decode(jwtToken); - const currentTime = Math.floor(Date.now() / 1000); - if (decodedToken?.exp < currentTime) { - const sessionIds = { sessions: [decodedToken?.sid] }; - await this.authzService.logout(sessionIds); - } if (!decodedToken) { throw new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken); } diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 164119545..9c2e02465 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -302,3 +302,10 @@ export interface IRestrictedUserSession { clientInfo: Prisma.JsonValue | null; sessionType: string; } + +export interface ITokenData { + sessionToken: string; + expires: number; + refreshToken: string; + expiresAt: Date; +} diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index 38f8ef0a8..3889e96c2 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -1,12 +1,20 @@ /* eslint-disable camelcase */ /* eslint-disable prefer-destructuring */ +import { + BadRequestException, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException +} from '@nestjs/common'; import { IOrgUsers, IRestrictedUserSession, ISendVerificationEmail, ISession, IShareUserCertificate, + ITokenData, IUserDeletedActivity, IUserInformation, IUsersProfile, @@ -17,7 +25,6 @@ import { UserRoleDetails, UserRoleMapping } from '../interfaces/user.interface'; -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { Prisma, RecordType, @@ -722,6 +729,21 @@ export class UserRepository { } } + //this function is to fetch all session details for a user including token details without any restriction + async fetchUserSessionDetails(userId: string): Promise { + try { + const userSessionCount = await this.prisma.session.findMany({ + where: { + userId + } + }); + return userSessionCount; + } catch (error) { + this.logger.error(`Error in getting user session details: ${error.message} `); + throw error; + } + } + async checkAccountDetails(userId: string): Promise { try { const accountDetails = await this.prisma.account.findUnique({ @@ -980,8 +1002,13 @@ export class UserRepository { }); return userSession; } catch (error) { - this.logger.error(`Error in logging out user: ${error.message}`); - throw error; + if (error instanceof Prisma.PrismaClientKnownRequestError && 'P2025' === error.code) { + this.logger.warn(`Session not found for deletion: ${sessionId}`); + throw new NotFoundException('Record to be deleted not found'); + } else { + this.logger.error(`Error in logging out user: ${error.message}`); + throw error; + } } } @@ -1032,4 +1059,26 @@ export class UserRepository { throw error; } } + + async updateSessionToken(id: string, tokenData: ITokenData): Promise { + if (!id || !tokenData) { + throw new BadRequestException(`Missing id or tokenData for session details update`); + } + try { + const sessionResponse = await this.prisma.session.update({ + where: { + id + }, + data: tokenData + }); + return sessionResponse; + } catch (error) { + this.logger.error(`Error in creating session: ${error.message} `); + if (error instanceof Prisma.PrismaClientKnownRequestError && 'P2025' === error.code) { + this.logger.warn(`Session not found for update: ${id}`); + throw new NotFoundException('Session not found'); + } + throw error; + } + } } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 1e3905c71..77f967b5f 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -451,7 +451,7 @@ export class UserService { if (true === isPasskey && false === userData?.isFidoVerified) { throw new UnauthorizedException(ResponseMessages.user.error.registerFido); } - // called seprate method to delete exp session + this.userRepository.deleteInactiveSessions(userData?.id); const userSessionDetails = await this.userRepository.fetchUserSessions(userData?.id); if (Number(process.env.SESSIONS_LIMIT) <= userSessionDetails?.length) { @@ -466,8 +466,10 @@ export class UserService { const decryptedPassword = await this.commonService.decryptPassword(password); tokenDetails = await this.generateToken(email.toLowerCase(), decryptedPassword, userData); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decodedToken: any = jwt.decode(tokenDetails?.access_token); + const decodedToken = jwt.decode(tokenDetails?.refresh_token); + if (!decodedToken || 'object' !== typeof decodedToken || !decodedToken.exp || !decodedToken.sid) { + throw new UnauthorizedException(ResponseMessages.user.error.refreshTokenExpired); + } const expiresAt = new Date(decodedToken.exp * 1000); const sessionData = { @@ -537,53 +539,47 @@ export class UserService { async refreshTokenDetails(refreshToken: string): Promise { try { - try { - const data = jwt.decode(refreshToken) as jwt.JwtPayload; - const userByKeycloakId = await this.userRepository.getUserByKeycloakId(data?.sub); - this.logger.debug(`User details::;${JSON.stringify(userByKeycloakId)}`); - const tokenResponse = await this.clientRegistrationService.getAccessToken( - refreshToken, - userByKeycloakId?.['clientId'], - userByKeycloakId?.['clientSecret'] - ); - this.logger.debug(`tokenResponse::::${JSON.stringify(tokenResponse)}`); - // Fetch the details from account table based on userid and refresh token - const userAccountDetails = await this.userRepository.checkAccountDetails(userByKeycloakId?.['id']); - // Update the account details with latest access token, refresh token and exp date - if (!userAccountDetails) { - throw new NotFoundException(ResponseMessages.user.error.userAccountNotFound); - } - // Fetch session details - const sessionDetails = await this.userRepository.fetchSessionByRefreshToken(refreshToken); - if (!sessionDetails) { - throw new NotFoundException(ResponseMessages.user.error.userSeesionNotFound); - } - // Delete previous session - const deletePreviousSession = await this.userRepository.deleteSession(sessionDetails.id); - if (!deletePreviousSession) { - throw new InternalServerErrorException(ResponseMessages.user.error.errorInDeleteSession); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decodedToken: any = jwt.decode(tokenResponse?.access_token); - const expiresAt = new Date(decodedToken.exp * 1000); - const sessionData = { - sessionToken: tokenResponse.access_token, - userId: userByKeycloakId?.['id'], - expires: tokenResponse.expires_in, - refreshToken: tokenResponse.refresh_token, - sessionType: SessionType.USER_SESSION, - accountId: userAccountDetails.id, - expiresAt - }; - const addSessionDetails = await this.userRepository.createSession(sessionData); - if (!addSessionDetails) { - throw new InternalServerErrorException(ResponseMessages.user.error.errorInSessionCreation); - } + const data = jwt.decode(refreshToken) as jwt.JwtPayload; + const refreshTokenExp = new Date(data.exp * 1000); + const currentTime = new Date(); + if (refreshTokenExp < currentTime) { + await this.userRepository.deleteSession(data?.sid); + throw new UnauthorizedException(ResponseMessages.user.error.refreshTokenExpired); + } + const userByKeycloakId = await this.userRepository.getUserByKeycloakId(data?.sub); + const tokenResponse = await this.clientRegistrationService.getAccessToken( + refreshToken, + userByKeycloakId?.['clientId'], + userByKeycloakId?.['clientSecret'] + ); + // Fetch the details from account table based on userid and refresh token + const userAccountDetails = await this.userRepository.checkAccountDetails(userByKeycloakId?.['id']); + // Update the account details with latest access token, refresh token and exp date + if (!userAccountDetails) { + throw new NotFoundException(ResponseMessages.user.error.userAccountNotFound); + } + // Fetch session details - return tokenResponse; - } catch (error) { - throw new BadRequestException(ResponseMessages.user.error.invalidRefreshToken); + const sessionDetails = await this.userRepository.getSession(data.sid); + if (!sessionDetails) { + throw new NotFoundException(ResponseMessages.user.error.userSessionNotFound); } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const decodedToken: any = jwt.decode(tokenResponse?.refresh_token); + const expiresAt = new Date(decodedToken.exp * 1000); + const sessionData = { + sessionToken: tokenResponse.access_token, + expires: tokenResponse.expires_in, + refreshToken: tokenResponse.refresh_token, + expiresAt + }; + const addSessionDetails = await this.userRepository.updateSessionToken(tokenResponse.session_state, sessionData); + if (!addSessionDetails) { + throw new InternalServerErrorException(ResponseMessages.user.error.errorInSessionCreation); + } + + return tokenResponse; } catch (error) { this.logger.error(`In refreshTokenDetails : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); diff --git a/libs/common/src/interfaces/user.interface.ts b/libs/common/src/interfaces/user.interface.ts index 54ab6f7fc..98e36572c 100644 --- a/libs/common/src/interfaces/user.interface.ts +++ b/libs/common/src/interfaces/user.interface.ts @@ -6,6 +6,7 @@ export interface ISignInUser { refresh_token?: string; isRegisteredToSupabase?: boolean; sessionId?: string; + refresh_expires_in?: number; } export interface IVerifyUserEmail { email: string; diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 8be466d1c..bd8f5f6fd 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -73,7 +73,8 @@ export const ResponseMessages = { errorInDeleteSession: 'Error in deleting the session', errorInSessionCreation: 'Error in create session', userAccountNotFound: 'User account not found', - userSeesionNotFound: 'User session not found' + userSessionNotFound: 'User session not found', + refreshTokenExpired: 'Refresh token has expired' } }, organisation: {