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.
| Package | Affected versions | Patched versions |
|---|---|---|
@workos-inc/authkit-remixnpm | < 0.15.0 | 0.15.0 |
Patches
25 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.
20102afc74bfMerge commit from fork
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- github.com/advisories/GHSA-v3gr-w9gf-23cxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55009ghsaADVISORY
- github.com/workos/authkit-remix/commit/20102afc74bf3dd5150a975a098067fb406b90b6nvdWEB
- github.com/workos/authkit-remix/releases/tag/v0.15.0nvdWEB
- github.com/workos/authkit-remix/security/advisories/GHSA-v3gr-w9gf-23cxnvdWEB
- osv.dev/vulnerability/CVE-2025-55009ghsaWEB
- osv.dev/vulnerability/GHSA-v3gr-w9gf-23cxghsaWEB
News mentions
0No linked articles in our index yet.