Skip to content

Conversation

@sujitaw
Copy link
Contributor

@sujitaw sujitaw commented Oct 10, 2025

What

  • made changes in jwt stratergy to handle refresh token based auth.
  • modified refresh token API to store newly generated access token and refresh token.
  • added session deletion logic on expiration of refresh token.

Summary by CodeRabbit

  • New Features

    • Session expiry now aligns with the refresh token’s lifetime; session records include refresh expiry info.
    • Added explicit “Refresh token has expired” error message.
  • Bug Fixes

    • Corrected typo in "User session not found".
    • Reduced unexpected sign-outs by changing when expired tokens trigger session termination.
  • Refactor

    • Streamlined refresh flow and session updates with clearer, consolidated error handling and new session-management operations.

Signed-off-by: sujitaw <sujit.sutar@ayanworks.com>
@sujitaw sujitaw self-assigned this Oct 10, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 10, 2025

Walkthrough

Adds session/token interfaces and token-data handling, removes JWT-strategy runtime logout-on-expiry, adds session fetch/update methods and refresh-token-driven login/refresh flows, updates response messages, and reorders some imports.

Changes

Cohort / File(s) Summary
Session & token interfaces
apps/api-gateway/common/interface.ts, apps/user/interfaces/user.interface.ts, libs/common/src/interfaces/user.interface.ts
Added ISession and ITokenData; added refresh_expires_in? to ISignInUser; minor formatting tweaks to ExceptionResponse/ResponseType.
Response messages
libs/common/src/response-messages/index.ts
Renamed userSeesionNotFounduserSessionNotFound; added refreshTokenExpired.
JWT strategy
apps/api-gateway/src/authz/jwt.strategy.ts
Removed runtime token-expiry handling and logout-on-expiry; retains decoded-token presence validation and Unauthorized on invalid token.
User repository
apps/user/repositories/user.repository.ts
Added fetchUserSessionDetails(userId): Promise<ISession[]> and updateSessionToken(id, tokenData): Promise<session); improved P2025 error handling mapping to NotFound/RpcException; consolidated imports and added ITokenData usage.
User service (login/refresh)
apps/user/src/user.service.ts
Login/refresh flows switched to derive expiry from refresh_token; refresh flow validates refresh expiry up-front, updates existing session via updateSessionToken, and streamlines error paths.
Client registration imports
libs/client-registration/src/client-registration.service.ts
Reordered/expanded imports (added IClientRoles, IFormattedResponse, payload DTOs); no runtime behavior changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UserService as UserService
  participant TokenSvc as TokenService
  participant Repo as UserRepository
  note over UserService,Repo: Login (updated to use refresh_token expiry)
  User->>UserService: login(credentials)
  UserService->>TokenSvc: generate tokens
  TokenSvc-->>UserService: { access_token, refresh_token }
  UserService->>TokenSvc: decode(refresh_token)
  TokenSvc-->>UserService: { exp, sid, ... }
  UserService->>Repo: create/update session with expiresAt from refresh_token
  Repo-->>UserService: session stored
  UserService-->>User: tokens + session details
Loading
sequenceDiagram
  autonumber
  actor Client
  participant UserService as UserService
  participant TokenSvc as TokenService
  participant Repo as UserRepository
  participant Account as AccountService
  rect rgb(245,249,255)
    note over Client,UserService: Refresh token flow (reworked)
    Client->>UserService: refreshTokenDetails(refresh_token)
    UserService->>TokenSvc: decode(refresh_token)
    TokenSvc-->>UserService: { exp, sub, sid }
    alt refresh token expired
      UserService-->>Client: error: refreshTokenExpired
    else valid
      UserService->>Repo: getSession(sid)
      Repo-->>UserService: session | not found
      alt session not found
        UserService-->>Client: error: userSessionNotFound
      else session found
        UserService->>Account: validate account/session
        Account-->>UserService: ok
        UserService->>TokenSvc: issue new tokens
        TokenSvc-->>UserService: { access_token, refresh_token }
        UserService->>Repo: updateSessionToken(sid, tokenData)
        Repo-->>UserService: updated session
        UserService-->>Client: token response
      end
    end
  end
