Skip to content

fix: refresh access tokens flow#3105

Closed
mcstepp wants to merge 2 commits intomainfrom
ENG-1713-access-tokens
Closed

fix: refresh access tokens flow#3105
mcstepp wants to merge 2 commits intomainfrom
ENG-1713-access-tokens

Conversation

@mcstepp
Copy link
Collaborator

@mcstepp mcstepp commented Apr 11, 2025

What does this PR do?

Appropriately handles access tokens from OAuth and appropriately authenticating with refresh tokens to get new access tokens.

Fixes ENG-1713

If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Login in using OAuth
  • Access token is set to 5 minutes in WorkOS dashboard
  • Be on the dashboard for longer than 5 minutes
  • Expect no sign out due to invalid access token or refresh token already used.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

Summary by CodeRabbit

  • New Features

    • Introduced an authentication provider to enforce authentication across the dashboard layout.
    • Added a session endpoint for checking user authentication status.
    • Improved authentication context, enabling session refresh and automatic periodic refresh for authenticated users.
  • Enhancements

    • Improved session, access, and refresh token management for more robust authentication handling.
    • Enhanced concurrency control during token refresh to prevent multiple simultaneous refresh attempts.
    • Extended session data to include user roles.
    • Added comprehensive authentication provider interface with expanded methods and middleware support.
    • Strengthened cookie security by enforcing strict same-site policies.
  • Bug Fixes

    • Improved error handling and status codes for authentication failures.
  • Refactor

    • Updated naming conventions and reorganized authentication types for clarity and maintainability.
  • Style

    • Minor whitespace cleanup for code consistency.

@linear
Copy link

linear bot commented Apr 11, 2025

@changeset-bot
Copy link

changeset-bot bot commented Apr 11, 2025

⚠️ No Changeset found

Latest commit: 39e6d6b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Apr 11, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
dashboard ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 23, 2025 6:20pm
engineering ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 23, 2025 6:20pm
play 🛑 Canceled (Inspect) Apr 23, 2025 6:20pm
www ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 23, 2025 6:20pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 11, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This set of changes refactors authentication and session management across the dashboard application. Key updates include replacing a global mutex with a per-refresh-token mutex to serialize concurrent refresh token requests, explicit handling and propagation of access and refresh tokens alongside session tokens, and a new React authentication provider for client-side session state management. The session refresh endpoint and supporting libraries now use the refresh token for access token renewal, setting multiple secure cookies with strict same-site policies. Type definitions are updated for clearer distinction between valid and invalid sessions, and additional user role information is incorporated throughout the authentication flow. The layout is wrapped in an authentication provider to enforce user authentication at the UI level.

Changes

