diff --git a/apps/cloud/src/auth/access-token-options.ts b/apps/cloud/src/auth/access-token-options.ts index 0cc108abd..d67915ce7 100644 --- a/apps/cloud/src/auth/access-token-options.ts +++ b/apps/cloud/src/auth/access-token-options.ts @@ -1,7 +1,10 @@ import type { JWTVerifyOptions } from "jose"; -/** Require expiring WorkOS tokens and cap local verification at 24 hours. */ +/** + * Require expiring WorkOS tokens. Token lifetime is controlled by WorkOS via + * `exp`; do not cap the age locally, since AuthKit issues MCP access tokens + * that live for several days. + */ export const workosAccessTokenOptions: JWTVerifyOptions = { requiredClaims: ["exp", "iat"], - maxTokenAge: "24h", }; diff --git a/apps/cloud/src/mcp/mcp-auth.node.test.ts b/apps/cloud/src/mcp/mcp-auth.node.test.ts index 79eb4296d..bd47c3f92 100644 --- a/apps/cloud/src/mcp/mcp-auth.node.test.ts +++ b/apps/cloud/src/mcp/mcp-auth.node.test.ts @@ -121,7 +121,7 @@ describe("MCP AuthKit token verification", () => { describe("access token expiry and identity boundaries", () => { for (const kind of ["mcp", "user-management"] as const) { - for (const invalidClaim of ["missing-exp", "missing-iat", "older-than-one-day"] as const) { + for (const invalidClaim of ["missing-exp", "missing-iat"] as const) { it.effect(`${kind} rejects ${invalidClaim}`, () => Effect.gen(function* () { const { publicKey, privateKey } = yield* Effect.promise(() => generateKeyPair("RS256")); @@ -134,9 +134,7 @@ describe("access token expiry and identity boundaries", () => { iss: issuer, aud: resource, ...(invalidClaim === "missing-exp" ? {} : { exp: now + 300 }), - ...(invalidClaim === "missing-iat" - ? {} - : { iat: invalidClaim === "older-than-one-day" ? now - 86401 : now }), + ...(invalidClaim === "missing-iat" ? {} : { iat: now }), }; const token = yield* Effect.promise(() => new SignJWT(claims) @@ -153,6 +151,30 @@ describe("access token expiry and identity boundaries", () => { }), ); } + it.effect(`${kind} accepts a token issued more than a day ago that has not expired`, () => + Effect.gen(function* () { + const { publicKey, privateKey } = yield* Effect.promise(() => generateKeyPair("RS256")); + const jwk = yield* Effect.promise(() => exportJWK(publicKey)); + const jwks = createLocalJWKSet({ keys: [{ ...jwk, kid: "expiry-key" }] }); + const now = Math.floor(Date.now() / 1000); + const token = yield* Effect.promise(() => + new SignJWT({ + sub: "user_test", + org_id: "org_test", + iss: issuer, + aud: resource, + iat: now - 5 * 86400, + exp: now + 2 * 86400, + }) + .setProtectedHeader({ alg: "RS256", kid: "expiry-key" }) + .sign(privateKey), + ); + const verified = yield* kind === "mcp" + ? verifyMcpAccessToken(token, jwks, { issuer, audience: resource }) + : verifyWorkosUserManagementToken(token, jwks); + expect(verified).toEqual({ accountId: "user_test", organizationId: "org_test" }); + }), + ); it.effect(`${kind} rejects a non-string organization claim`, () => Effect.gen(function* () { const { jwks, sign } = yield* Effect.promise(() => makeVerifier());