From cf53313ff488c6572949c53fed2fc65b05572289 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 30 Mar 2026 17:43:06 -0700 Subject: [PATCH] rename signed_up_at_millis JWT claim to signed_up_at --- apps/backend/src/lib/tokens.tsx | 2 +- apps/e2e/tests/backend/backend-helpers.ts | 2 +- apps/e2e/tests/js/access-token-refresh.test.ts | 9 +++++---- claude/CLAUDE-KNOWLEDGE.md | 4 ++-- docs/content/docs/(guides)/concepts/jwt.mdx | 4 ++-- packages/stack-shared/src/schema-fields.ts | 3 +-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/backend/src/lib/tokens.tsx b/apps/backend/src/lib/tokens.tsx index 872b4726a..e9c817f79 100644 --- a/apps/backend/src/lib/tokens.tsx +++ b/apps/backend/src/lib/tokens.tsx @@ -314,7 +314,7 @@ export async function generateAccessTokenFromRefreshTokenIfValid(options: Refres email: user.primary_email, email_verified: user.primary_email_verified, selected_team_id: user.selected_team_id, - signed_up_at_millis: user.signed_up_at_millis, + signed_up_at: Math.floor(user.signed_up_at_millis / 1000), is_anonymous: user.is_anonymous, is_restricted: user.is_restricted, restricted_reason: user.restricted_reason, diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index 71b2c9d73..7365aefd2 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -299,7 +299,7 @@ export namespace Auth { "iss": expectedIssuer, "branch_id": "main", "refresh_token_id": expect.any(String), - "signed_up_at_millis": expect.any(Number), + "signed_up_at": expect.any(Number), "requires_totp_mfa": expect.any(Boolean), "aud": backendContext.value.projectKeys === "no-project" ? expect.any(String) : backendContext.value.projectKeys.projectId, "sub": expect.any(String), diff --git a/apps/e2e/tests/js/access-token-refresh.test.ts b/apps/e2e/tests/js/access-token-refresh.test.ts index fb4ee81ab..d3cbcaa38 100644 --- a/apps/e2e/tests/js/access-token-refresh.test.ts +++ b/apps/e2e/tests/js/access-token-refresh.test.ts @@ -366,8 +366,8 @@ describe("access token refresh on user property changes", () => { }); }); - describe("signed_up_at_millis claim", () => { - it("should include signed_up_at_millis and keep it stable across token refreshes", async ({ expect }) => { + describe("signed_up_at claim", () => { + it("should include signed_up_at and keep it stable across token refreshes", async ({ expect }) => { const { clientApp } = await createApp({ config: { credentialEnabled: true, @@ -385,7 +385,8 @@ describe("access token refresh on user property changes", () => { expect(initialToken).toBeDefined(); const initialPayload = decodeAccessToken(initialToken!); - expect(initialPayload.signed_up_at_millis).toBe(user.signedUpAt.getTime()); + const signedUpAtSeconds = Math.floor(user.signedUpAt.getTime() / 1000); + expect(initialPayload.signed_up_at).toBe(signedUpAtSeconds); await user.setDisplayName("Updated display name"); @@ -393,7 +394,7 @@ describe("access token refresh on user property changes", () => { expect(refreshedToken).toBeDefined(); const refreshedPayload = decodeAccessToken(refreshedToken!); - expect(refreshedPayload.signed_up_at_millis).toBe(user.signedUpAt.getTime()); + expect(refreshedPayload.signed_up_at).toBe(signedUpAtSeconds); }); }); diff --git a/claude/CLAUDE-KNOWLEDGE.md b/claude/CLAUDE-KNOWLEDGE.md index 507c86fc1..0f5fa27da 100644 --- a/claude/CLAUDE-KNOWLEDGE.md +++ b/claude/CLAUDE-KNOWLEDGE.md @@ -154,5 +154,5 @@ A: In `packages/template/src/components-page/stack-handler-client.tsx`, parse ha Q: What is the current `app.urls` contract after deprecating runtime URL mutation? A: `app.urls` is now static (`getUrls(...)` only) and no longer injects runtime `after_auth_return_to` / `stack_cross_domain_*` params from `window.location`. For navigation flows, examples and consumers should use `redirectToXyz()` methods instead (for example `redirectToSignIn()` / `redirectToSignOut()`), while tests for hosted flows should assert dynamic params on actual redirect methods, not on `app.urls`. -Q: How should new JWT claims be rolled out without breaking old access tokens? -A: Add the claim to token generation in `apps/backend/src/lib/tokens.tsx`, but keep the decode schema backward-compatible by adding the field as optional in `packages/stack-shared/src/schema-fields.ts` with a `// TODO next-release` to later switch it to `.defined()` after all deployments issue the new claim. +Q: How should user signup time be exposed in JWT claims before production rollout? +A: Use `signed_up_at` (OIDC-style naming) in access tokens and encode it as Unix seconds in `apps/backend/src/lib/tokens.tsx` (`Math.floor(user.signed_up_at_millis / 1000)`). Since this is pre-prod, the payload schema can require `signed_up_at` directly without a backward-compat optional shim. diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx index 42e7a0e08..e53d71130 100644 --- a/docs/content/docs/(guides)/concepts/jwt.mdx +++ b/docs/content/docs/(guides)/concepts/jwt.mdx @@ -47,7 +47,7 @@ Stack Auth JWTs contain standardized headers and claims that power authenticatio - **`email`**: The user's primary email address (nullable) - **`email_verified`**: Whether the user's email has been verified - **`selected_team_id`**: The currently selected team ID (nullable) -- **`signed_up_at_millis`**: When this user signed up (Unix timestamp in milliseconds) +- **`signed_up_at`**: When this user signed up (Unix timestamp in seconds) - **`is_anonymous`**: Whether this is an anonymous user session - **`is_restricted`**: Whether the user is restricted (e.g., unverified email, anonymous, or restricted by an administrator) - **`restricted_reason`**: Why the user is restricted (nullable). The `type` field is `anonymous`, `email_not_verified`, or `restricted_by_administrator` @@ -72,7 +72,7 @@ Here's what a typical Stack Auth JWT payload looks like: "email": "john@example.com", "email_verified": true, "selected_team_id": "team_789", - "signed_up_at_millis": 1735000000000, + "signed_up_at": 1735000000, "is_anonymous": false, "is_restricted": false, "restricted_reason": null, diff --git a/packages/stack-shared/src/schema-fields.ts b/packages/stack-shared/src/schema-fields.ts index 74b5b2490..5e81fdf95 100644 --- a/packages/stack-shared/src/schema-fields.ts +++ b/packages/stack-shared/src/schema-fields.ts @@ -789,8 +789,7 @@ export const accessTokenPayloadSchema = yupObject({ email: yupString().defined().nullable(), email_verified: yupBoolean().defined(), selected_team_id: yupString().defined().nullable(), - // TODO next-release: make this .defined() once all deployments generate signed_up_at_millis in access tokens - signed_up_at_millis: signedUpAtMillisSchema.optional(), + signed_up_at: yupNumber().defined(), is_anonymous: yupBoolean().defined(), is_restricted: yupBoolean().defined(), restricted_reason: restrictedReasonSchema.defined().nullable(),