Skip to content
Merged
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
7 changes: 5 additions & 2 deletions apps/cloud/src/auth/access-token-options.ts
Original file line number Diff line number Diff line change
@@ -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",
};
30 changes: 26 additions & 4 deletions apps/cloud/src/mcp/mcp-auth.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -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)
Expand All @@ -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());
Expand Down
Loading