Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Peter/fix refresh token #242

Closed
wants to merge 6 commits into from
Closed

Conversation

peterphanouvong
Copy link
Collaborator

@peterphanouvong peterphanouvong commented Nov 13, 2024

Explain your changes

  • refresh tokens on a timeout (once access token has expired)

resolves: #237

Checklist

Copy link
Contributor

coderabbitai bot commented Nov 13, 2024

Walkthrough

The pull request introduces a new authentication management system in a React application by replacing the existing AuthProvider.jsx with AuthProvider.tsx. The new TypeScript file implements an authentication context using React's context API, providing various methods for accessing user data and feature flags. Additionally, several exports related to authentication and token management have been updated across multiple files, including changes to import paths and the introduction of a new token refresh function. The overall structure and type safety have been enhanced.

Changes

File Change Summary
src/frontend/AuthProvider.jsx Deleted file containing the old authentication context provider.
src/frontend/AuthProvider.tsx New file implementing the authentication context with TypeScript, including KindeProvider, useKindeAuth, and token fetching logic.
src/frontend/KindeBrowserClient.js Updated import statement for flagDataTypeMap to reflect the new file structure.
src/frontend/index.js Removed exports for KindeProvider and useKindeAuth from the deleted AuthProvider.jsx.
src/frontend/index.ts Added exports for KindeProvider, useKindeAuth, and useKindeBrowserClient from their respective files.
src/handlers/setup.ts Updated setup function to use getAccessTokenWithRefresh, added type assertions, and modified return structure for improved type safety.
src/index.js Updated export path for isTokenValid.
src/session/getAccessTokenWithRefresh.ts New file with a function to manage access token retrieval and refresh logic.
src/session/index.ts Streamlined import statements and updated the structure of the exported function to include new factory functions.
src/session/isAuthenticated.js Updated import path for isTokenValid.
src/utils/isTokenValid.ts Modified audience validation logic and updated import path for config.
types.d.ts Updated KindeOrganization and KindeSetupResponse types with new naming conventions and additional fields.

Possibly related PRs

Suggested reviewers

  • DaveOrDead
  • DanielRivers

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 eslint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/frontend/AuthProvider.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-jest".