File(s) Change Summary
apps/dashboard/app/(app)/api/auth/refresh/route.ts Refactored the session refresh endpoint to retrieve the refresh token from headers or cookies, implemented a per-refresh-token mutex using a Map to serialize concurrent refresh attempts, updated to call auth.refreshAccessToken returning session, access, and refresh tokens, and set three secure cookies with HttpOnly, Secure, and SameSite=Strict flags and appropriate max-age. Enhanced error handling distinguishes missing tokens and refresh failures with proper status codes. The mutex entry is cleared after refresh completion.
apps/dashboard/app/(app)/api/auth/session/route.ts Added a new GET endpoint that uses getAuth to retrieve the authenticated user's session info, returning 401 if unauthenticated and 200 if valid. Includes error logging and exports a dynamic constant.
apps/dashboard/app/(app)/layout.tsx Wrapped the entire layout JSX in an AuthProvider component with requireAuth set to true to enforce authentication before rendering any child components.
apps/dashboard/app/auth/actions.ts, apps/dashboard/lib/trpc/routers/user/switchOrg.ts Renamed the destructured token variable from newToken to sessionToken in organization switching logic, updating the token passed to session cookie setting accordingly. No other logic changes.
apps/dashboard/app/auth/sso-callback/[[...sso-callback]]/route.ts Removed extraneous blank lines in the GET function without altering logic or control flow.
apps/dashboard/lib/auth.ts Changed import alias from noCacheGetAuth to baseGetAuth and updated getAuth to call baseGetAuth. Extended the GetAuthResult type by adding a role property of type `string
apps/dashboard/lib/auth/base-provider.ts, apps/dashboard/lib/auth/workos.ts Renamed abstract method refreshSession to refreshAccessToken with parameter changed from session token to refresh token. Updated implementations to explicitly handle and return access and refresh tokens alongside session tokens and role information. Added extensive new abstract methods for email sign-in, OAuth, organization management, user and membership management, invitation handling, and middleware creation. Added protected utility methods for error handling, response creation, and public path checking. Updated cookie sameSite attributes from "lax" to "strict".
apps/dashboard/lib/auth/cookies.ts Changed cookie sameSite attribute from "lax" to "strict" in updateCookie and SetSessionCookie functions. Updated JSDoc comment to generalize the response object parameter.
apps/dashboard/lib/auth/get-auth.ts Refactored authentication logic to add a cached session validation function with error handling, introduced a per-refresh-token mutex map to serialize refresh token requests, and updated getAuth to retrieve session and refresh tokens from cookies, validate or refresh sessions as needed, and return user data including role. Improved error handling and unified return structure. Added utility functions to clear mutex entries and get ongoing refresh count.
apps/dashboard/lib/auth/types.ts Added constants for access and refresh token names and max age. Refactored SessionValidationResult into a union of ValidSessionResult and InvalidSessionResult for clearer typing. Renamed newToken to sessionToken in SessionRefreshResult and added optional accessToken and refreshToken fields. Extended SessionData interface with an optional role field.
apps/dashboard/providers/AuthProvider.tsx Introduced a new React context AuthContext and AuthProvider component to manage authentication state, including isAuthenticated, isLoading, and a refreshSession function. Implements session validation via /api/auth/session and session refresh via /api/auth/refresh. Supports redirection to sign-in if authentication is required but absent. Sets up periodic session refresh interval based on access token max age. Conditionally renders children only if authenticated when required.
apps/dashboard/app/new/create-workspace.tsx Changed the sameSite attribute of the session cookie set during organization switch to "strict" for enhanced security. No other logic changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthProvider (React)
    participant API (/api/auth/session)
    participant API (/api/auth/refresh)
    participant Auth Library

    Client->>AuthProvider (React): Mounts or periodic check
    AuthProvider (React)->>API (/api/auth/session): GET /api/auth/session
    API (/api/auth/session)->>Auth Library: getAuth()
    Auth Library-->>API (/api/auth/session): Session info or 401
    API (/api/auth/session)-->>AuthProvider (React): 200 OK or 401 Unauthorized
    alt Not authenticated and requireAuth
        AuthProvider (React)->>Client: Redirect to sign-in
    else Authenticated
        AuthProvider (React)->>Client: Render children
    end

    loop Periodic refresh
        AuthProvider (React)->>API (/api/auth/refresh): POST /api/auth/refresh
        API (/api/auth/refresh)->>Auth Library: refreshAccessToken(refreshToken)
        Auth Library-->>API (/api/auth/refresh): New tokens/session
        API (/api/auth/refresh)-->>AuthProvider (React): 200 OK with new cookies or 401/500
        alt Refresh failed and requireAuth
            AuthProvider (React)->>Client: Redirect to sign-in
        end
    end
Loading

Possibly related PRs

Suggested labels

Bug

Suggested reviewers

  • perkinsjr
  • ogzhanolguncu
  • chronark

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
🪧 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 generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this 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.

@github-actions
Copy link
Contributor

github-actions bot commented Apr 11, 2025

Thank you for following the naming conventions for pull request titles! 🙏

@mcstepp mcstepp changed the title Eng 1713 access tokens fix: refresh access tokens flow Apr 11, 2025
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: 0

🔭 Outside diff range comments (1)
apps/dashboard/lib/auth/workos.ts (1)

768-813: 🛠️ Refactor suggestion

Duplicate cookie-setting logic.

You are setting session, access, and refresh tokens in multiple places (e.g., completeOrgSelection and completeOAuthSignIn). Consider extracting cookie-setting into a shared helper function to reduce duplication and improve maintainability.

🧹 Nitpick comments (14)
apps/dashboard/lib/auth/cookies.ts (1)

86-86: Comment is slightly inconsistent with function signature

The comment has been updated to refer to a "Response object", but the function signature still explicitly uses and returns a NextResponse type. Consider either:

  1. Keeping the comment as "NextResponse object" to match the signature, or
  2. Modifying the function to work with the more general Response type if that's the intention
apps/dashboard/app/(app)/api/auth/session/route.ts (1)

1-18: Good implementation of session verification endpoint

This new endpoint provides a clean way to verify user authentication status. It utilizes the enhanced getAuth function with caching and mutex protection for handling concurrent requests efficiently. The error handling is appropriate with proper logging.

Consider adding more context to the error log by including the specific error message:

- console.error("Error checking session:", error);
+ console.error("Error checking session:", error instanceof Error ? error.message : error);
apps/dashboard/app/(app)/api/auth/refresh/route.ts (4)

10-12: Consider multi-instance concurrency.

Using a single global mutex (refreshInProgress and refreshPromise) only works within a single runtime instance. In a distributed environment (multiple replicas or serverless), concurrent refresh attempts might still occur. Consider a more robust approach (e.g., shared cache or database-locked approach) if you anticipate scaling horizontally.


16-18: Check the fallback priority of refresh token retrieval.

You are prioritizing the header value "x-refresh-token" over the cookie value. Ensure this aligns with security best practices (e.g., only trust the cookie if the header is not provided or vice versa) and document the expected usage strategy.


53-54: Clarify the comment for max-age calculation.

Though this logic calculates the difference for a 7-day window, the comment references “// 7 days in seconds” which may be confusing if expiresAt is dynamically determined. Consider clarifying in code or comments that expiresAt is set to 7 days from now elsewhere and that you’re computing the number of remaining seconds until that time.


76-80: Refine error handling for invalid refresh attempts.

When refreshAccessToken fails, you respond with a generic 401. Consider providing more granular messages or codes, especially if the user experience could benefit from knowing whether the refresh token is expired, revoked, or malformed.

apps/dashboard/lib/auth/workos.ts (2)

95-129: Refactor or remove the "butt" error log message.

At line 126, the error log "console.error('butt', error);" may be unintentional or unprofessional. Consider replacing it with something more descriptive like "console.error('Refresh error:', error);" for clarity and consistency.

- console.error("butt", error);
+ console.error("Refresh error:", error);

119-123: Consider retrieving user role during refresh.

You’re returning role: null by default after authenticateWithRefreshToken. If user roles are critical to your logic, explore whether the library offers a way to fetch the user’s role or re-check membership/permissions post-refresh to keep the session data accurate.

apps/dashboard/lib/auth/get-auth.ts (3)

13-35: Validate session caching approach carefully.

validateSessionCached may lead to stale session data if changes occur outside of its lifetime. Consider introducing an eviction or expiration policy in case the session is revoked or updated. Alternatively, incorporate a more robust invalidation mechanism if real-time updates matter.


37-45: Expand concurrency handling approach.

Similar to the refresh endpoint, this local in-memory mutex won’t prevent concurrent refreshes across multiple server instances. Evaluate whether you need a distributed lock or a short expiration-based approach in high-availability scenarios.


95-106: Gracefully handle missing tokens in partial scenarios.

Currently, if either sessionToken or refreshToken is missing, you immediately return null user data. If your flow sometimes allows usage without refresh tokens (e.g., ephemeral sessions), consider adjusting logic or adding a fallback/renew step if that scenario is valid.

apps/dashboard/providers/AuthProvider.tsx (3)

35-57: Verify error handling for authentication failures.

The checkAuth function implements proper error handling and loading state management. However, consider whether logging the full error object to the console is appropriate in a production environment, as it might expose sensitive information.

Consider replacing the console.error with more structured error logging:

-      console.error("Auth check failed:", error);
+      console.error("Auth check failed:", error instanceof Error ? error.message : "Unknown error");

60-82: Consider implementing exponential backoff for failed refresh attempts.

The refreshSession function handles errors appropriately, but it doesn't implement any retry mechanism. For network issues, implementing exponential backoff could improve resilience.

Consider adding a simple retry mechanism with backoff:

  const refreshSession = useCallback(async () => {
+   const maxRetries = 3;
+   let retryCount = 0;
+   
+   const attemptRefresh = async (): Promise<boolean> => {
      try {
        const response = await fetch("/api/auth/refresh", {
          method: "POST",
          credentials: "include",
        });

        setIsAuthenticated(response.ok);

        if (!response.ok && requireAuth) {
          router.push(redirectTo);
        }

        return response.ok;
      } catch (error) {
-       console.error("Session refresh failed:", error);
+       console.error("Session refresh failed:", error instanceof Error ? error.message : "Unknown error");
        setIsAuthenticated(false);
+       
+       if (retryCount < maxRetries) {
+         retryCount++;
+         const backoffTime = Math.pow(2, retryCount) * 1000; // Exponential backoff
+         await new Promise(resolve => setTimeout(resolve, backoffTime));
+         return attemptRefresh();
+       }
+       
        if (requireAuth) {
          router.push(redirectTo);
        }
        return false;
      }
+   };
+   
+   return attemptRefresh();
  }, [requireAuth, router, redirectTo]);

104-114: Consider adding a loading state for the UI.

The current implementation doesn't render children during the loading state for routes that require authentication. Consider showing a loading indicator instead of a blank screen.

  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        isLoading,
        refreshSession,
      }}
    >
-     {!requireAuth || (requireAuth && isAuthenticated) ? children : null}
+     {!requireAuth ? children : isLoading ? <LoadingSpinner /> : isAuthenticated ? children : null}
    </AuthContext.Provider>
  );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36becb3 and cd903c3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • apps/dashboard/app/(app)/api/auth/refresh/route.ts (1 hunks)
  • apps/dashboard/app/(app)/api/auth/session/route.ts (1 hunks)
  • apps/dashboard/app/(app)/layout.tsx (2 hunks)
  • apps/dashboard/app/auth/actions.ts (1 hunks)
  • apps/dashboard/app/auth/sso-callback/[[...sso-callback]]/route.ts (0 hunks)
  • apps/dashboard/lib/auth.ts (3 hunks)
  • apps/dashboard/lib/auth/base-provider.ts (1 hunks)
  • apps/dashboard/lib/auth/cookies.ts (1 hunks)
  • apps/dashboard/lib/auth/get-auth.ts (1 hunks)
  • apps/dashboard/lib/auth/types.ts (2 hunks)
  • apps/dashboard/lib/auth/workos.ts (8 hunks)
  • apps/dashboard/lib/trpc/routers/user/switchOrg.ts (1 hunks)
  • apps/dashboard/package.json (1 hunks)
  • apps/dashboard/providers/AuthProvider.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/dashboard/app/auth/sso-callback/[[...sso-callback]]/route.ts
🧰 Additional context used
🧬 Code Graph Analysis (6)
apps/dashboard/app/auth/actions.ts (1)
apps/dashboard/lib/auth/cookies.ts (1)
  • SetSessionCookie (103-120)
apps/dashboard/lib/auth/base-provider.ts (1)
apps/dashboard/lib/auth/types.ts (1)
  • SessionRefreshResult (123-129)
apps/dashboard/app/(app)/api/auth/refresh/route.ts (2)
apps/dashboard/lib/auth/cookies.ts (1)
  • getCookie (24-27)
apps/dashboard/lib/auth/types.ts (4)
  • UNKEY_REFRESH_TOKEN (7-7)
  • UNKEY_SESSION_COOKIE (4-4)
  • UNKEY_ACCESS_TOKEN (5-5)
  • UNKEY_ACCESS_MAX_AGE (6-6)
apps/dashboard/app/(app)/layout.tsx (4)
apps/dashboard/providers/AuthProvider.tsx (1)
  • AuthProvider (21-115)
apps/dashboard/components/ui/sidebar.tsx (1)
  • SidebarProvider (600-600)
apps/dashboard/components/navigation/sidebar/app-sidebar/index.tsx (1)
  • AppSidebar (29-130)
apps/dashboard/components/navigation/sidebar/sidebar-mobile.tsx (1)
  • SidebarMobile (9-28)
apps/dashboard/providers/AuthProvider.tsx (4)
apps/dashboard/lib/trpc/context.ts (1)
  • createContext (7-31)
apps/dashboard/lib/trpc/routers/index.ts (1)
  • router (79-206)
apps/dashboard/lib/auth/utils.ts (1)
  • requireAuth (10-16)
apps/dashboard/lib/auth/types.ts (1)
  • UNKEY_ACCESS_MAX_AGE (6-6)
apps/dashboard/lib/auth/get-auth.ts (3)
apps/dashboard/lib/auth.ts (1)
  • getAuth (30-37)
apps/dashboard/lib/auth/cookies.ts (1)
  • getCookie (24-27)
apps/dashboard/lib/auth/types.ts (2)
  • UNKEY_SESSION_COOKIE (4-4)
  • UNKEY_REFRESH_TOKEN (7-7)
⏰ Context from checks skipped due to timeout of 90000ms (12)
  • GitHub Check: Test Packages / Test ./packages/cache
  • GitHub Check: Test Packages / Test ./packages/nextjs
  • GitHub Check: Test Go API Local / Test (Shard 2/8)
  • GitHub Check: Test Packages / Test ./packages/rbac
  • GitHub Check: Test Go API Local / Test (Shard 6/8)
  • GitHub Check: Test Packages / Test ./internal/hash
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
  • GitHub Check: Test Agent Local / test_agent_local
  • GitHub Check: Docs
  • GitHub Check: autofix
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (19)
apps/dashboard/package.json (1)

72-72:

Details

✅ Verification successful

Version update aligns with authentication improvements

The update to WorkOS node SDK from ^7.35.1 to ^7.46.0 is appropriate as it likely contains improvements related to authentication and token handling that support the refresh token flow being implemented in this PR.


🌐 Web query:

What features or changes were added in @workos-inc/node between versions 7.35.1 and 7.46.0 related to authentication or token management?

💡 Result:

Between versions 7.35.1 and 7.46.0 of @workos-inc/node, several authentication and token management features/changes were introduced:

Authentication Enhancements

  • Passkey support: Added Passkey to the AuthenticationMethod union type, enabling passwordless authentication flows[1][3].
  • MFA TOTP authentication: Added support for time-based one-time passwords during MFA challenges, including error handling for pending authentication tokens[8].
  • Organization domain verification: Made verificationToken optional on OrganizationDomain to align with API behavior[1][3].

Token Management Improvements

  • Widget tokens: Introduced server-side token generation for long-lived user management widgets to avoid frontend token expiration issues[2][6].
  • Session refresh: Updated Session#refresh() to return all JWT claims instead of partial data[3][5].
  • OAuth token handling:
    • Added oauthTokens property to AuthenticationResponse for expanded OAuth data[1][3].
    • Added PKCE code challenge support with codeVerifier parameter in authentication flows[4][8].

Security Features

  • Stable auth tokens: Implemented widget token system allowing 1-hour stable tokens without changing global session durations[2][6].
  • Idempotency keys: Added support for Idempotency-Key headers in API requests to prevent duplicate operations[8].
  • SCIM role assignment: Enabled role updates via identity provider synchronization in AuthKit[5][10].

API Changes

  • Added permissions field to organization roles and events APIs[3][5].
  • Exposed warrant_token attribute from FGA check endpoints[3][8].
  • Made scopes field optional in authentication requests[3][8].

These changes enhance authentication flows, provide better token lifecycle management, and improve integration capabilities with enterprise security systems. For implementation details, refer to the WorkOS API Reference[8] and RBAC guide[10].

Citations:


Authentication and Token Management Upgrade Confirmed

The update from version ^7.35.1 to ^7.46.0 of the WorkOS Node SDK now brings several significant improvements that benefit our refresh token flow and overall authentication handling:

  • Improved OAuth Support: The new version introduces expanded OAuth token handling, including the addition of the oauthTokens property and enhanced PKCE code challenge support.
  • Enhanced Session Refresh: The updated Session#refresh() method now returns all JWT claims, providing more comprehensive token details.
  • Authentication Enhancements: Additional improvements like passkey support and MFA TOTP authentication further strengthen our authentication processes.

Overall, this dependency bump is well-aligned with the PR’s goals of improving authentication and token management.

apps/dashboard/lib/trpc/routers/user/switchOrg.ts (1)

10-10: Improved token naming for better clarity and consistency

Renaming from newToken to sessionToken improves code clarity by explicitly indicating this is a session token. This naming change helps maintain consistency with other authentication-related code in the application.

Also applies to: 13-13

apps/dashboard/app/auth/actions.ts (1)

188-190: Improved naming consistency for tokens

The variable renaming from newToken to sessionToken improves clarity and aligns with the token naming convention used throughout the codebase, making the code more consistent and easier to maintain.

apps/dashboard/app/(app)/layout.tsx (2)

6-6: Appropriate import for the new AuthProvider

The AuthProvider import is correctly added to support the layout-level authentication requirements.


33-85: Good implementation of layout-level authentication

Wrapping the entire layout with AuthProvider and setting requireAuth={true} properly enforces authentication at the layout level. This is an excellent architectural decision that centralizes authentication logic and ensures all routes within this layout are protected.

apps/dashboard/lib/auth.ts (3)

1-1: Improved import naming for clarity

Renaming the import from noCacheGetAuth to baseGetAuth better reflects the function's purpose as the base implementation for the cached getAuth function.


10-10: Enhanced authentication with role information

Adding the role property to the GetAuthResult type properly extends the authentication system to support role-based authorization. This is a forward-thinking change that maintains backward compatibility while enabling more granular access control.


31-31: Consistent use of renamed import

The code correctly uses the renamed baseGetAuth function, maintaining consistency with the import change.

apps/dashboard/lib/auth/base-provider.ts (1)

31-31: Improved token handling with OAuth best practices

The method signature change from refreshSession(sessionToken) to refreshAccessToken(currentRefreshToken) reflects a shift to a more robust OAuth-style token management approach. This change:

  1. Separates concerns between access and refresh tokens
  2. Better aligns with OAuth 2.0 patterns
  3. Improves the application's ability to handle token expiration gracefully

This is a significant improvement to the authentication system that enhances security and user experience.

apps/dashboard/lib/auth/workos.ts (1)

73-78: Ensure role retrieval is feasible.

You’re assigning role: authResult.role ?? null in validateSession. Confirm that the role can be consistently retrieved for all valid sessions. If some of your flows never assign a role, ensure calling code gracefully handles null.

apps/dashboard/providers/AuthProvider.tsx (5)

1-5: Well-structured imports and client-side directive.

The "use client" directive is correctly placed at the top, and all necessary imports are present. The combination of Next.js navigation and React hooks will support client-side authentication management.


8-19: Clear context definition with appropriate defaults.

The AuthContext is well-typed with the necessary properties for authentication state management. Default values are sensibly set with isAuthenticated as false and isLoading as true.


21-33: Good component API design with sensible defaults.

The AuthProvider component has a clean API with optional parameters and reasonable defaults. The state initialization is appropriate for the authentication flow.


85-97: Good practice refreshing before token expiration.

Setting up a refresh interval that triggers before the token expires (with a 6-second buffer) is a good practice to ensure continuous authentication. The cleanup on component unmount prevents memory leaks.


88-90:

Details

❓ Verification inconclusive

Verify the refresh buffer is sufficient.

The current refresh is set to occur 6 seconds before token expiration. Ensure this buffer is sufficient to account for network latency and potential server processing time.


🏁 Script executed:

#!/bin/bash
# Check for network latency considerations in other parts of the codebase
grep -r "setInterval.*refresh" --include="*.ts" --include="*.tsx" .

Length of output: 195


Action: Reassess the 6-Second Refresh Buffer

The token refresh in apps/dashboard/providers/AuthProvider.tsx is set to trigger 6 seconds before token expiration (UNKEY_ACCESS_MAX_AGE - 6000). While this is the only instance we found in the codebase, please verify that a 6-second window reliably accommodates both expected network latencies and potential server processing delays. If your deployment environment occasionally exhibits higher latency, consider either testing this buffer under realistic conditions or making the value configurable to better adapt to varying operational scenarios.

apps/dashboard/lib/auth/types.ts (4)

5-7: Good token naming conventions and appropriate expiration time.

The constants are well-named and follow a consistent pattern. Setting the access token expiration to 5 minutes aligns with security best practices by limiting the window of opportunity if a token is compromised.


102-121: Well-structured session validation types.

The restructured session validation types provide clearer type safety and better support for type guards. This is a good improvement that makes the code more maintainable and less error-prone.


123-129: Improved token management in SessionRefreshResult.

Renaming newToken to sessionToken and adding optional fields for access and refresh tokens enhances clarity and aligns with the separation of concerns in token management.


134-134: Good addition of role information for RBAC support.

Adding the optional role field to SessionData enables role-based access control, which is an important security feature for controlling user permissions.

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: 0

🧹 Nitpick comments (2)
apps/dashboard/app/(app)/api/auth/session/route.ts (2)

6-6: Consider removing or rewording comment.

The comment "Use your robust cached and mutex-protected getAuth function" reads like a development note rather than documentation. Consider rewording to something more objective like "Using cached and mutex-protected authentication".

-    // Use your robust cached and mutex-protected getAuth function
+    // Using cached and mutex-protected authentication

7-7: Consider adding type annotation for clarity.

Adding a type annotation for the destructured result from getAuth() would improve code readability and maintainability.

-    const { userId } = await getAuth();
+    const { userId }: { userId: string | null } = await getAuth();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd903c3 and a8011f1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/dashboard/app/(app)/api/auth/session/route.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/dashboard/app/(app)/api/auth/session/route.ts (2)
apps/dashboard/lib/auth/get-auth.ts (1)
  • getAuth (96-142)
apps/dashboard/lib/auth.ts (1)
  • getAuth (30-37)
⏰ Context from checks skipped due to timeout of 90000ms (11)
  • GitHub Check: Test Go API Local / Test (Shard 1/8)
  • GitHub Check: Test Packages / Test ./packages/nextjs
  • GitHub Check: Test Packages / Test ./internal/clickhouse
  • GitHub Check: Test Packages / Test ./internal/encryption
  • GitHub Check: Test Packages / Test ./internal/resend
  • GitHub Check: Test Packages / Test ./internal/billing
  • GitHub Check: Build / Build
  • GitHub Check: Test Agent Local / test_agent_local
  • GitHub Check: Test API / API Test Local
  • GitHub Check: autofix
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
apps/dashboard/app/(app)/api/auth/session/route.ts (2)

3-3: Good use of dynamic configuration for authentication endpoint.

Setting dynamic = 'force-dynamic' ensures this authentication endpoint is never cached, which is appropriate for session verification that should always reflect the current state.


4-19: Well-structured session verification endpoint.

This implementation correctly uses the updated getAuth function that now handles refresh tokens with mutex protection. The error handling is appropriate, with clear status codes (401 for unauthenticated users and errors, 200 for authenticated users) and error logging.

The function aligns well with the PR objective of fixing the refresh access tokens flow by providing a reliable endpoint to verify authentication status.

Copy link
Contributor

@ogzhanolguncu ogzhanolguncu left a comment

Choose a reason for hiding this comment

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

Can we get rid of comments if we don't plan to use them any time soon?

Copy link
Collaborator

what comments are you talking about?

Copy link
Contributor

Mostly apps/dashboard/lib/auth/workos.ts

Copy link
Contributor

ogzhanolguncu commented Apr 14, 2025

I tend to remove comments if they are "not" flagged with TODO, INFO or something

Copy link
Member

@perkinsjr perkinsjr left a comment

Choose a reason for hiding this comment

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

See comments, ask questions as needed.

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 comments (1)
apps/dashboard/lib/auth/base-provider.ts (1)

359-366: 🛠️ Refactor suggestion

Sign‑out redirect only clears the session cookie

redirectToLogin deletes config.cookieName but leaves UNKEY_ACCESS_TOKEN and UNKEY_REFRESH_TOKEN intact, which could:

  1. Trigger another automatic refresh attempt on the next request, causing a redirect loop.
  2. Expose stale refresh tokens if the redirect target is a different domain.

Consider deleting all auth‑related cookies, e.g.:

 response.cookies.delete(config.cookieName);
+response.cookies.delete("unkey-access-token");
+response.cookies.delete("unkey-refresh-token");

(or better, reuse the constants from types.ts).

🧹 Nitpick comments (3)
apps/dashboard/lib/auth/get-auth.ts (1)

59-66: Missing timeout / abort‑signal for internal refresh request

The POST to /api/auth/refresh can in theory hang forever (network hiccup, cold‑start, bug in the route).
That stalls every request waiting on this promise, defeating the mutex and potentially exhausting lambda concurrency.

Add an AbortController with a conservative timeout (e.g. 5 s) and surface a retriable error:

+const ctrl = new AbortController();
+const id = setTimeout(() => ctrl.abort(), 5000);
 const refreshResult = await fetch(`${baseUrl}/api/auth/refresh`, {
   method: "POST",
   credentials: "include",
   headers: { "x-refresh-token": refreshToken },
+  signal: ctrl.signal,
 });
+clearTimeout(id);

This keeps the caller responsive and allows graceful degradation.

apps/dashboard/app/(app)/api/auth/refresh/route.ts (1)

50-67: Cookies are set, but response is cacheable by default

Browsers / intermediaries might cache application/json responses when no explicit headers are present.
Because this payload contains sensitive session data, add Cache-Control: no-store (and optionally Pragma: no-cache) to be safe:

 const response = Response.json({ success: true, session });
+response.headers.set("Cache-Control", "no-store");

This ensures nothing persists the response inadvertently.

apps/dashboard/lib/auth/base-provider.ts (1)

347-350: startsWith may over‑match public paths

/public will match /publicity.
If you expect exact path prefixes ending with /, document or tighten the check:

const isPublic = publicPaths.some((path) =>
-  pathname.startsWith(path)
+  pathname === path || pathname.startsWith(path.endsWith("/") ? path : `${path}/`)
);

Minor, but prevents accidental exposure of private routes whose slug begins with a public one.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a532cd1 and 4c10c84.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • apps/dashboard/app/(app)/api/auth/refresh/route.ts (1 hunks)
  • apps/dashboard/app/(app)/api/auth/session/route.ts (1 hunks)
  • apps/dashboard/app/(app)/layout.tsx (2 hunks)
  • apps/dashboard/app/auth/actions.ts (1 hunks)
  • apps/dashboard/app/auth/sso-callback/[[...sso-callback]]/route.ts (0 hunks)
  • apps/dashboard/app/new/create-workspace.tsx (1 hunks)
  • apps/dashboard/lib/auth.ts (3 hunks)
  • apps/dashboard/lib/auth/base-provider.ts (3 hunks)
  • apps/dashboard/lib/auth/cookies.ts (3 hunks)
  • apps/dashboard/lib/auth/get-auth.ts (1 hunks)
  • apps/dashboard/lib/auth/types.ts (2 hunks)
  • apps/dashboard/lib/auth/workos.ts (14 hunks)
  • apps/dashboard/lib/trpc/routers/user/switchOrg.ts (1 hunks)
  • apps/dashboard/providers/AuthProvider.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/dashboard/app/auth/sso-callback/[[...sso-callback]]/route.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • apps/dashboard/app/new/create-workspace.tsx
  • apps/dashboard/app/auth/actions.ts
  • apps/dashboard/app/(app)/api/auth/session/route.ts
  • apps/dashboard/lib/auth.ts
  • apps/dashboard/lib/auth/cookies.ts
  • apps/dashboard/providers/AuthProvider.tsx
  • apps/dashboard/lib/auth/types.ts
  • apps/dashboard/app/(app)/layout.tsx
  • apps/dashboard/lib/trpc/routers/user/switchOrg.ts
  • apps/dashboard/lib/auth/workos.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/dashboard/lib/auth/get-auth.ts (3)
apps/dashboard/lib/auth.ts (1)
  • getAuth (30-37)
apps/dashboard/lib/auth/cookies.ts (1)
  • getCookie (24-27)
apps/dashboard/lib/auth/types.ts (2)
  • UNKEY_SESSION_COOKIE (4-4)
  • UNKEY_REFRESH_TOKEN (7-7)
apps/dashboard/lib/auth/base-provider.ts (1)
apps/dashboard/lib/auth/types.ts (17)
  • SessionValidationResult (121-121)
  • SessionRefreshResult (123-129)
  • EmailAuthResult (81-81)
  • VerificationResult (82-85)
  • UserData (177-181)
  • SignInViaOAuthOptions (140-143)
  • OAuthResult (86-90)
  • User (12-21)
  • UpdateOrgParams (161-164)
  • Organization (23-28)
  • MembershipListResponse (98-98)
  • UpdateMembershipParams (166-169)
  • Membership (30-38)
  • OrgInviteParams (171-175)
  • Invitation (146-158)
  • InvitationListResponse (99-99)
  • StateChangeResponse (46-48)
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Test Packages / Test ./packages/hono
  • GitHub Check: Test Packages / Test ./internal/clickhouse
  • GitHub Check: Test Packages / Test ./internal/encryption
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Agent Local / test_agent_local
  • GitHub Check: Build / Build
  • GitHub Check: Docs
  • GitHub Check: autofix
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
apps/dashboard/app/(app)/api/auth/refresh/route.ts (1)

20-27: Early return leaks no context to caller – consider 400 vs 401

When the refresh token is missing, the route returns 401 Unauthorized.
Technically the client is unauthenticated yet also made a malformed request.
Returning 400 Bad Request is often clearer (the client forgot to include mandatory data) and avoids log noise that might be interpreted as auth failures.

Double‑check consumer expectations and adjust if appropriate.

@mcstepp
Copy link
Collaborator Author

mcstepp commented Apr 28, 2025

Closing for a newer branch

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.

4 participants