VYPR
High severity7.1NVD Advisory· Published Aug 9, 2025· Updated Apr 15, 2026

CVE-2025-55008

CVE-2025-55008

Description

The AuthKit library for React Router 7+ provides helpers for authentication and session management using WorkOS & AuthKit with React Router. In versions 0.6.1 and below, @workos-inc/authkit-react-router exposed sensitive authentication artifacts — specifically sealedSession and accessToken by returning them from the authkitLoader. This caused them to be rendered into the browser HTML. This issue is fixed in version 0.7.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@workos-inc/authkit-react-routernpm
< 0.7.00.7.0

Patches

2
540ba02c20d8

v0.7.0 (#30)

4 files changed · +5 5
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@workos-inc/authkit-react-router",
    -  "version": "0.6.1",
    +  "version": "0.7.0",
       "description": "Authentication and session helpers for using WorkOS & AuthKit with React Router 7+",
       "sideEffects": false,
       "type": "commonjs",
    
  • package-lock.json+2 2 modified
    @@ -1,12 +1,12 @@
     {
       "name": "@workos-inc/authkit-react-router",
    -  "version": "0.6.1",
    +  "version": "0.7.0",
       "lockfileVersion": 3,
       "requires": true,
       "packages": {
         "": {
           "name": "@workos-inc/authkit-react-router",
    -      "version": "0.6.1",
    +      "version": "0.7.0",
           "license": "MIT",
           "dependencies": {
             "@workos-inc/node": "^7.41.0",
    
  • src/session.spec.ts+1 1 modified
    @@ -495,7 +495,7 @@ describe('session', () => {
           it('should return null from getAccessToken for unauthenticated users', async () => {
             // Mock no session
             unsealData.mockResolvedValue(null);
    -        
    +
             const customLoader = jest.fn().mockImplementation(({ getAccessToken }) => {
               const token = getAccessToken();
               return { retrievedToken: token };
    
  • src/workos.ts+1 1 modified
    @@ -2,7 +2,7 @@ import { WorkOS } from '@workos-inc/node';
     import { getConfig } from './config.js';
     import { lazy } from './utils.js';
     
    -const VERSION = '0.6.1';
    +const VERSION = '0.7.0';
     
     /**
      * Create a WorkOS instance with the provided API key and optional settings.
    
607caac65878

Merge commit from fork

4 files changed · +169 26
  • README.md+98 8 modified
    @@ -146,7 +146,7 @@ export const loader = (args: LoaderFunctionArgs) => authkitLoader(args);
     
     export function App() {
       // Retrieves the user from the session or returns `null` if no user is signed in
    -  // Other supported values include `sessionId`, `accessToken`, `organizationId`,
    +  // Other supported values include `sessionId`, `organizationId`,
       // `role`, `permissions`, `entitlements`, `featureFlags`, and `impersonator`.
       const { user, signInUrl, signUpUrl } = useLoaderData<typeof loader>();
     
    @@ -230,32 +230,122 @@ export async function action({ request }: ActionFunctionArgs) {
     
     ### Get the access token
     
    -Sometimes it is useful to obtain the access token directly, for instance to make API requests to another service.
    +Access tokens are available through the `getAccessToken()` function within your loader. This design encourages server-side token usage while making the security implications explicit.
     
     ```tsx
     import { data, type LoaderFunctionArgs } from 'react-router';
     import { authkitLoader } from '@workos-inc/authkit-react-router';
     
     export const loader = (args: LoaderFunctionArgs) =>
    -  authkitLoader(args, async ({ auth }) => {
    -    const { accessToken } = auth;
    -
    -    if (!accessToken) {
    -      // Not signed in
    +  authkitLoader(args, async ({ auth, getAccessToken }) => {
    +    if (!auth.user) {
    +      // Not signed in - getAccessToken() would return null
    +      return data({ data: null });
         }
     
    +    // Explicitly call the function to get the access token
    +    const accessToken = getAccessToken();
    +    
         const serviceData = await fetch('/api/path', {
           headers: {
             Authorization: `Bearer ${accessToken}`,
           },
         });
     
         return data({
    -      data: serviceData,
    +      data: await serviceData.json(),
         });
       });
     ```
     
    +#### Security Considerations
    +
    +By default, access tokens are not included in the data sent to React components. This helps prevent unintentional token exposure in:
    +- Browser developer tools
    +- HTML source code  
    +- Client-side logs or error reporting
    +
    +If you need to expose the access token to client-side code, you can explicitly return it from your loader:
    +
    +```tsx
    +export const loader = (args: LoaderFunctionArgs) =>
    +  authkitLoader(args, async ({ auth, getAccessToken }) => {
    +    const accessToken = getAccessToken();
    +    
    +    return {
    +      // Only expose to client if absolutely necessary
    +      accessToken,
    +      userData: await fetchUserData(accessToken)
    +    };
    +  }, { ensureSignedIn: true });
    +```
    +
    +**Note:** Only expose access tokens to the client when necessary for your use case (e.g., making direct API calls from the browser). Consider alternatives like:
    +- Making API calls server-side in your loaders
    +- Creating proxy endpoints in your application
    +- Using separate client-specific tokens with limited scope
    +
    +#### Using with `ensureSignedIn`
    +
    +When using the `ensureSignedIn` option, you can be confident that `getAccessToken()` will always return a valid token:
    +
    +```tsx
    +export const loader = (args: LoaderFunctionArgs) =>
    +  authkitLoader(args, async ({ auth, getAccessToken }) => {
    +    // With ensureSignedIn: true, the user is guaranteed to be authenticated
    +    const accessToken = getAccessToken();
    +    
    +    // Use the token for your API calls
    +    const data = await fetchProtectedData(accessToken);
    +    
    +    return { data };
    +  }, { ensureSignedIn: true });
    +```
    +
    +### Using withAuth for low-level access
    +
    +For advanced use cases, the `withAuth` function provides direct access to authentication data, including the access token. Unlike `authkitLoader`, this function:
    +
    +- Does not handle automatic token refresh
    +- Does not manage cookies or session updates  
    +- Returns the access token directly as a property
    +- Requires manual redirect handling for unauthenticated users
    +
    +```tsx
    +import { withAuth } from '@workos-inc/authkit-react-router';
    +import { redirect, type LoaderFunctionArgs } from 'react-router';
    +
    +export const loader = async (args: LoaderFunctionArgs) => {
    +  const auth = await withAuth(args);
    +  
    +  if (!auth.user) {
    +    // Manual redirect - withAuth doesn't handle this automatically
    +    throw redirect('/sign-in');
    +  }
    +  
    +  // Access token is directly available as a property
    +  const { accessToken, user, sessionId } = auth;
    +  
    +  // Use the token for server-side operations
    +  const apiData = await fetch('https://api.example.com/data', {
    +    headers: { Authorization: `Bearer ${accessToken}` }
    +  });
    +  
    +  // Be careful what you return - accessToken will be exposed if included
    +  return {
    +    user,
    +    apiData: await apiData.json(),
    +    // accessToken, // ⚠️ Only include if client-side access is necessary
    +  };
    +};
    +```
    +
    +**When to use `withAuth` vs `authkitLoader`:**
    +
    +- Use `authkitLoader` for most cases - it handles token refresh, cookies, and provides safer defaults
    +- Use `withAuth` when you need more control or are building custom authentication flows
    +- `withAuth` is useful for API routes or middleware where you don't need the full loader functionality
    +
     ### Debugging
     
     To enable debug logs, pass in the debug flag when using `authkitLoader`.
    
  • src/interfaces.ts+0 4 modified
    @@ -108,27 +108,23 @@ export type AuthKitLoaderOptions = {
     export interface AuthorizedData {
       user: User;
       sessionId: string;
    -  accessToken: string;
       organizationId: string | null;
       role: string | null;
       permissions: string[];
       entitlements: string[];
       featureFlags: string[];
       impersonator: Impersonator | null;
    -  sealedSession: string;
     }
     
     export interface UnauthorizedData {
       user: null;
       sessionId: null;
    -  accessToken: null;
       organizationId: null;
       role: null;
       permissions: null;
       entitlements: null;
       featureFlags: null;
       impersonator: null;
    -  sealedSession: null;
     }
     
     /**
    
  • src/session.spec.ts+47 6 modified
    @@ -277,15 +277,13 @@ describe('session', () => {
     
             expect(data).toEqual({
               user: null,
    -          accessToken: null,
               impersonator: null,
               organizationId: null,
               permissions: null,
               entitlements: null,
               featureFlags: null,
               role: null,
               sessionId: null,
    -          sealedSession: null,
             });
           });
     
    @@ -397,15 +395,13 @@ describe('session', () => {
     
             expect(data).toEqual({
               user: mockSessionData.user,
    -          accessToken: mockSessionData.accessToken,
               impersonator: null,
               organizationId: 'org-123',
               permissions: ['read', 'write'],
               entitlements: ['premium'],
               featureFlags: ['flag-1', 'flag-2'],
               role: 'admin',
               sessionId: 'test-session-id',
    -          sealedSession: 'encrypted-jwt',
             });
           });
     
    @@ -422,7 +418,6 @@ describe('session', () => {
                 customData: 'test-value',
                 metadata: { key: 'value' },
                 user: mockSessionData.user,
    -            accessToken: mockSessionData.accessToken,
                 sessionId: 'test-session-id',
               }),
             );
    @@ -469,6 +464,53 @@ describe('session', () => {
               expect(response.headers.get('X-Redirect-Reason')).toBe('test');
             }
           });
    +
    +      it('should provide getAccessToken function to custom loader', async () => {
    +        const customLoader = jest.fn().mockImplementation(({ getAccessToken }) => {
    +          const token = getAccessToken();
    +          return { retrievedToken: token };
    +        });
    +
    +        const { data } = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
    +
    +        // Verify the loader was called with getAccessToken function
    +        expect(customLoader).toHaveBeenCalledWith(
    +          expect.objectContaining({
    +            auth: expect.objectContaining({
    +              user: mockSessionData.user,
    +            }),
    +            getAccessToken: expect.any(Function),
    +          }),
    +        );
    +
    +        // Verify the token was retrieved correctly
    +        expect(data).toEqual(
    +          expect.objectContaining({
    +            retrievedToken: mockSessionData.accessToken,
    +            user: mockSessionData.user,
    +          }),
    +        );
    +      });
    +
    +      it('should return null from getAccessToken for unauthenticated users', async () => {
    +        // Mock no session
    +        unsealData.mockResolvedValue(null);
    +        
    +        const customLoader = jest.fn().mockImplementation(({ getAccessToken }) => {
    +          const token = getAccessToken();
    +          return { retrievedToken: token };
    +        });
    +
    +        const { data } = await authkitLoader(createLoaderArgs(createMockRequest()), customLoader);
    +
    +        // Verify getAccessToken returned null
    +        expect(data).toEqual(
    +          expect.objectContaining({
    +            retrievedToken: null,
    +            user: null,
    +          }),
    +        );
    +      });
         });
     
         describe('session refresh', () => {
    @@ -539,7 +581,6 @@ describe('session', () => {
             // Verify the response contains the new token data
             expect(data).toEqual(
               expect.objectContaining({
    -            accessToken: 'new.valid.token',
                 sessionId: 'new-session-id',
                 organizationId: 'org-123',
                 role: 'user',
    
  • src/session.ts+24 8 modified
    @@ -164,10 +164,12 @@ type LoaderValue<Data> = Response | TypedResponse<Data> | NonNullable<Data> | nu
     type LoaderReturnValue<Data> = Promise<LoaderValue<Data>> | LoaderValue<Data>;
     
     type AuthLoader<Data> = (
    -  args: LoaderFunctionArgs & { auth: AuthorizedData | UnauthorizedData },
    +  args: LoaderFunctionArgs & { auth: AuthorizedData | UnauthorizedData; getAccessToken: () => string | null },
     ) => LoaderReturnValue<Data>;
     
    -type AuthorizedAuthLoader<Data> = (args: LoaderFunctionArgs & { auth: AuthorizedData }) => LoaderReturnValue<Data>;
    +type AuthorizedAuthLoader<Data> = (
    +  args: LoaderFunctionArgs & { auth: AuthorizedData; getAccessToken: () => string },
    +) => LoaderReturnValue<Data>;
     
     /**
      * This loader handles authentication state, session management, and access token refreshing
    @@ -322,15 +324,13 @@ export async function authkitLoader<Data = unknown>(
     
           const auth: UnauthorizedData = {
             user: null,
    -        accessToken: null,
             impersonator: null,
             organizationId: null,
             permissions: null,
             entitlements: null,
             featureFlags: null,
             role: null,
             sessionId: null,
    -        sealedSession: null,
           };
     
           return await handleAuthLoader(loader, loaderArgs, auth);
    @@ -346,7 +346,6 @@ export async function authkitLoader<Data = unknown>(
           featureFlags = [],
         } = getClaimsFromAccessToken(session.accessToken);
     
    -    const cookieSession = await getSession(request.headers.get('Cookie'));
         const { impersonator = null } = session;
     
         // checking for 'headers' in session determines if the session was refreshed or not
    @@ -362,14 +361,12 @@ export async function authkitLoader<Data = unknown>(
         const auth: AuthorizedData = {
           user: session.user,
           sessionId,
    -      accessToken: session.accessToken,
           organizationId,
           role,
           permissions,
           entitlements,
           featureFlags,
           impersonator,
    -      sealedSession: cookieSession.get('jwt'),
         };
     
         return await handleAuthLoader(loader, loaderArgs, auth, session);
    @@ -420,7 +417,26 @@ async function handleAuthLoader(
     
       // If there's a custom loader, get the resulting data and return it with our
       // auth data plus session cookie header
    -  const loaderResult = await loader({ ...args, auth: auth as AuthorizedData });
    +  let loaderResult;
    +
    +  if (auth.user) {
    +    // Authorized case
    +    const getAccessToken = () => {
    +      if (!session?.accessToken) {
    +        throw new Error('No access token available');
    +      }
    +      return session.accessToken;
    +    };
    +    loaderResult = await (loader as AuthorizedAuthLoader<unknown>)({
    +      ...args,
    +      auth: auth as AuthorizedData,
    +      getAccessToken,
    +    });
    +  } else {
    +    // Unauthorized case
    +    const getAccessToken = () => null;
    +    loaderResult = await (loader as AuthLoader<unknown>)({ ...args, auth, getAccessToken });
    +  }
     
       if (isResponse(loaderResult)) {
         // If the result is a redirect, return it unedited
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.