Loading
sequenceDiagram
  autonumber
  participant Gateway as API Gateway
  participant JwtStrat as JWT Strategy
  note over JwtStrat: Strategy change — no logout on expiry
  Gateway->>JwtStrat: validate(request)
  JwtStrat->>JwtStrat: decode token
  alt invalid token
    JwtStrat-->>Gateway: UnauthorizedException
  else valid token
    JwtStrat-->>Gateway: proceed
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25–30 minutes

Possibly related PRs

Suggested reviewers

  • shitrerohit
  • RinkalBhojani
  • GHkrishna

Poem

A rabbit taps keys with a hop and a grin,
Refresh ticks whisper when sessions begin.
Tokens replaced with a gentler beat,
Sessions updated, no abrupt defeat.
Carrots for code, and tests passing in. 🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly captures the core change of shifting session management to use the refresh token and directly relates to the main updates in the PR.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/jwt_token_management

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libs/client-registration/src/client-registration.service.ts (1)

178-181: Stop logging tokens, secrets, and sensitive payloads.

Logs at these sites include client_secret, access/refresh tokens, or whole payloads/responses. This is a security and compliance risk.

  • Remove or mask sensitive fields (client_secret, access_token, refresh_token).
  • Prefer structured logs with redaction.

Example masking pattern:

this.logger.debug('Token response received', {
  endpoint: 'SAT',
  has_access_token: !!tokenResponse?.access_token,
  // do not log token values
});

Also applies to: 345-347, 695-696

apps/user/src/user.service.ts (1)

484-496: Guard decode of refresh_token to avoid NPE and fallback on refresh_expires_in

jwt.decode can return null/string; exp may be missing. Add a safe fallback.

Apply this diff:

-      const decodedToken: any = jwt.decode(tokenDetails?.refresh_token);
-      const expiresAt = new Date(decodedToken.exp * 1000);
+      const decodedRefresh = jwt.decode(tokenDetails?.refresh_token) as jwt.JwtPayload | null;
+      const expiresAt =
+        decodedRefresh?.exp
+          ? new Date(decodedRefresh.exp * 1000)
+          : new Date(Date.now() + (tokenDetails?.refresh_expires_in ?? 0) * 1000);
🧹 Nitpick comments (8)
libs/client-registration/src/client-registration.service.ts (1)

21-23: DTO naming consistency (optional).

accessTokenPayloadDto and userTokenPayloadDto are constructed via new but use lowerCamelCase. Nest/TS conventions prefer PascalCase for classes/DTOs (e.g., AccessTokenPayloadDto).

If feasible, rename classes/files to PascalCase to align with codebase conventions.

libs/common/src/interfaces/user.interface.ts (1)

9-9: Clarify units for refresh_expires_in.

Is this seconds (Keycloak default) or milliseconds? Ensure it matches expires_in units and document to avoid misuse.

Add a brief JSDoc comment indicating the unit.

apps/user/interfaces/user.interface.ts (1)

305-310: Disambiguate expiry fields in ITokenData.

Having both expires: number and expiresAt: Date is redundant and can drift. Define one source of truth or clearly document:

  • expires: seconds since epoch?
  • expiresAt: absolute Date?

Consider keeping only expiresAt (Date) or renaming expires → expiresInSeconds with clear units.

apps/api-gateway/common/interface.ts (2)

1-2: Avoid Prisma type leakage in gateway.

Importing Prisma just for JsonValue couples the gateway to ORM internals.

Use a local JsonValue type (see apps/api-gateway/src/issuance/utils/helper.ts) or a shared union in a common lib.


16-28: DRY: ISession duplication across services.

This ISession mirrors apps/user/interfaces/user.interface.ts. Duplicated contracts will drift.

Extract ISession into a shared lib (e.g., @credebl/common/interfaces) and import from there. Also replace Prisma.JsonValue with a shared JsonValue.

apps/user/src/user.controller.ts (1)