(The package "eslint-plugin-jest" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-jest@latest --save-dev

The plugin "eslint-plugin-jest" was referenced from the config file in ".eslintrc.json".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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: 11

🧹 Outside diff range and nitpick comments (15)
src/session/isAuthenticated.js (2)

Line range hint 11-20: Consider implementing token refresh mechanism

Given that this PR aims to implement token refresh functionality, the current implementation might need enhancement. When the token is invalid (expired), instead of immediately returning false, consider attempting to refresh the token first.

Consider updating the implementation like this:

 export const isAuthenticatedFactory = (req, res) => async () => {
   const accessToken = await (
     await sessionManager(req, res)
   ).getSessionItem('access_token');
 
-  const validToken = isTokenValid(accessToken);
+  let validToken = isTokenValid(accessToken);
+  
+  // If token is invalid, attempt to refresh
+  if (!validToken) {
+    const refreshToken = await (
+      await sessionManager(req, res)
+    ).getSessionItem('refresh_token');
+    
+    if (refreshToken) {
+      try {
+        const newTokens = await refreshAccessToken(refreshToken);
+        await (await sessionManager(req, res)).setSessionItems(newTokens);
+        validToken = true;
+      } catch (error) {
+        console.error('Failed to refresh token:', error);
+        validToken = false;
+      }
+    }
+  }
 
   const user = await getUserFactory(req, res)();
   return validToken && Boolean(user);
 };

Line range hint 6-10: Consider migrating to TypeScript and improving error handling

Since other files are being converted to TypeScript (e.g., AuthProvider.tsx), consider:

  1. Converting this file to TypeScript for better type safety
  2. Adding more robust error handling and logging

Example TypeScript conversion:

import { NextApiRequest, NextApiResponse } from 'next';

export const isAuthenticatedFactory = (
  req?: NextApiRequest,
  res?: NextApiResponse
) => async (): Promise<boolean> => {
  try {
    const session = await sessionManager(req, res);
    const accessToken = await session.getSessionItem('access_token');
    
    if (!accessToken) {
      console.warn('No access token found in session');
      return false;
    }

    const validToken = isTokenValid(accessToken);
    const user = await getUserFactory(req, res)();
    
    return validToken && Boolean(user);
  } catch (error) {
    console.error('Authentication check failed:', error);
    return false;
  }
};
src/utils/isTokenValid.ts (2)

Line range hint 4-28: Consider adding expiration buffer and error reporting

The token validation is thorough but could be enhanced in two ways:

  1. Add a small buffer (e.g., 30 seconds) to the expiration check to prevent edge cases where the token expires during request processing
  2. Return specific validation failure reasons for better debugging

Here's a suggested enhancement:

-const isTokenValid = (token) => {
+const isTokenValid = (token, expirationBufferSeconds = 30) => {
   const accessToken = token?.access_token ?? token;
-  if (!accessToken) return false;
+  if (!accessToken) return { isValid: false, reason: 'No token provided' };

   const accessTokenHeader = jwtDecoder(accessToken, TokenPart.header);
   const accessTokenPayload = jwtDecoder(accessToken);
   let isAudienceValid = true;
+  let validationErrors = [];

   // ... audience validation logic ...

+  const currentTime = Math.floor(Date.now() / 1000);
+  const isExpired = accessTokenPayload.exp <= currentTime + expirationBufferSeconds;

   if (
     accessTokenPayload.iss == config.issuerURL &&
     accessTokenHeader.alg == 'RS256' &&
-    accessTokenPayload.exp > Math.floor(Date.now() / 1000) &&
+    !isExpired &&
     isAudienceValid
   ) {
-    return true;
+    return { isValid: true };
   } else {
-    return false;
+    if (accessTokenPayload.iss !== config.issuerURL) validationErrors.push('Invalid issuer');
+    if (accessTokenHeader.alg !== 'RS256') validationErrors.push('Invalid algorithm');
+    if (isExpired) validationErrors.push('Token expired or expiring soon');
+    if (!isAudienceValid) validationErrors.push('Invalid audience');
+    return { isValid: false, reasons: validationErrors };
   }
 };

This enhancement would help with debugging token validation issues and provide a grace period for expiring tokens.


Line range hint 1-28: Alignment with token refresh mechanism

The enhanced token validation aligns well with the PR's objective of implementing token refresh. The more robust audience validation and suggested improvements for expiration handling will help ensure smooth token refresh flows.

Consider documenting the relationship between this validation logic and the token refresh mechanism in the codebase, particularly how the expiration check triggers the refresh flow.

src/session/index.ts (2)

Line range hint 27-28: Remove @ts-ignore by properly typing the response

The @ts-ignore comment suggests a type safety issue. Consider properly typing the response from kindeClient.refreshTokens to maintain type safety throughout the codebase.

-        // @ts-ignore
-        const response = await kindeClient.refreshTokens(
+        const response: {
+          access_token: string;
+          refresh_token: string;
+          expires_in: number;
+        } = await kindeClient.refreshTokens(

Line range hint 25-37: Improve error handling in refreshTokens method

The current implementation has several areas for improvement:

  1. The error type in the catch clause should be specified
  2. Returning null on error makes it difficult for consumers to handle errors appropriately
  3. Error information is lost when not in debug mode

Consider implementing this improved version:

     refreshTokens: async () => {
       try {
-        // @ts-ignore
         const response = await kindeClient.refreshTokens(
           await sessionManager(req, res)
         );
         return response;
-      } catch (error) {
+      } catch (error: unknown) {
         if (config.isDebugMode) {
           console.error(error);
         }
-        return null;
+        throw new Error(
+          error instanceof Error
+            ? error.message
+            : 'Failed to refresh tokens'
+        );
       }
     },
src/handlers/setup.ts (1)

Line range hint 74-109: Consider extracting mapping logic to separate utilities

The response construction contains complex nested property access and mapping logic. This could be simplified for better maintainability.

Consider extracting the organization properties mapping:

function mapOrganizationProperties(props: KindeTokenOrganizationProperties) {
  return {
    city: props?.kp_org_city?.v,
    industry: props?.kp_org_industry?.v,
    postcode: props?.kp_org_postcode?.v,
    state_region: props?.kp_org_state_region?.v,
    street_address: props?.kp_org_street_address?.v,
    street_address_2: props?.kp_org_street_address_2?.v
  };
}

And the organizations mapping:

function mapOrganizations(orgCodes: string[], orgs: Array<{id: string; name: string}>) {
  return {
    orgCodes,
    orgs: orgs?.map(org => ({
      code: org?.id,
      name: org?.name
    }))
  };
}
src/frontend/KindeBrowserClient.js (3)

Line range hint 31-53: Critical: Implement token expiration handling and automatic refresh

The current implementation lacks essential token refresh functionality:

  1. No mechanism to detect token expiration
  2. No automatic refresh trigger on token expiry
  3. Missing error handling for 401/403 responses that could indicate expired tokens

This doesn't fulfill the PR's objective of implementing token refresh functionality.

Consider implementing the following:

 const refreshData = async () => {
   const setupUrl = `${apiPath}/setup`;
   const res = await fetch(setupUrl);
+  // Handle 401/403 responses
+  if (res.status === 401 || res.status === 403) {
+    // Attempt token refresh
+    const refreshResult = await fetch(`${apiPath}/refresh`, {
+      method: 'POST',
+      credentials: 'include'
+    });
+    if (refreshResult.ok) {
+      // Retry original request with new token
+      return refreshData();
+    }
+  }
   const kindeData = await res.json();
   if (res.status == 200) {
     setState({

Line range hint 127-139: Fix inconsistent error handling in flag getters

The getStringFlag function has inconsistent error handling compared to other flag getters:

  1. Unused err; statement at the end
  2. Silent error swallowing could mask issues

Apply this fix:

 const getStringFlag = (code, defaultValue) => {
   try {
     const flag = getFlag(code, defaultValue, 's');
     return flag.value;
   } catch (err) {
     if (config.isDebugMode) {
       console.error(err);
     }
-    err;
+    return undefined;
   }
 };

Line range hint 13-26: Architecture Advice: Consider implementing a comprehensive token management system

The current implementation needs a more robust token management system to fulfill the PR's objective. Consider:

  1. Implement token expiration tracking using JWT expiry claims
  2. Add automatic refresh mechanism before token expiration
  3. Handle concurrent requests during refresh
  4. Add retry mechanisms with exponential backoff
  5. Implement proper error handling for authentication failures

This would provide a more reliable authentication experience and prevent session interruptions.

Would you like me to provide a detailed implementation plan for these improvements?

types.d.ts (1)

354-354: LGTM: Token handling improvements for refresh implementation.

The addition of accessTokenRaw, idTokenEncoded, and idTokenRaw properties to KindeSetupResponse aligns well with the PR objective of implementing token refresh functionality. These properties provide the necessary token formats for proper token validation and refresh operations.

Consider documenting the following token handling best practices:

  • Use *Raw properties only for token validation and refresh operations
  • Use *Encoded properties for transmission
  • Never expose raw tokens in logs or client-side storage

Also applies to: 356-357

src/session/getAccessTokenWithRefresh.ts (3)

13-19: Optimize sessionManager calls by reusing the session instance

You are calling await sessionManager(req, res) multiple times to retrieve session items. This can be optimized by storing the session instance in a variable to reduce redundant calls and improve performance.

Apply this diff to store the session instance:

+const session = await sessionManager(req, res);

 const accessToken = (await (
-  await sessionManager(req, res)
+  session
 ).getSessionItem('access_token')) as string | null;

 const refreshToken = (await (
-  await sessionManager(req, res)
+  session
 ).getSessionItem('refresh_token')) as string | null;

29-33: Simplify error handling for expired access tokens without refresh tokens

The check for !validToken && !refreshToken may be unnecessary if you've already handled the case where accessToken is missing. Ensure that your error handling logic is consistent and doesn't duplicate checks.

Consider revising the condition to streamline the error handling:

-if (!validToken && !refreshToken) {
+if (!validToken) {
   throw new Error(
     'The access token expired and a refresh token is not available. The user will need to sign in again.'
   );
 }

35-39: Clarify error message when force refresh is requested without a refresh token

The error message could be more specific. Indicate that the refresh token is missing even though a force refresh was requested.

Refine the error message as follows:

 throw new Error(
-  'A refresh token is required to refresh the access token, but none is present.'
+  'Cannot force refresh access token without a refresh token present.'
 );
src/frontend/AuthProvider.tsx (1)

184-190: Add Cleanup Function to useEffect Hook

The useEffect hook should return a cleanup function to clear the timer when the component unmounts, preventing potential memory leaks.

Apply this diff to add a cleanup function:

      useEffect((): void => {
        if (state.accessToken) return;
        (async (): Promise<void> => {
          await checkSession();
          setState((previous) => ({...previous, isLoading: false}));
        })();
+       return () => {
+         if (timer) clearTimeout(timer);
+       };
      }, [state.accessToken]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b9e01b3 and 7386d43.

⛔ Files ignored due to path filters (1)
  • tsconfig.json is excluded by !**/*.json
📒 Files selected for processing (12)
  • src/frontend/AuthProvider.jsx (0 hunks)
  • src/frontend/AuthProvider.tsx (1 hunks)
  • src/frontend/KindeBrowserClient.js (1 hunks)
  • src/frontend/index.js (0 hunks)
  • src/frontend/index.ts (1 hunks)
  • src/handlers/setup.ts (2 hunks)
  • src/index.js (1 hunks)
  • src/session/getAccessTokenWithRefresh.ts (1 hunks)
  • src/session/index.ts (1 hunks)
  • src/session/isAuthenticated.js (1 hunks)
  • src/utils/isTokenValid.ts (2 hunks)
  • types.d.ts (2 hunks)
💤 Files with no reviewable changes (2)
  • src/frontend/AuthProvider.jsx
  • src/frontend/index.js
✅ Files skipped from review due to trivial changes (2)
  • src/frontend/index.ts
  • src/index.js
🔇 Additional comments (8)
src/utils/isTokenValid.ts (1)

2-2: Verify the config import path

The import path has been updated. Let's ensure this path is correct and consistent across the codebase.

✅ Verification successful

Import path is correct and consistent

The import path '../config/index' is correctly used across multiple files in the codebase, including:

  • src/session/ files
  • src/utils/isTokenValid.ts
  • src/handlers/ files
  • src/frontend/AuthProvider.tsx
  • src/authMiddleware/authMiddleware.ts

The config module exists at src/config/index.js and all imports follow the same relative path pattern.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the config import path exists and is used consistently

# Check if the config file exists at the new location
fd --type f "index.ts" --exec sh -c 'echo "Found config at: {}"; head -n 1 {}'

# Check for any other imports of the config module
rg --type ts --type jsx "from ['\"].*config/index['\"]" --no-filename

Length of output: 377


Script:

#!/bin/bash
# Let's try a different approach to verify the config import and its usage

# First, let's find the config directory and its contents
fd config --type d
fd --type f --full-path "./src/config"

# Then check for any imports of config across the codebase with ripgrep
# Using a more flexible pattern without the jsx type
rg --type ts "from ['\"].*config['\"]"

# Also check if there are any direct references to config
rg --type ts "import.*config"

Length of output: 844

src/session/index.ts (2)

1-20: LGTM! Import statements are well-organized

The new imports for token-related factories align well with the PR's objective of implementing token refresh functionality. The imports follow a consistent pattern and are properly typed.


Line range hint 38-58: LGTM! Factory methods are well-organized

The returned object provides a comprehensive set of authentication and authorization utilities. The new methods (getAccessTokenRaw, getClaim, getIdTokenRaw, getRoles) align well with the token refresh implementation.

src/handlers/setup.ts (1)

2-11: LGTM! Type safety improvements

The addition of explicit types and the RouterClient import enhance type safety and maintainability.

src/frontend/KindeBrowserClient.js (1)

2-2: Verify import resolution across different environments

The import path './AuthProvider' without an extension might need explicit extension handling in certain bundler configurations.

✅ Verification successful

Import path is correctly configured

The import of flagDataTypeMap from './AuthProvider' is valid as:

  • AuthProvider.tsx exists in the same directory
  • flagDataTypeMap is properly exported and used within the file
  • Modern bundlers can resolve TypeScript files without explicit extensions
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if both files exist and if import would resolve correctly
fd -e ts -e js -e jsx -e tsx "AuthProvider" --exec echo "Found: {}"

Length of output: 106


Script:

#!/bin/bash
# Check if flagDataTypeMap is exported from AuthProvider.tsx
ast-grep --pattern 'export $_ flagDataTypeMap'

Length of output: 48


Script:

#!/bin/bash
# Let's try a different pattern to find flagDataTypeMap in AuthProvider.tsx
rg "flagDataTypeMap" ./src/frontend/AuthProvider.tsx -A 2

Length of output: 411

types.d.ts (1)

201-207: Breaking Change: Organization property names have been simplified.

The removal of the 'org_' prefix from organization properties is a breaking change that could affect existing implementations.

Let's verify the impact and alignment with the API:

src/session/getAccessTokenWithRefresh.ts (1)

43-45: Ensure accurate token expiration check

When checking if the token is expired, multiplying decodedToken.exp by 1000 assumes that exp is in seconds. Confirm that exp is in the correct unit (seconds since the epoch). If exp is already in milliseconds, the multiplication is unnecessary.

Run the following script to verify the unit of exp in tokens:

✅ Verification successful

Multiplication by 1000 is necessary - token expiration is in seconds

The verification confirms that the multiplication by 1000 is correct and necessary. Evidence from the codebase:

  • In src/utils/isTokenValid.ts, the token validation compares accessTokenPayload.exp directly with Math.floor(Date.now() / 1000), confirming that exp is in seconds
  • The current implementation in getAccessTokenWithRefresh.ts correctly multiplies decodedToken.exp by 1000 to convert it to milliseconds before comparing with Date.now()
  • This pattern is consistently used across the codebase, including in AuthProvider.tsx
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify if 'exp' in the JWT token is in seconds or milliseconds.

# Decode a sample token to check the 'exp' field.
# Replace 'YOUR_ACCESS_TOKEN' with a valid token.
echo 'YOUR_ACCESS_TOKEN' | cut -d '.' -f2 | base64 --decode | jq '.exp'

# Expected: 'exp' should be a Unix timestamp in seconds.

Length of output: 141


Script:

#!/bin/bash
# Let's search for any test files or documentation that might indicate the token format
rg -i "jwt.*exp|token.*exp" -A 3 -B 3

# Also search for any token decoding or validation logic
ast-grep --pattern 'decodedToken.exp'

Length of output: 3272

src/frontend/AuthProvider.tsx (1)

50-50: ⚠️ Potential issue

Props Destructuring Incomplete in KindeProvider

The KindeProvider component destructures only children from props. According to the JSDoc comment, it should also accept options.

Apply this diff to include options in the props:

- export const KindeProvider = ({children}) => {
+ export const KindeProvider = ({children, options}) => {

Likely invalid or redundant comment.

src/session/isAuthenticated.js Show resolved Hide resolved
src/utils/isTokenValid.ts Outdated Show resolved Hide resolved
src/handlers/setup.ts Show resolved Hide resolved
Comment on lines +21 to +33
const accessToken = await getAccessTokenWithRefresh(
routerClient.req,
routerClient.res
);

const accessTokenEncoded =
await routerClient.sessionManager.getSessionItem('access_token');
(await routerClient.sessionManager.getSessionItem(
'access_token'
)) as string;

const idTokenEncoded =
await routerClient.sessionManager.getSessionItem('id_token');

const accessToken = jwtDecoder<KindeAccessToken>(accessTokenEncoded);
const idTokenEncoded = (await routerClient.sessionManager.getSessionItem(
'id_token'
)) as string;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

🛠️ Refactor suggestion

Type assertions are handled safely in most places except the setup handler

The codebase shows a consistent pattern for token handling:

  • In getAccessTokenWithRefresh.ts, tokens are properly typed as string | null
  • In callback.ts, there's proper null checking with an if statement
  • However, the setup.ts handler is the only place where unsafe type assertions are used without null checks

The review comment's suggestion for adding null checks is valid and necessary for maintaining consistency with the rest of the codebase's safer type handling patterns.

🔗 Analysis chain

Consider adding null checks before type assertions

The type assertions for session items assume the values will always be strings. This could lead to runtime errors if the session items are undefined or null.

Consider adding null checks and proper error handling:

-    const accessTokenEncoded =
-      (await routerClient.sessionManager.getSessionItem(
-        'access_token'
-      )) as string;
+    const accessTokenEncoded = await routerClient.sessionManager.getSessionItem(
+      'access_token'
+    );
+    if (!accessTokenEncoded) {
+      throw new Error('Access token not found in session');
+    }

-    const idTokenEncoded = (await routerClient.sessionManager.getSessionItem(
-      'id_token'
-    )) as string;
+    const idTokenEncoded = await routerClient.sessionManager.getSessionItem(
+      'id_token'
+    );
+    if (!idTokenEncoded) {
+      throw new Error('ID token not found in session');
+    }

Let's verify the token retrieval implementation across the codebase:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for similar token retrieval patterns that might need the same safety checks
ast-grep --pattern 'await $_.sessionManager.getSessionItem($_) as string'

Length of output: 75


Script:

#!/bin/bash
# Search for all sessionManager.getSessionItem usages to analyze token retrieval patterns
rg "sessionManager\.getSessionItem" -A 2

# Also search for type assertions with 'as string' to find similar patterns
rg "as string" -A 2

Length of output: 2631

Comment on lines +53 to +55
if (!validToken) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Evaluate necessity of returning null when the token is invalid

Returning null when the token is invalid may lead to unhandled cases upstream. Consider throwing an error or handling this scenario to avoid unexpected null values.

Modify the code to handle invalid tokens explicitly:

 if (!validToken) {
-  return null;
+  throw new Error('The access token is invalid. User may need to sign in again.');
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!validToken) {
return null;
}
if (!validToken) {
throw new Error('The access token is invalid. User may need to sign in again.');
}

*/
export const KindeProvider = ({children}) => {
const setupUrl = `${config.apiPath}/setup`;
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 14, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Incorrect Timer Type in useState Hook

The setTimeout function in the browser environment returns a number, not a NodeJS.Timeout. Using NodeJS.Timeout is appropriate for Node.js but not for browser-based React applications.

Apply this diff to correct the timer type:

- const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
+ const [timer, setTimer] = useState<number | null>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
const [timer, setTimer] = useState<number | null>(null);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But typescript doesn't like it...

Copy link
Contributor

Choose a reason for hiding this comment

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

@peterphanouvong

Understood. Since TypeScript is complaining about using number, a better approach is to use ReturnType<typeof setTimeout> for the timer type. This ensures compatibility across both Node.js and browser environments.

Here's the updated code:

const [timer, setTimer] = useState<ReturnType<typeof setTimeout> | null>(null);

This should resolve the TypeScript errors.

Let me know if this works!


✏️ Learnings added
Learnt from: peterphanouvong
PR: kinde-oss/kinde-auth-nextjs#242
File: src/frontend/AuthProvider.tsx:52-52
Timestamp: 2024-11-14T01:06:30.863Z
Learning: When using `setTimeout` in React with TypeScript, prefer `ReturnType<typeof setTimeout>` as the timer type in `useState` to ensure compatibility in both Node.js and browser environments.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

src/frontend/AuthProvider.tsx Show resolved Hide resolved
Comment on lines 174 to 179
const t = setTimeout(
checkSession,
kindeData?.accessToken.exp * 1000 - Date.now()
);
setTimer(t);
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle Potential Negative Timeout Delay

The calculation for the timeout delay may result in a negative value if the access token has already expired, causing unintended behavior. You should check if the delay is positive before setting the timeout.

Apply this diff to ensure the delay is positive:

          const delay = kindeData?.accessToken.exp * 1000 - Date.now();
+         if (delay > 0) {
            const t = setTimeout(
              checkSession,
-             kindeData?.accessToken.exp * 1000 - Date.now()
+             delay
            );
            setTimer(t);
+         } else {
+           // Token has expired, handle accordingly
+           await checkSession();
+         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const t = setTimeout(
checkSession,
kindeData?.accessToken.exp * 1000 - Date.now()
);
setTimer(t);
} catch (error) {
const delay = kindeData?.accessToken.exp * 1000 - Date.now();
if (delay > 0) {
const t = setTimeout(
checkSession,
delay
);
setTimer(t);
} else {
// Token has expired, handle accordingly
await checkSession();
}
} catch (error) {

Comment on lines +159 to +164
getClaim: (claim: string, tokenKey = 'access_token') => {
const token =
tokenKey === 'access_token'
? kindeData.accessToken
: kindeData.idToken;
return token ? {name: claim, value: token[claim]} : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Accessing Claims from Encoded Tokens

The getClaim function attempts to access claims directly from the token, but accessToken and idToken are typically encoded strings (JWTs), not decoded objects.

Apply this diff to decode the tokens before accessing claims:

+ import jwt_decode from 'jwt-decode';
            getClaim: (claim: string, tokenKey = 'access_token') => {
              const token =
                tokenKey === 'access_token'
                  ? kindeData.accessTokenRaw
                  : kindeData.idTokenRaw;
+             const decodedToken = token ? jwt_decode(token) : null;
-             return token ? {name: claim, value: token[claim]} : null;
+             return decodedToken ? {name: claim, value: decodedToken[claim]} : null;
            },

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +147 to +151
getPermission: (key: string) => {
return {
isGranted: kindeData.permissions.permissions.some((p) => p === key),
orgCode: kindeData.organization.orgCode
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add Null Checks for permissions and organization

In the getPermission function, kindeData.permissions and kindeData.organization may be undefined, leading to runtime errors when accessing their properties.

Apply this diff to add null checks:

            getPermission: (key: string) => {
+             if (!kindeData.permissions || !kindeData.organization) {
+               throw new Error('Permissions or organization data is not available');
+             }
              return {
-               isGranted: kindeData.permissions.permissions.some((p) => p === key),
+               isGranted: kindeData.permissions.permissions.some((p) => p === key),
                orgCode: kindeData.organization.orgCode
              };
            },

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@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: 3

🧹 Outside diff range and nitpick comments (1)
src/api-client.js (1)

Line range hint 33-43: Add error handling for token fetch failures.

The token fetch operation lacks error handling, which could lead to unclear errors if the authentication server is unavailable or returns an error.

Add proper error handling:

-  const response = await fetch(`${config.issuerURL}/oauth2/token`, {
-    method: 'POST',
-    headers: {
-      'content-type': 'application/x-www-form-urlencoded'
-    },
-    body: new URLSearchParams({
-      grant_type: 'client_credentials',
-      client_id: config.clientID || '',
-      client_secret: config.clientSecret || '',
-      audience: config.issuerURL + '/api'
-    })
-  });
-  apiToken = (await response.json()).access_token;
+  try {
+    const response = await fetch(`${config.issuerURL}/oauth2/token`, {
+      method: 'POST',
+      headers: {
+        'content-type': 'application/x-www-form-urlencoded'
+      },
+      body: new URLSearchParams({
+        grant_type: 'client_credentials',
+        client_id: config.clientID || '',
+        client_secret: config.clientSecret || '',
+        audience: config.issuerURL + '/api'
+      })
+    });
+    
+    if (!response.ok) {
+      throw new Error(`Failed to fetch token: ${response.status} ${response.statusText}`);
+    }
+    
+    const data = await response.json();
+    if (!data.access_token) {
+      throw new Error('Token response missing access_token');
+    }
+    
+    apiToken = data.access_token;
+  } catch (error) {
+    throw new Error(`Failed to initialize Kinde API client: ${error.message}`);
+  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 7386d43 and d8e109b.

📒 Files selected for processing (4)
  • src/api-client.js (1 hunks)
  • src/authMiddleware/authMiddleware.ts (1 hunks)
  • src/frontend/AuthProvider.tsx (1 hunks)
  • src/utils/isTokenValid.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/authMiddleware/authMiddleware.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/isTokenValid.ts
🔇 Additional comments (3)
src/api-client.js (1)

Line range hint 1-91: Verify token refresh implementation across the application.

Given that this PR's objective is to implement token refresh functionality, we should verify how this management API client integrates with the broader token refresh mechanism.

src/frontend/AuthProvider.tsx (2)

147-152: ⚠️ Potential issue

Add null checks in getPermission

The function assumes permissions and organization are always available, which could lead to runtime errors.

Apply this diff:

 getPermission: (key: string) => {
+  if (!kindeData?.permissions?.permissions || !kindeData?.organization?.orgCode) {
+    return {
+      isGranted: false,
+      orgCode: null
+    };
+  }
   return {
     isGranted: kindeData.permissions.permissions.some((p) => p === key),
     orgCode: kindeData.organization.orgCode
   };
 },

Likely invalid or redundant comment.


186-192: ⚠️ Potential issue

Add cleanup in useEffect

The effect should clean up the timer when the component unmounts to prevent memory leaks and potential errors.

Apply this diff:

 useEffect((): void => {
   if (state.accessToken) return;
   (async (): Promise<void> => {
     await checkSession();
     setState((previous) => ({...previous, isLoading: false}));
   })();
+  return () => {
+    if (timer) {
+      clearTimeout(timer);
+    }
+  };
- }, [state.accessToken]);
+ }, [state.accessToken, timer]);

Likely invalid or redundant comment.

@@ -30,7 +29,7 @@ export const createKindeManagementAPIClient = async (req, res) => {

const store = await sessionManager(req, res);
store.removeSessionItem('kinde_api_access_token');

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider caching valid tokens to prevent unnecessary token requests.

The current implementation removes and fetches a new token on every API client creation, which could lead to:

  1. Unnecessary load on the authentication server
  2. Potential rate limiting issues
  3. Increased latency for API requests

Consider implementing token caching:

export const createKindeManagementAPIClient = async (req, res) => {
  let apiToken = null;
  const store = await sessionManager(req, res);
-  store.removeSessionItem('kinde_api_access_token');
+  apiToken = store.getSessionItem('kinde_api_access_token');
+  
+  if (!apiToken) {
    const response = await fetch(`${config.issuerURL}/oauth2/token`, {
      // ... existing token fetch code ...
    });
    apiToken = (await response.json()).access_token;
+    store.setSessionItem('kinde_api_access_token', apiToken);
+  }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 174 to 180
const delay = kindeData?.accessToken.exp * 1000 - Date.now();
if (delay > 0) {
const t = setTimeout(checkSession, delay);
setTimer(t);
} else {
await checkSession();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent concurrent token refresh attempts

The current implementation could lead to race conditions if multiple refresh attempts occur simultaneously. Additionally, as noted in past reviews, the timer type should be number for browser environments.

Apply this diff to improve the implementation:

+ const [isRefreshing, setIsRefreshing] = useState(false);
  // ... in checkSession function
+ if (isRefreshing) return;
+ setIsRefreshing(true);
  try {
    const delay = kindeData?.accessToken.exp * 1000 - Date.now();
    if (delay > 0) {
      const t = setTimeout(checkSession, delay);
      setTimer(t);
    } else {
      await checkSession();
    }
+  } finally {
+    setIsRefreshing(false);
  }

Also update the timer type:

- const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
+ const [timer, setTimer] = useState<number | null>(null);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +31 to +43
const tokenFetcher = async (
url: string
): Promise<KindeSetupResponse | undefined> => {
let response: Response;
try {
response = await fetch(url);
} catch {
throw new Error('Network error');
}
if (response.status == 204) return undefined;
if (response.ok) return response.json();
throw new Error('Error fetching data');
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling in tokenFetcher

  1. Missing await on response.json() as noted in past review
  2. Network error message could be more descriptive
  3. Error handling could be more specific

Apply this diff to improve the implementation:

 const tokenFetcher = async (
   url: string
 ): Promise<KindeSetupResponse | undefined> => {
   let response: Response;
   try {
     response = await fetch(url);
   } catch (error) {
-    throw new Error('Network error');
+    throw new Error(`Failed to fetch from ${url}: ${error.message}`);
   }
   if (response.status == 204) return undefined;
-  if (response.ok) return response.json();
+  if (response.ok) return await response.json();
+  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
-  throw new Error('Error fetching data');
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tokenFetcher = async (
url: string
): Promise<KindeSetupResponse | undefined> => {
let response: Response;
try {
response = await fetch(url);
} catch {
throw new Error('Network error');
}
if (response.status == 204) return undefined;
if (response.ok) return response.json();
throw new Error('Error fetching data');
};
const tokenFetcher = async (
url: string
): Promise<KindeSetupResponse | undefined> => {
let response: Response;
try {
response = await fetch(url);
} catch (error) {
throw new Error(`Failed to fetch from ${url}: ${error.message}`);
}
if (response.status == 204) return undefined;
if (response.ok) return await response.json();
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
};

Copy link
Contributor

@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: 2

🧹 Outside diff range and nitpick comments (3)
src/handlers/setup.ts (1)

Line range hint 74-109: Consider breaking down the response construction

The response construction is complex and could benefit from being broken down into smaller, more focused functions for better maintainability and testability.

Consider extracting the organization properties mapping and user organizations mapping into separate utility functions:

function mapOrganizationProperties(orgProperties: KindeTokenOrganizationProperties) {
  return {
    city: orgProperties?.kp_org_city?.v,
    industry: orgProperties?.kp_org_industry?.v,
    postcode: orgProperties?.kp_org_postcode?.v,
    state_region: orgProperties?.kp_org_state_region?.v,
    street_address: orgProperties?.kp_org_street_address?.v,
    street_address_2: orgProperties?.kp_org_street_address_2?.v
  };
}

function mapUserOrganizations(orgCodes: string[], orgNames: Array<{id: string; name: string}>) {
  return {
    orgCodes,
    orgs: orgNames?.map((org) => ({
      code: org?.id,
      name: org?.name
    }))
  };
}
src/frontend/AuthProvider.tsx (2)

64-68: Improve type safety in getFlag function parameters

The defaultValue: any parameter reduces type safety. Consider using a generic type parameter that's constrained by the flag types.

-        defaultValue: any,
+        defaultValue: string | number | boolean,
         flagType: 'b' | 'i' | 's'

116-145: Improve error handling in flag getters

The current implementation silently fails in production and has duplicated error handling code. Consider:

  1. Extracting common error handling logic
  2. Providing a way to handle errors in production
+ const handleFlagError = (err: Error, defaultValue: any) => {
+   if (config.isDebugMode) {
+     console.error(err);
+   }
+   return defaultValue;
+ };

  getBooleanFlag: (code: string, defaultValue: boolean) => {
    try {
      const flag = getFlag(code, defaultValue, 'b');
      return flag.value;
    } catch (err) {
-     if (config.isDebugMode) {
-       console.error(err);
-     }
+     return handleFlagError(err, defaultValue);
    }
  },
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between d8e109b and e6227c0.

📒 Files selected for processing (2)
  • src/frontend/AuthProvider.tsx (1 hunks)
  • src/handlers/setup.ts (2 hunks)
🔇 Additional comments (3)
src/handlers/setup.ts (2)

27-33: ⚠️ Potential issue

Add null checks for session items

The type assertions for session items are still unsafe. While the code has been updated to use getAccessTokenWithRefresh, the session item retrievals need proper null checks.

The previous review comment's suggestion for adding null checks is still valid. Please refer to the existing review comment for the implementation details.


37-72: ⚠️ Potential issue

Add validation for claim values

Multiple claims are retrieved using type assertions without proper validation. This pattern is repeated across permissions, organization, featureFlags, and other claims.

The previous review comment's suggestion for adding a validation utility is still valid. Please refer to the existing review comment for the implementation details.

src/frontend/AuthProvider.tsx (1)

1-17: LGTM! Clean imports and type definitions

The imports are well-organized and the flag data type mapping is implemented effectively.

Comment on lines +186 to +192
useEffect((): void => {
if (state.accessToken) return;
(async (): Promise<void> => {
await checkSession();
setState((previous) => ({...previous, isLoading: false}));
})();
}, [state.accessToken]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add cleanup handler to prevent memory leaks

The useEffect hook should include a cleanup function to handle component unmounting during the authentication check.

 useEffect((): void => {
   if (state.accessToken) return;
+  let mounted = true;
   (async (): Promise<void> => {
     await checkSession();
+    if (mounted) {
       setState((previous) => ({...previous, isLoading: false}));
+    }
   })();
+  return () => {
+    mounted = false;
+  };
 }, [state.accessToken]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect((): void => {
if (state.accessToken) return;
(async (): Promise<void> => {
await checkSession();
setState((previous) => ({...previous, isLoading: false}));
})();
}, [state.accessToken]);
useEffect((): void => {
if (state.accessToken) return;
let mounted = true;
(async (): Promise<void> => {
await checkSession();
if (mounted) {
setState((previous) => ({...previous, isLoading: false}));
}
})();
return () => {
mounted = false;
};
}, [state.accessToken]);

Comment on lines +174 to +180
const tokenExpiresIn = kindeData?.accessToken.exp * 1000 - Date.now();
if (tokenExpiresIn > 0) {
const t = setTimeout(checkSession, tokenExpiresIn);
setTimer(t);
} else {
await checkSession();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add request deduplication for token refresh

The current implementation could trigger multiple concurrent refresh attempts if checkSession is called multiple times near the token expiration. Consider implementing a request deduplication mechanism.

+ const [isRefreshing, setIsRefreshing] = useState(false);

  const checkSession = useCallback(async (): Promise<void> => {
    try {
+     if (isRefreshing) return;
+     setIsRefreshing(true);
      // ... existing code ...
    } finally {
+     setIsRefreshing(false);
    }
  }, [setupUrl]);

Committable suggestion skipped: line range outside the PR's diff.

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.

Feature request: Refresh Active Sessions Automatically
3 participants