Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,18 @@ When retrieving configuration values, AuthKit follows this priority order:
>
> To print out the entire config, a `getFullConfig` function is provided for debugging purposes.

| Option | Environment Variable | Default | Required | Description |
| ---------------- | ------------------------ | --------------------- | -------- | --------------------------------------------- |
| `clientId` | `WORKOS_CLIENT_ID` | - | Yes | Your WorkOS Client ID |
| `apiKey` | `WORKOS_API_KEY` | - | Yes | Your WorkOS API Key |
| `redirectUri` | `WORKOS_REDIRECT_URI` | - | Yes | The callback URL configured in WorkOS |
| `cookiePassword` | `WORKOS_COOKIE_PASSWORD` | - | Yes | Password for cookie encryption (min 32 chars) |
| `cookieName` | `WORKOS_COOKIE_NAME` | `wos-session` | No | Name of the session cookie |
| `apiHttps` | `WORKOS_API_HTTPS` | `true` | No | Whether to use HTTPS for API calls |
| `cookieMaxAge` | `WORKOS_COOKIE_MAX_AGE` | `34560000` (400 days) | No | Maximum age of cookie in seconds |
| `apiHostname` | `WORKOS_API_HOSTNAME` | `api.workos.com` | No | WorkOS API hostname |
| `apiPort` | `WORKOS_API_PORT` | - | No | Port to use for API calls |
| Option | Environment Variable | Default | Required | Description |
| ---------------- | ------------------------ | ----------------------- | -------- | --------------------------------------------- |
| `clientId` | `WORKOS_CLIENT_ID` | - | Yes | Your WorkOS Client ID |
| `apiKey` | `WORKOS_API_KEY` | - | Yes | Your WorkOS API Key |
| `redirectUri` | `WORKOS_REDIRECT_URI` | - | Yes | The callback URL configured in WorkOS |
| `cookiePassword` | `WORKOS_COOKIE_PASSWORD` | - | Yes | Password for cookie encryption (min 32 chars) |
| `cookieName` | `WORKOS_COOKIE_NAME` | `wos-session` | No | Name of the session cookie |
| `apiHttps` | `WORKOS_API_HTTPS` | `true` | No | Whether to use HTTPS for API calls |
| `cookieMaxAge` | `WORKOS_COOKIE_MAX_AGE` | `34560000` (400 days) | No | Maximum age of cookie in seconds |
| `apiHostname` | `WORKOS_API_HOSTNAME` | `api.workos.com` | No | WorkOS API hostname |
| `apiPort` | `WORKOS_API_PORT` | - | No | Port to use for API calls |
| `issuer` | `WORKOS_ISSUER` | `https://{apiHostname}` | No | Expected `iss` claim of access tokens |

> [!NOTE]
>
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ export interface AuthKitConfig {
*/
apiPort?: number;

/**
* The expected `iss` claim of WorkOS access tokens
* Equivalent to the WORKOS_ISSUER environment variable
* Defaults to `https://${apiHostname}`
*/
issuer?: string;

/**
* The maximum age of the session cookie in seconds
* Equivalent to the WORKOS_COOKIE_MAX_AGE environment variable
Expand Down
34 changes: 33 additions & 1 deletion src/session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ describe('session', () => {
jsonSpy.mockRestore();
});

it('validates the access token issuer claim against https://api.workos.com', async () => {
it('validates the access token issuer claim against https://api.workos.com by default', async () => {
await authkitLoader(createLoaderArgs(createMockRequest()));

expect(jwtVerify).toHaveBeenCalled();
Expand All @@ -470,6 +470,38 @@ describe('session', () => {
}
});

it('derives the expected issuer from apiHostname', async () => {
jwtVerify.mockClear();
process.env.WORKOS_API_HOSTNAME = 'api.workos-test.com';
try {
await authkitLoader(createLoaderArgs(createMockRequest()));
} finally {
delete process.env.WORKOS_API_HOSTNAME;
}

expect(jwtVerify).toHaveBeenCalled();
for (const call of jwtVerify.mock.calls) {
expect(call[2]).toEqual({ issuer: 'https://api.workos-test.com' });
}
});

it('prefers an explicitly configured issuer over apiHostname', async () => {
jwtVerify.mockClear();
process.env.WORKOS_API_HOSTNAME = 'api.workos-test.com';
process.env.WORKOS_ISSUER = 'https://auth.example.com';
try {
await authkitLoader(createLoaderArgs(createMockRequest()));
} finally {
delete process.env.WORKOS_API_HOSTNAME;
delete process.env.WORKOS_ISSUER;
}

expect(jwtVerify).toHaveBeenCalled();
for (const call of jwtVerify.mock.calls) {
expect(call[2]).toEqual({ issuer: 'https://auth.example.com' });
}
});

it('should return authorized data with session claims', async () => {
const { data } = await authkitLoader(createLoaderArgs(createMockRequest()));

Expand Down
14 changes: 8 additions & 6 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,22 +771,24 @@ function getJWKS(): ReturnType<typeof createRemoteJWKSet> {
}
return cachedJWKS;
}
// WorkOS access tokens carry a fixed `iss` claim regardless of environment
// or client id; see
// The `iss` claim on WorkOS access tokens is the API host that minted the
// token (e.g. `https://api.workos.com`), or a custom issuer when the
// environment has one configured; see
// https://workos.com/docs/reference/user-management/session-tokens/access-token.
// Validating it defends against tokens signed by a different WorkOS project
// whose JWKS happens to resolve to the same keys, and matches the team's
// "always validate iss" JWT rule.
// whose JWKS happens to resolve to the same keys.
//
// WorkOS access tokens do not carry a standard `aud` claim — the target
// client is encoded as `client_id` instead — so we do not pass `audience`
// to jwtVerify here; doing so would reject every token.
const WORKOS_JWT_ISSUER = 'https://api.workos.com';
function getExpectedIssuer(): string {
return getConfig('issuer') ?? `https://${getConfig('apiHostname')}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Custom API origins reject valid sessions

With apiHttps: false or apiPort set, getExpectedIssuer still uses HTTPS and omits the port. Valid tokens for that API origin fail verification, forcing repeated session refreshes.

Prompt for agents
Update src/session.ts getExpectedIssuer so its derived default reflects the complete configured API origin, including apiHttps and apiPort, while preserving an explicit issuer override. Add session tests covering HTTP and custom-port API configurations. Keep the default WorkOS configuration at https://api.workos.com.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

async function verifyAccessToken(accessToken: string) {
const JWKS = getJWKS();
try {
await jwtVerify(accessToken, JWKS, { issuer: WORKOS_JWT_ISSUER });
await jwtVerify(accessToken, JWKS, { issuer: getExpectedIssuer() });
return true;
} catch (e) {
return false;
Expand Down