98-101: LGTM; add DTO validation for payload.

Handler is fine. Consider a DTO with class-validator to enforce refreshToken presence/type.

Example:

export class GenerateAccessTokenUsingRefreshTokenDto {
  @IsString()
  refreshToken: string;
}

Then type payload accordingly.

apps/user/repositories/user.repository.ts (2)

732-745: Minimize exposure: select only needed fields for session cleanup

This method returns full session records “without any restriction,” including tokens. Prefer selecting only fields required by callers to reduce accidental leakage.

Apply this diff:

-  async fetchUserSessionDetails(userId: string): Promise<ISession[]> {
+  async fetchUserSessionDetails(userId: string): Promise<ISession[]> {
     try {
-      const userSessionCount = await this.prisma.session.findMany({
-        where: {
-          userId
-        }
-      });
-      return userSessionCount;
+      const sessions = await this.prisma.session.findMany({
+        where: { userId },
+        select: { id: true, refreshToken: true, expiresAt: true }
+      });
+      return sessions as unknown as ISession[];
     } catch (error) {
       this.logger.error(`Error in getting user session details: ${error.message} `);
       throw error;
     }
   }

1005-1011: Good P2025 handling; align other methods for consistency

Catching Prisma P2025 and mapping to NotFound is good. Mirror this pattern in deleteSessionBySessionId for consistent behavior across repository methods.

Suggested change outside this range (deleteSessionBySessionId):

} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
    throw new RpcException(new NotFoundException(`Session not found for userId: ${userId}`));
  }
  this.logger.error(`Error in Deleting Session: ${error.message}`);
  throw error;
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69b8f27 and fbc34ff.

📒 Files selected for processing (10)
  • apps/api-gateway/common/interface.ts (2 hunks)
  • apps/api-gateway/src/authz/jwt.strategy.ts (0 hunks)
  • apps/api-gateway/src/user/user.service.ts (2 hunks)
  • apps/user/interfaces/user.interface.ts (1 hunks)
  • apps/user/repositories/user.repository.ts (4 hunks)
  • apps/user/src/user.controller.ts (1 hunks)
  • apps/user/src/user.service.ts (3 hunks)
  • libs/client-registration/src/client-registration.service.ts (1 hunks)
  • libs/common/src/interfaces/user.interface.ts (1 hunks)
  • libs/common/src/response-messages/index.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/api-gateway/src/authz/jwt.strategy.ts
🧰 Additional context used
🧬 Code graph analysis (5)
apps/api-gateway/src/user/user.service.ts (3)
apps/api-gateway/src/authz/authz.controller.ts (1)
  • refreshToken (328-337)
apps/api-gateway/src/authz/authz.service.ts (1)
  • refreshToken (80-82)
libs/common/src/interfaces/user.interface.ts (1)
  • ISignInUser (1-10)
apps/user/src/user.controller.ts (1)
libs/common/src/interfaces/user.interface.ts (1)
  • ISignInUser (1-10)
apps/api-gateway/common/interface.ts (2)
apps/user/interfaces/user.interface.ts (1)
  • ISession (185-197)
apps/api-gateway/src/issuance/utils/helper.ts (1)
  • JsonValue (5-5)
apps/user/src/user.service.ts (5)
apps/api-gateway/src/authz/authz.controller.ts (2)
  • refreshToken (328-337)
  • sessionDetails (223-241)
apps/api-gateway/src/authz/authz.service.ts (1)
  • refreshToken (80-82)
apps/api-gateway/src/authz/jwt-payload.interface.ts (1)
  • JwtPayload (1-13)
libs/common/src/response-messages/index.ts (1)
  • ResponseMessages (1-519)
libs/common/src/interfaces/user.interface.ts (1)
  • ISignInUser (1-10)
apps/user/repositories/user.repository.ts (3)
apps/api-gateway/common/interface.ts (1)
  • ISession (16-28)
apps/user/interfaces/user.interface.ts (2)
  • ISession (185-197)
  • ITokenData (305-310)
