History

Stop capping WorkOS access token age at 24 hours (#2009)

Rhys Sullivan committed 1 month ago

Commit 6bbb2bb1 parent 73a62dd2 files · +31 −6Browse files

Unified · Split

2 files
  1. apps/cloud/src/auth/access-token-options.ts +5 −2
  2. apps/cloud/src/mcp/mcp-auth.node.test.ts +26 −4
apps/cloud/src/auth/access-token-options.ts+5−2
@@ -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+26−4
@@ -121,7 +121,7 @@ describe("MCP AuthKit token verification", () => { · 120 unmodified lines
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", () => { · 6 unmodified lines
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", () => { · 10 unmodified lines
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());