Skip to content

Commit 6bbb2bb

Browse files
RhysSullivanclaude
andauthored
Stop capping WorkOS access token age at 24 hours (#2009)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 73a62dd commit 6bbb2bb

2 files changed

Lines changed: 31 additions & 6 deletions

File tree

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { JWTVerifyOptions } from "jose";
22

3-
/** Require expiring WorkOS tokens and cap local verification at 24 hours. */
3+
/**
4+
* Require expiring WorkOS tokens. Token lifetime is controlled by WorkOS via
5+
* `exp`; do not cap the age locally, since AuthKit issues MCP access tokens
6+
* that live for several days.
7+
*/
48
export const workosAccessTokenOptions: JWTVerifyOptions = {
59
requiredClaims: ["exp", "iat"],
6-
maxTokenAge: "24h",
710
};

apps/cloud/src/mcp/mcp-auth.node.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ describe("MCP AuthKit token verification", () => {
121121

122122
describe("access token expiry and identity boundaries", () => {
123123
for (const kind of ["mcp", "user-management"] as const) {
124-
for (const invalidClaim of ["missing-exp", "missing-iat", "older-than-one-day"] as const) {
124+
for (const invalidClaim of ["missing-exp", "missing-iat"] as const) {
125125
it.effect(`${kind} rejects ${invalidClaim}`, () =>
126126
Effect.gen(function* () {
127127
const { publicKey, privateKey } = yield* Effect.promise(() => generateKeyPair("RS256"));
@@ -134,9 +134,7 @@ describe("access token expiry and identity boundaries", () => {
134134
iss: issuer,
135135
aud: resource,
136136
...(invalidClaim === "missing-exp" ? {} : { exp: now + 300 }),
137-
...(invalidClaim === "missing-iat"
138-
? {}
139-
: { iat: invalidClaim === "older-than-one-day" ? now - 86401 : now }),
137+
...(invalidClaim === "missing-iat" ? {} : { iat: now }),
140138
};
141139
const token = yield* Effect.promise(() =>
142140
new SignJWT(claims)
@@ -153,6 +151,30 @@ describe("access token expiry and identity boundaries", () => {
153151
}),
154152
);
155153
}
154+
it.effect(`${kind} accepts a token issued more than a day ago that has not expired`, () =>
155+
Effect.gen(function* () {
156+
const { publicKey, privateKey } = yield* Effect.promise(() => generateKeyPair("RS256"));
157+
const jwk = yield* Effect.promise(() => exportJWK(publicKey));
158+
const jwks = createLocalJWKSet({ keys: [{ ...jwk, kid: "expiry-key" }] });
159+
const now = Math.floor(Date.now() / 1000);
160+
const token = yield* Effect.promise(() =>
161+
new SignJWT({
162+
sub: "user_test",
163+
org_id: "org_test",
164+
iss: issuer,
165+
aud: resource,
166+
iat: now - 5 * 86400,
167+
exp: now + 2 * 86400,
168+
})
169+
.setProtectedHeader({ alg: "RS256", kid: "expiry-key" })
170+
.sign(privateKey),
171+
);
172+
const verified = yield* kind === "mcp"
173+
? verifyMcpAccessToken(token, jwks, { issuer, audience: resource })
174+
: verifyWorkosUserManagementToken(token, jwks);
175+
expect(verified).toEqual({ accountId: "user_test", organizationId: "org_test" });
176+
}),
177+
);
156178
it.effect(`${kind} rejects a non-string organization claim`, () =>
157179
Effect.gen(function* () {
158180
const { jwks, sign } = yield* Effect.promise(() => makeVerifier());

0 commit comments

Comments
 (0)