libs/logger/src/logger.service.ts (1)
  • error (51-54)
🔇 Additional comments (3)
apps/api-gateway/common/interface.ts (1)

11-14: OK: ExceptionResponse typing tightened.

apps/api-gateway/src/user/user.service.ts (2)

10-10: Import update looks good.


77-81: LGTM; confirm subject naming across services.

Method correctly proxies to user service. Double-check we need both flows:

  • refresh-token-details (existing)
  • generate-accessToken-using-refresh-token (new)

If both remain, document their distinct purposes to avoid caller confusion.

Signed-off-by: sujitaw <sujit.sutar@ayanworks.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
libs/common/src/response-messages/index.ts (1)

76-76: Typo fix already flagged in previous review.

The typo correction from userSeesionNotFound to userSessionNotFound was already identified in a previous review comment.

apps/user/src/user.service.ts (1)

540-583: Critical: Multiple security and reliability issues in refresh token flow.

This method has several critical issues that remain from previous reviews:

  1. Line 540: Unsafe jwt.decode without validation - can crash if token is malformed
  2. Lines 548, 554: Logging PII (user details) and sensitive data (tokens, client secrets)
  3. Line 544: Session deletion not wrapped in try-catch - could mask refreshTokenExpired error
  4. Line 577: Using tokenResponse.session_state instead of sessionDetails.id for update - may fail or update wrong session
  5. Lines 569-570: Unsafe decode of new refresh token without validation

Apply this comprehensive diff to address all issues:

-      const data = jwt.decode(refreshToken) as jwt.JwtPayload;
-      const refreshTokenExp = new Date(data.exp * 1000);
+      const payload = jwt.decode(refreshToken) as jwt.JwtPayload | null;
+      if (!payload || typeof payload !== 'object' || !payload.exp || !payload.sid || !payload.sub) {
+        throw new UnauthorizedException(ResponseMessages.user.error.invalidRefreshToken);
+      }
+      const refreshTokenExp = new Date(payload.exp * 1000);
       const currentTime = new Date();
       if (refreshTokenExp < currentTime) {
-        await this.userRepository.deleteSession(data?.sid);
+        // Best-effort cleanup; ignore not-found errors
+        try {
+          await this.userRepository.deleteSession(payload.sid as string);
+        } catch (err) {
+          // Ignore deletion errors (e.g., session already deleted)
+        }
         throw new UnauthorizedException(ResponseMessages.user.error.refreshTokenExpired);
       }
-      const userByKeycloakId = await this.userRepository.getUserByKeycloakId(data?.sub);
-      this.logger.debug(`User details::;${JSON.stringify(userByKeycloakId)}`);
+      const userByKeycloakId = await this.userRepository.getUserByKeycloakId(payload.sub as string);
       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.getSession(data.sid);
