Skip to content
83 changes: 76 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,13 @@ export const config = { matcher: ['/', '/admin'] };

The middleware can be configured with several options.

| Option | Default | Description |
| ---------------- | ----------- | ------------------------------------------------------------------------------------------------------ |
| `redirectUri` | `undefined` | Used in cases where you need your redirect URI to be set dynamically (e.g. Vercel preview deployments) |
| `middlewareAuth` | `undefined` | Used to configure middleware auth options. See [middleware auth](#middleware-auth) for more details. |
| `debug` | `false` | Enables debug logs. |
| `signUpPaths` | `[]` | Used to specify paths that should use the 'sign-up' screen hint when redirecting to AuthKit. |
| Option | Default | Description |
| ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| `redirectUri` | `undefined` | Used in cases where you need your redirect URI to be set dynamically (e.g. Vercel preview deployments) |
| `middlewareAuth` | `undefined` | Used to configure middleware auth options. See [middleware auth](#middleware-auth) for more details. |
| `debug` | `false` | Enables debug logs. |
| `signUpPaths` | `[]` | Used to specify paths that should use the 'sign-up' screen hint when redirecting to AuthKit. |
| `eagerAuth` | `false` | Enables synchronous access token availability for third-party services. See [eager auth](#eager-auth) for more details. |

#### Custom redirect URI

Expand Down Expand Up @@ -430,6 +431,74 @@ In the above example the `/admin` page will require a user to be signed in, wher

`unauthenticatedPaths` uses the same glob logic as the [Next.js matcher](https://nextjs.org/docs/pages/building-your-application/routing/middleware#matcher).

### Eager auth

The `eagerAuth` option enables synchronous access to authentication tokens on initial page load, which is required by some third-party services that validate tokens directly with WorkOS. When enabled, tokens are available immediately without requiring an asynchronous fetch.

#### How it works

When `eagerAuth: true` is set, the middleware temporarily stores the access token in a short-lived cookie (30 seconds) that is:

- Only set on initial page loads (not API or prefetch requests)
- Immediately consumed and deleted by the client
- Available synchronously on the first render

#### Usage

Enable eager auth in your middleware configuration:

```ts
import { authkitMiddleware } from '@workos-inc/authkit-nextjs';

export default authkitMiddleware({
eagerAuth: true,
});
```

Then access the token synchronously in your client components:

```tsx
'use client';

import { useAuth } from '@workos-inc/authkit-nextjs';

function MyComponent() {
const { getAccessToken } = useAuth();

// Token is available immediately on initial page load
const token = getAccessToken();

// Use with third-party services that need immediate token access
if (token) {
// Initialize your third-party client with the token
thirdPartyClient.authenticate(token);
}

return <div>...</div>;
}
```

#### Security considerations

Eager auth makes tokens briefly accessible via JavaScript (30-second window) to enable synchronous access. This is a common pattern used by many authentication libraries and is generally safe with standard XSS protections.

**Best practices:**

- Implement a Content Security Policy (CSP) if handling sensitive data
- Review third-party scripts on authenticated pages
- Use the standard `getAccessToken()` method when synchronous access isn't required

**When to use:**

- Third-party services that require synchronous token access
- Real-time features that need immediate authentication
- When you want to avoid loading states on initial render

**When to use standard async tokens:**

- Most API calls where a brief loading state is acceptable
- When you don't need immediate token access on page load

### Composing middleware

> **Security note:** Always forward `request.headers` when returning `NextResponse.*` to mitigate SSRF issues in Next.js < 14.2.32 (14.x) or < 15.4.7 (15.x). This pattern is safe on all versions. We strongly recommend upgrading to the latest Next.js.
Expand All @@ -451,7 +520,7 @@ export default async function middleware(request: NextRequest) {
});

const { pathname } = new URL(request.url);

// Control of what to do when there's no session on a protected route is left to the developer
if (pathname.startsWith('/account') && !session.user) {
console.log('No session on protected path');
Expand Down
133 changes: 129 additions & 4 deletions __tests__/cookie.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,10 @@ describe('cookie.ts', () => {
it('should return the cookie options as a string', async () => {
const { getCookieOptions } = await import('../src/cookie');
const options = getCookieOptions('http://example.com', true, false);
expect(options).toEqual(
expect.stringContaining('Path=/; HttpOnly; SameSite=Lax; Max-Age=34560000; Domain=example.com'),
);
expect(options).toEqual(expect.stringContaining('HttpOnly; SameSite=Lax; Max-Age=34560000; Domain=example.com'));
expect(options).toEqual(expect.not.stringContaining('Secure'));

const options2 = getCookieOptions('https://example.com', true, true);
expect(options2).toEqual(expect.stringContaining('Path=/'));
expect(options2).toEqual(expect.stringContaining('HttpOnly'));
expect(options2).toEqual(expect.stringContaining('Secure'));
expect(options2).toEqual(expect.stringContaining('SameSite=Lax'));
Expand Down Expand Up @@ -148,4 +145,132 @@ describe('cookie.ts', () => {
expect(cookieString).toContain('SameSite=Lax'); // Capitalized
});
});

describe('getJwtCookie', () => {
beforeEach(() => {
// Reset NODE_ENV for each test
delete process.env.NODE_ENV;
});

it('should create JWT cookie with Secure flag for HTTPS URLs', async () => {
const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie('test-token', 'https://example.com');

expect(cookie).toBe('workos-access-token=test-token; SameSite=Lax; Max-Age=30; Secure');
});

it('should create JWT cookie without Secure flag for HTTP URLs', async () => {
const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie('test-token', 'http://localhost:3000');

expect(cookie).toBe('workos-access-token=test-token; SameSite=Lax; Max-Age=30');
});

it('should force Secure in production except for localhost', async () => {
process.env.NODE_ENV = 'production';

const { getJwtCookie } = await import('../src/cookie');

// Production with regular domain should be secure
const prodCookie = getJwtCookie('prod-token', 'http://example.com');
expect(prodCookie).toContain('Secure');

// Production with localhost should not be secure
const localhostCookie = getJwtCookie('local-token', 'http://localhost:3000');
expect(localhostCookie).not.toContain('Secure');
});

it('should handle invalid URLs with no fallback URL', async () => {
process.env.NODE_ENV = 'production';

// Mock no WORKOS_REDIRECT_URI
const envVars = await import('../src/env-variables');
Object.defineProperty(envVars, 'WORKOS_REDIRECT_URI', { value: '' });

const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie('token', 'invalid-url');

expect(cookie).toContain('Secure'); // Should default to secure in production when no fallback
});

it('should fall back to WORKOS_REDIRECT_URI when invalid URL provided', async () => {
const envVars = await import('../src/env-variables');
Object.defineProperty(envVars, 'WORKOS_REDIRECT_URI', { value: 'https://app.workos.com/callback' });

const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie('token', 'invalid-url');

expect(cookie).toContain('Secure'); // Should use HTTPS from fallback URL
});

it('should set secure to false when WORKOS_REDIRECT_URI parsing fails', async () => {
process.env.NODE_ENV = 'development'; // Not production

const envVars = await import('../src/env-variables');
Object.defineProperty(envVars, 'WORKOS_REDIRECT_URI', { value: 'also-invalid-url' });

const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie('token', null); // This triggers the WORKOS_REDIRECT_URI path

expect(cookie).not.toContain('Secure'); // Should be false when URL parsing fails (line 128)
});

it('should handle both main URL and fallback URL parsing failures', async () => {
const envVars = await import('../src/env-variables');
Object.defineProperty(envVars, 'WORKOS_REDIRECT_URI', { value: 'invalid-fallback-url' });

const { getJwtCookie } = await import('../src/cookie');

// Invalid main URL with invalid fallback URL - should hit line 118
const cookie = getJwtCookie('token', 'invalid-main-url');

expect(cookie).not.toContain('Secure'); // Line 118: secure = false when fallback parsing fails
});

it('should use WORKOS_REDIRECT_URI when no URL provided', async () => {
const envVars = await import('../src/env-variables');
Object.defineProperty(envVars, 'WORKOS_REDIRECT_URI', { value: 'https://secure.example.com' });

const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie('token', null);

expect(cookie).toContain('Secure'); // Should use HTTPS from WORKOS_REDIRECT_URI
});

it('should create expired JWT cookie for deletion', async () => {
const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie('token', 'https://example.com', true);

expect(cookie).toBe(
'workos-access-token=; SameSite=Lax; Max-Age=0; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
);
});

it('should handle null token body', async () => {
const { getJwtCookie } = await import('../src/cookie');

const cookie = getJwtCookie(null, 'https://example.com');

expect(cookie).toBe('workos-access-token=; SameSite=Lax; Max-Age=30; Secure');
});

it('should handle localhost vs 127.0.0.1 in production', async () => {
process.env.NODE_ENV = 'production';

const { getJwtCookie } = await import('../src/cookie');

const localhostCookie = getJwtCookie('token', 'http://localhost:3000');
const ipCookie = getJwtCookie('token', 'http://127.0.0.1:3000');

expect(localhostCookie).not.toContain('Secure');
expect(ipCookie).not.toContain('Secure');
});
});
});
Loading