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 @@
1import type { JWTVerifyOptions } from "jose";1import type { JWTVerifyOptions } from "jose";
2 2
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 */
4export const workosAccessTokenOptions: JWTVerifyOptions = {8export const workosAccessTokenOptions: JWTVerifyOptions = {
5 requiredClaims: ["exp", "iat"],9 requiredClaims: ["exp", "iat"],
6 maxTokenAge: "24h",
7};10};
apps/cloud/src/mcp/mcp-auth.node.test.ts+26−4
@@ -121,7 +121,7 @@ describe("MCP AuthKit token verification", () => { · 120 unmodified lines
121 121
122describe("access token expiry and identity boundaries", () => {122describe("access token expiry and identity boundaries", () => {
123 for (const kind of ["mcp", "user-management"] as const) {123 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) {
125 it.effect(`${kind} rejects ${invalidClaim}`, () =>125 it.effect(`${kind} rejects ${invalidClaim}`, () =>
126 Effect.gen(function* () {126 Effect.gen(function* () {
127 const { publicKey, privateKey } = yield* Effect.promise(() => generateKeyPair("RS256"));127 const { publicKey, privateKey } = yield* Effect.promise(() => generateKeyPair("RS256"));
@@ -134,9 +134,7 @@ describe("access token expiry and identity boundaries", () => { · 6 unmodified lines
134 iss: issuer,134 iss: issuer,
135 aud: resource,135 aud: resource,
136 ...(invalidClaim === "missing-exp" ? {} : { exp: now + 300 }),136 ...(invalidClaim === "missing-exp" ? {} : { exp: now + 300 }),
137 ...(invalidClaim === "missing-iat"137 ...(invalidClaim === "missing-iat" ? {} : { iat: now }),
138 ? {}
139 : { iat: invalidClaim === "older-than-one-day" ? now - 86401 : now }),
140 };138 };
141 const token = yield* Effect.promise(() =>139 const token = yield* Effect.promise(() =>
142 new SignJWT(claims)140 new SignJWT(claims)
@@ -153,6 +151,30 @@ describe("access token expiry and identity boundaries", () => { · 10 unmodified lines
153 }),151 }),
154 );152 );
155 }153 }
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 );
156 it.effect(`${kind} rejects a non-string organization claim`, () =>178 it.effect(`${kind} rejects a non-string organization claim`, () =>
157 Effect.gen(function* () {179 Effect.gen(function* () {
158 const { jwks, sign } = yield* Effect.promise(() => makeVerifier());180 const { jwks, sign } = yield* Effect.promise(() => makeVerifier());