+      const sessionDetails = await this.userRepository.getSession(payload.sid as string);
       if (!sessionDetails) {
-        throw new NotFoundException(ResponseMessages.user.error.userSessionNotFound);
+        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 decodedRefresh = jwt.decode(tokenResponse?.refresh_token) as jwt.JwtPayload | null;
+      const expiresAt =
+        decodedRefresh?.exp
+          ? new Date(decodedRefresh.exp * 1000)
+          : new Date(Date.now() + (tokenResponse?.refresh_expires_in ?? 0) * 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);
+      const addSessionDetails = await this.userRepository.updateSessionToken(sessionDetails.id, sessionData);
       if (!addSessionDetails) {
         throw new InternalServerErrorException(ResponseMessages.user.error.errorInSessionCreation);
       }
 
       return tokenResponse;

Based on past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fbc34ff and 71b3285.

📒 Files selected for processing (3)
  • apps/user/repositories/user.repository.ts (4 hunks)
  • apps/user/src/user.service.ts (3 hunks)
  • libs/common/src/response-messages/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/user/repositories/user.repository.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/user/src/user.service.ts (3)
apps/api-gateway/src/authz/authz.controller.ts (1)
  • refreshToken (328-337)
apps/api-gateway/src/authz/authz.service.ts (1)
  • refreshToken (80-82)
apps/api-gateway/src/authz/jwt-payload.interface.ts (1)
  • JwtPayload (1-13)
🔇 Additional comments (2)
libs/common/src/response-messages/index.ts (1)

77-77: LGTM — refreshTokenExpired is referenced in apps/user/src/user.service.ts:545.

apps/user/src/user.service.ts (1)

455-455: deleteInactiveSessions correctly uses stored expiresAt
The repository implementation deletes sessions where expiresAt < new Date(), so it relies on the stored expiry timestamp rather than decoding tokens.

Signed-off-by: sujitaw <sujit.sutar@ayanworks.com>
@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
apps/user/src/user.service.ts (4)

542-548: Critical: Validate decoded refresh token before accessing claims.

The code directly accesses data.exp and data.sid without validating that jwt.decode returned a valid payload. If the token is malformed, this will crash.

Apply this diff to add validation:

-      const data = jwt.decode(refreshToken) as jwt.JwtPayload;
-      const refreshTokenExp = new Date(data.exp * 1000);
+      const payload = jwt.decode(refreshToken) as jwt.JwtPayload | null;
+      if (!payload || typeof payload !== 'object' || !payload.exp || !payload.sid || !payload.sub) {
+        throw new UnauthorizedException(ResponseMessages.user.error.invalidRefreshToken);
+      }
+      const refreshTokenExp = new Date(payload.exp * 1000);
       const currentTime = new Date();
       if (refreshTokenExp < currentTime) {
-        await this.userRepository.deleteSession(data?.sid);
+        try {
+          await this.userRepository.deleteSession(payload.sid as string);
+        } catch (_) {
+          // Best-effort cleanup; ignore not-found errors
+        }
         throw new UnauthorizedException(ResponseMessages.user.error.refreshTokenExpired);
       }
-      const userByKeycloakId = await this.userRepository.getUserByKeycloakId(data?.sub);
+      const userByKeycloakId = await this.userRepository.getUserByKeycloakId(payload.sub as string);

550-550: Remove PII from logs.

Logging the full user object exposes personally identifiable information (PII). Remove this debug log.

Apply this diff:

-      this.logger.debug(`User details::;${JSON.stringify(userByKeycloakId)}`);

556-556: Remove sensitive token data from logs.

Logging tokenResponse exposes access tokens, refresh tokens, and client secrets. Remove this debug log.

Apply this diff:

-      this.logger.debug(`tokenResponse::::${JSON.stringify(tokenResponse)}`);

570-579: Critical: Validate decoded refresh token and use correct session ID.

Two issues here:

  1. The decoded token is typed as any and accessed without validation.
  2. Line 579 uses tokenResponse.session_state instead of sessionDetails.id to update the session.

Apply this diff:

-      // 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 decodedRefresh = jwt.decode(tokenResponse?.refresh_token) as jwt.JwtPayload | null;
+      const expiresAt =
+        decodedRefresh?.exp
+          ? new Date(decodedRefresh.exp * 1000)
+          : new Date(Date.now() + (tokenResponse?.refresh_expires_in ?? 0) * 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);
+      const addSessionDetails = await this.userRepository.updateSessionToken(sessionDetails.id, sessionData);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71b3285 and a1bb7cb.

📒 Files selected for processing (1)
  • apps/user/src/user.service.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/user/src/user.service.ts (4)
libs/common/src/response-messages/index.ts (1)
  • ResponseMessages (1-519)
apps/api-gateway/src/authz/authz.controller.ts (1)
  • refreshToken (328-337)
apps/api-gateway/src/authz/authz.service.ts (1)
  • refreshToken (80-82)
apps/api-gateway/src/authz/jwt-payload.interface.ts (1)
  • JwtPayload (1-13)
🔇 Additional comments (1)
apps/user/src/user.service.ts (1)

565-568: Use consistent error message key.

The error message key userSessionNotFound should match the response messages definition. Based on the relevant code snippets, the key is userSessionNotFound (not userSeesionNotFound).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant