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

CVE-2025-55009

CVE-2025-55009

Description

The AuthKit library for Remix provides convenient helpers for authentication and session management using WorkOS & AuthKit with Remix. In versions 0.14.1 and below, @workos-inc/authkit-remix exposed sensitive authentication artifacts — specifically sealedSession and accessToken — by returning them from the authkitLoader. This caused them to be rendered into the browser HTML.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@workos-inc/authkit-remixnpm
< 0.15.00.15.0

Patches

2
233d6c9f69ef

v0.15.0 (#70)

https://github.com/workos/authkit-remixNick NisiAug 1, 2025via osv
5 files changed · +14 9
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@workos-inc/authkit-remix",
    -  "version": "0.14.1",
    +  "version": "0.15.0",
       "description": "Authentication and session helpers for using WorkOS & AuthKit with Remix",
       "sideEffects": false,
       "type": "commonjs",
    
  • package-lock.json+2 2 modified
    @@ -1,12 +1,12 @@
     {
       "name": "@workos-inc/authkit-remix",
    -  "version": "0.14.1",
    +  "version": "0.15.0",
       "lockfileVersion": 3,
       "requires": true,
       "packages": {
         "": {
           "name": "@workos-inc/authkit-remix",
    -      "version": "0.14.1",
    +      "version": "0.15.0",
           "license": "MIT",
           "dependencies": {
             "@workos-inc/node": "^7.41.0",
    
  • src/session.spec.ts+1 1 modified
    @@ -688,7 +688,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/session.ts+9 4 modified
    @@ -165,7 +165,9 @@ type AuthLoader<Data> = (
       args: LoaderFunctionArgs & { auth: AuthorizedData | UnauthorizedData; getAccessToken: () => string | null },
     ) => LoaderReturnValue<Data>;
     
    -type AuthorizedAuthLoader<Data> = (args: LoaderFunctionArgs & { auth: AuthorizedData; getAccessToken: () => string }) => LoaderReturnValue<Data>;
    +type AuthorizedAuthLoader<Data> = (
    +  args: LoaderFunctionArgs & { auth: AuthorizedData; getAccessToken: () => string },
    +) => LoaderReturnValue<Data>;
     
     /**
      * This loader handles authentication state, session management, and access token refreshing
    @@ -340,7 +342,6 @@ export async function authkitLoader<Data = unknown>(
           entitlements = [],
         } = 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
    @@ -412,7 +413,7 @@ async function handleAuthLoader(
       // If there's a custom loader, get the resulting data and return it with our
       // auth data plus session cookie header
       let loaderResult;
    -  
    +
       if (auth.user) {
         // Authorized case
         const getAccessToken = () => {
    @@ -421,7 +422,11 @@ async function handleAuthLoader(
           }
           return session.accessToken;
         };
    -    loaderResult = await (loader as AuthorizedAuthLoader<unknown>)({ ...args, auth: auth as AuthorizedData, getAccessToken });
    +    loaderResult = await (loader as AuthorizedAuthLoader<unknown>)({
    +      ...args,
    +      auth: auth as AuthorizedData,
    +      getAccessToken,
    +    });
       } else {
         // Unauthorized case
         const getAccessToken = () => null;
    
  • 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.14.1';
    +const VERSION = '0.15.0';
     
     /**
      * Create a WorkOS instance with the provided API key and optional settings.
    
20102afc74bf

Merge commit from fork

https://github.com/workos/authkit-remixNick NisiAug 1, 2025via ghsa
4 files changed · +166 26
  • README.md+99 8 modified
    @@ -144,7 +144,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`, and `impersonator`.
       const { user, signInUrl, signUpUrl } = useLoaderData<typeof loader>();
     
    @@ -222,32 +222,123 @@ 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 type { LoaderFunctionArgs } from '@remix-run/node';
     import { authkitLoader } from '@workos-inc/authkit-remix';
     
     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: 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: 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-remix';
    +import { redirect } from '@remix-run/node';
    +import type { LoaderFunctionArgs } from '@remix-run/node';
    +
    +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
    @@ -100,25 +100,21 @@ export type AuthKitLoaderOptions = {
     export interface AuthorizedData {
       user: User;
       sessionId: string;
    -  accessToken: string;
       organizationId: string | null;
       role: string | null;
       permissions: string[];
       entitlements: string[];
       impersonator: Impersonator | null;
    -  sealedSession: string;
     }
     
     export interface UnauthorizedData {
       user: null;
       sessionId: null;
    -  accessToken: null;
       organizationId: null;
       role: null;
       permissions: null;
       entitlements: null;
       impersonator: null;
    -  sealedSession: null;
     }
     
     /**
    
  • src/session.spec.ts+47 6 modified
    @@ -275,14 +275,12 @@ describe('session', () => {
     
             expect(data).toEqual({
               user: null,
    -          accessToken: null,
               impersonator: null,
               organizationId: null,
               permissions: null,
               entitlements: null,
               role: null,
               sessionId: null,
    -          sealedSession: null,
             });
           });
     
    @@ -393,14 +391,12 @@ describe('session', () => {
     
             expect(data).toEqual({
               user: mockSessionData.user,
    -          accessToken: mockSessionData.accessToken,
               impersonator: null,
               organizationId: 'org-123',
               permissions: ['read', 'write'],
               entitlements: ['premium'],
               role: 'admin',
               sessionId: 'test-session-id',
    -          sealedSession: 'encrypted-jwt',
             });
           });
     
    @@ -417,7 +413,6 @@ describe('session', () => {
                 customData: 'test-value',
                 metadata: { key: 'value' },
                 user: mockSessionData.user,
    -            accessToken: mockSessionData.accessToken,
                 sessionId: 'test-session-id',
               }),
             );
    @@ -662,6 +657,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', () => {
    @@ -730,7 +772,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+20 8 modified
    @@ -162,10 +162,10 @@ 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
    @@ -320,14 +320,12 @@ export async function authkitLoader<Data = unknown>(
     
           const auth: UnauthorizedData = {
             user: null,
    -        accessToken: null,
             impersonator: null,
             organizationId: null,
             permissions: null,
             entitlements: null,
             role: null,
             sessionId: null,
    -        sealedSession: null,
           };
     
           return await handleAuthLoader(loader, loaderArgs, auth);
    @@ -358,13 +356,11 @@ export async function authkitLoader<Data = unknown>(
         const auth: AuthorizedData = {
           user: session.user,
           sessionId,
    -      accessToken: session.accessToken,
           organizationId,
           role,
           permissions,
           entitlements,
           impersonator,
    -      sealedSession: cookieSession.get('jwt'),
         };
     
         return await handleAuthLoader(loader, loaderArgs, auth, session);
    @@ -413,8 +409,24 @@ async function handleAuthLoader(
         return data(auth, session ? { headers: { ...session.headers } } : undefined);
       }
     
    -  // Call the user's loader function
    -  const loaderResult = await loader({ ...args, auth: auth as AuthorizedData });
    +  // If there's a custom loader, get the resulting data and return it with our
    +  // auth data plus session cookie header
    +  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 });
    +  }
     
       // Special handling for DataWithResponseInit (from data())
       if (isDataWithResponseInit(loaderResult)) {
    

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

7

News mentions

0

No linked articles in our index yet.