From 0b873eafcca681f6da2815236e43ed45becdea27 Mon Sep 17 00:00:00 2001 From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:33:37 +0000 Subject: [PATCH 1/5] Document isRestricted and restrictedReason JWT claims --- docs/code-examples/concepts.ts | 140 ++++++++++++++++++++ docs/code-examples/index.ts | 2 + docs/content/docs/(guides)/concepts/jwt.mdx | 101 +++++--------- 3 files changed, 177 insertions(+), 66 deletions(-) create mode 100644 docs/code-examples/concepts.ts diff --git a/docs/code-examples/concepts.ts b/docs/code-examples/concepts.ts new file mode 100644 index 0000000000..d3b253cd02 --- /dev/null +++ b/docs/code-examples/concepts.ts @@ -0,0 +1,140 @@ +import { CodeExample } from '../lib/code-examples'; + +export const conceptsExamples = { + 'jwt': { + 'client-side-usage': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `import { useUser } from '@stackframe/stack'; + +export function UserProfile() { + const user = useUser(); + + if (!user) { + return
Please sign in
; + } + + return
Welcome, {user.displayName}!
; +}`, + highlightLanguage: 'tsx', + filename: 'app/components/user-profile.tsx' + }, + { + language: 'JavaScript', + framework: 'React', + code: `import { useUser } from '@stackframe/react'; + +export function UserProfile() { + const user = useUser(); + + if (!user) { + return
Please sign in
; + } + + return
Welcome, {user.displayName}!
; +}`, + highlightLanguage: 'tsx', + filename: 'components/UserProfile.tsx' + }, + ] as CodeExample[], + + 'server-side-usage': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `import { stackServerApp } from '@/stack'; + +export async function GET() { + const user = await stackServerApp.getUser(); + + if (!user) { + return new Response('Unauthorized', { status: 401 }); + } + + // Access user information from the JWT + return Response.json({ + id: user.id, + displayName: user.displayName, + primaryEmail: user.primaryEmail, + selectedTeamId: user.selectedTeamId, + // Other user properties... + }); +}`, + highlightLanguage: 'typescript', + filename: 'app/api/user/route.ts' + }, + ] as CodeExample[], + + 'manual-jwt-verification': [ + { + language: 'JavaScript', + framework: 'Node.js', + code: `import * as jose from 'jose'; + +// Get the public key set from Stack Auth +const jwks = jose.createRemoteJWKSet( + new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json') +); + +// Verify a regular (non-anonymous) access token +try { + const { payload } = await jose.jwtVerify(token, jwks, { + issuer: 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', + audience: 'YOUR_PROJECT_ID', + }); + + console.log('JWT is valid:', payload); +} catch (error) { + console.error('JWT verification failed:', error); +}`, + highlightLanguage: 'typescript', + filename: 'verify-jwt.ts' + }, + ] as CodeExample[], + + 'manual-jwt-verification-anonymous': [ + { + language: 'JavaScript', + framework: 'Node.js', + code: `import * as jose from 'jose'; + +const jwks = jose.createRemoteJWKSet( + new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true') +); + +const { payload } = await jose.jwtVerify(token, jwks, { + issuer: [ + 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', + 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID', + ], + audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'], +});`, + highlightLanguage: 'typescript', + filename: 'verify-jwt.ts' + }, + ] as CodeExample[], + + 'manual-jwt-verification-restricted': [ + { + language: 'JavaScript', + framework: 'Node.js', + code: `import * as jose from 'jose'; + +const jwks = jose.createRemoteJWKSet( + new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true') +); + +const { payload } = await jose.jwtVerify(token, jwks, { + issuer: [ + 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', + 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID', + ], + audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon', 'YOUR_PROJECT_ID:restricted'], +});`, + highlightLanguage: 'typescript', + filename: 'verify-jwt.ts' + }, + ] as CodeExample[], + }, +}; diff --git a/docs/code-examples/index.ts b/docs/code-examples/index.ts index 7d690775d1..2aaa8f4bfd 100644 --- a/docs/code-examples/index.ts +++ b/docs/code-examples/index.ts @@ -1,5 +1,6 @@ import { CodeExample } from '../lib/code-examples'; import { apiKeysExamples } from './api-keys'; +import { conceptsExamples } from './concepts'; import { customizationExamples } from './customization'; import { paymentsExamples } from './payments'; import { selfHostExamples } from './self-host'; @@ -9,6 +10,7 @@ import { viteExamples } from './vite-example'; const allExamples: Record>> = { 'setup': setupExamples, 'apps': {...apiKeysExamples, ...paymentsExamples }, + 'concepts': conceptsExamples, 'getting-started': viteExamples, 'others': selfHostExamples, 'customization': customizationExamples, diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx index bb4b607316..135b527714 100644 --- a/docs/content/docs/(guides)/concepts/jwt.mdx +++ b/docs/content/docs/(guides)/concepts/jwt.mdx @@ -48,6 +48,8 @@ Stack Auth JWTs contain standardized headers and claims that power authenticatio - **`email_verified`**: Whether the user's email has been verified - **`selected_team_id`**: The currently selected team ID (nullable) - **`is_anonymous`**: Whether this is an anonymous user session +- **`is_restricted`**: Whether the user is restricted (e.g., unverified email or anonymous) +- **`restricted_reason`**: Why the user is restricted (nullable). The `type` field is either `anonymous` or `email_not_verified` ## Example JWT Payload @@ -68,7 +70,9 @@ Here's what a typical Stack Auth JWT payload looks like: "email": "john@example.com", "email_verified": true, "selected_team_id": "team_789", - "is_anonymous": false + "is_anonymous": false, + "is_restricted": false, + "restricted_reason": null } ``` @@ -77,6 +81,14 @@ Anonymous user tokens have the same shape, but: - `iss` becomes `https://api.stack-auth.com/api/v1/projects-anonymous-users/` - `aud` becomes `:anon` - `is_anonymous` is `true` +- `is_restricted` is `true` +- `restricted_reason` is `{ "type": "anonymous" }` + +Restricted user tokens (e.g., users who haven't verified their email when verification is required) have: + +- `aud` becomes `:restricted` +- `is_restricted` is `true` +- `restricted_reason` is `{ "type": "email_not_verified" }` ## Working with JWTs @@ -84,85 +96,42 @@ Anonymous user tokens have the same shape, but: Stack Auth automatically handles JWT tokens for you. When you use hooks like `useUser()`, the JWT is automatically included in API requests: -```tsx -import { useUser } from '@stackframe/stack'; - -export function UserProfile() { - const user = useUser(); - - if (!user) { - return
Please sign in
; - } - - return
Welcome, {user.displayName}!
; -} -``` + ### Server-Side Usage On the server side, you can access the JWT and its claims through the Stack Auth API: -```tsx -import { stackServerApp } from '@/stack'; - -export async function GET() { - const user = await stackServerApp.getUser(); - - if (!user) { - return new Response('Unauthorized', { status: 401 }); - } - - // Access user information from the JWT - return Response.json({ - id: user.id, - displayName: user.displayName, - primaryEmail: user.primaryEmail, - selectedTeamId: user.selectedTeamId, - // Other user properties... - }); -} -``` + ### Manual JWT Verification If you need to manually verify a JWT (for example, in a different service), fetch the public keys from Stack Auth's JWKS endpoint. Keys are derived per audience so the `kid` in the JWT header always matches one of the published keys. -```typescript -import * as jose from 'jose'; + -// Get the public key set from Stack Auth -const jwks = jose.createRemoteJWKSet( - new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json') -); - -// Verify a regular (non-anonymous) access token -try { - const { payload } = await jose.jwtVerify(token, jwks, { - issuer: 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', - audience: 'YOUR_PROJECT_ID', - }); +To support anonymous sessions, include those keys and allow both issuers and audiences: - console.log('JWT is valid:', payload); -} catch (error) { - console.error('JWT verification failed:', error); -} -``` + -To support anonymous sessions, include those keys and allow both issuers and audiences: +To support restricted users (e.g., users who haven't verified their email), add `include_restricted=true`: -```typescript -const jwks = jose.createRemoteJWKSet( - new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true') -); - -const { payload } = await jose.jwtVerify(token, jwks, { - issuer: [ - 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', - 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID', - ], - audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'], -}); -``` + ### Signing Keys From b9eb62db881b4d898ae241fa18e3c4f958ba26bf Mon Sep 17 00:00:00 2001 From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:48:40 +0000 Subject: [PATCH 2/5] Sync documentation updates --- docs/code-examples/concepts/index.ts | 5 + .../{concepts.ts => concepts/jwt.ts} | 110 +++++++++--------- 2 files changed, 59 insertions(+), 56 deletions(-) create mode 100644 docs/code-examples/concepts/index.ts rename docs/code-examples/{concepts.ts => concepts/jwt.ts} (57%) diff --git a/docs/code-examples/concepts/index.ts b/docs/code-examples/concepts/index.ts new file mode 100644 index 0000000000..117b4cb054 --- /dev/null +++ b/docs/code-examples/concepts/index.ts @@ -0,0 +1,5 @@ +import { jwtExamples } from './jwt'; + +export const conceptsExamples = { + 'jwt': jwtExamples, +}; diff --git a/docs/code-examples/concepts.ts b/docs/code-examples/concepts/jwt.ts similarity index 57% rename from docs/code-examples/concepts.ts rename to docs/code-examples/concepts/jwt.ts index d3b253cd02..155ab40bbe 100644 --- a/docs/code-examples/concepts.ts +++ b/docs/code-examples/concepts/jwt.ts @@ -1,12 +1,11 @@ -import { CodeExample } from '../lib/code-examples'; +import { CodeExample } from '../../lib/code-examples'; -export const conceptsExamples = { - 'jwt': { - 'client-side-usage': [ - { - language: 'JavaScript', - framework: 'Next.js', - code: `import { useUser } from '@stackframe/stack'; +export const jwtExamples = { + 'client-side-usage': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `import { useUser } from '@stackframe/stack'; export function UserProfile() { const user = useUser(); @@ -17,13 +16,13 @@ export function UserProfile() { return
Welcome, {user.displayName}!
; }`, - highlightLanguage: 'tsx', - filename: 'app/components/user-profile.tsx' - }, - { - language: 'JavaScript', - framework: 'React', - code: `import { useUser } from '@stackframe/react'; + highlightLanguage: 'tsx', + filename: 'app/components/user-profile.tsx' + }, + { + language: 'JavaScript', + framework: 'React', + code: `import { useUser } from '@stackframe/react'; export function UserProfile() { const user = useUser(); @@ -34,16 +33,16 @@ export function UserProfile() { return
Welcome, {user.displayName}!
; }`, - highlightLanguage: 'tsx', - filename: 'components/UserProfile.tsx' - }, - ] as CodeExample[], + highlightLanguage: 'tsx', + filename: 'components/UserProfile.tsx' + }, + ] as CodeExample[], - 'server-side-usage': [ - { - language: 'JavaScript', - framework: 'Next.js', - code: `import { stackServerApp } from '@/stack'; + 'server-side-usage': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `import { stackServerApp } from '@/stack'; export async function GET() { const user = await stackServerApp.getUser(); @@ -61,16 +60,16 @@ export async function GET() { // Other user properties... }); }`, - highlightLanguage: 'typescript', - filename: 'app/api/user/route.ts' - }, - ] as CodeExample[], + highlightLanguage: 'typescript', + filename: 'app/api/user/route.ts' + }, + ] as CodeExample[], - 'manual-jwt-verification': [ - { - language: 'JavaScript', - framework: 'Node.js', - code: `import * as jose from 'jose'; + 'manual-jwt-verification': [ + { + language: 'JavaScript', + framework: 'Node.js', + code: `import * as jose from 'jose'; // Get the public key set from Stack Auth const jwks = jose.createRemoteJWKSet( @@ -88,16 +87,16 @@ try { } catch (error) { console.error('JWT verification failed:', error); }`, - highlightLanguage: 'typescript', - filename: 'verify-jwt.ts' - }, - ] as CodeExample[], + highlightLanguage: 'typescript', + filename: 'verify-jwt.ts' + }, + ] as CodeExample[], - 'manual-jwt-verification-anonymous': [ - { - language: 'JavaScript', - framework: 'Node.js', - code: `import * as jose from 'jose'; + 'manual-jwt-verification-anonymous': [ + { + language: 'JavaScript', + framework: 'Node.js', + code: `import * as jose from 'jose'; const jwks = jose.createRemoteJWKSet( new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true') @@ -110,16 +109,16 @@ const { payload } = await jose.jwtVerify(token, jwks, { ], audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'], });`, - highlightLanguage: 'typescript', - filename: 'verify-jwt.ts' - }, - ] as CodeExample[], + highlightLanguage: 'typescript', + filename: 'verify-jwt.ts' + }, + ] as CodeExample[], - 'manual-jwt-verification-restricted': [ - { - language: 'JavaScript', - framework: 'Node.js', - code: `import * as jose from 'jose'; + 'manual-jwt-verification-restricted': [ + { + language: 'JavaScript', + framework: 'Node.js', + code: `import * as jose from 'jose'; const jwks = jose.createRemoteJWKSet( new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true') @@ -132,9 +131,8 @@ const { payload } = await jose.jwtVerify(token, jwks, { ], audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon', 'YOUR_PROJECT_ID:restricted'], });`, - highlightLanguage: 'typescript', - filename: 'verify-jwt.ts' - }, - ] as CodeExample[], - }, + highlightLanguage: 'typescript', + filename: 'verify-jwt.ts' + }, + ] as CodeExample[], }; From 6c2dc5a6d7c9fbb569e8c54d64ec6fc7132d11bb Mon Sep 17 00:00:00 2001 From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 23:07:05 +0000 Subject: [PATCH 3/5] Sync documentation updates --- docs/code-examples/concepts/jwt.ts | 2 ++ docs/content/docs/(guides)/concepts/jwt.mdx | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/code-examples/concepts/jwt.ts b/docs/code-examples/concepts/jwt.ts index 155ab40bbe..64fd005bdf 100644 --- a/docs/code-examples/concepts/jwt.ts +++ b/docs/code-examples/concepts/jwt.ts @@ -124,6 +124,8 @@ const jwks = jose.createRemoteJWKSet( new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true') ); +// Restricted (non-anonymous) users use the same issuer as regular users, +// so only two issuers are needed even though there are three audiences const { payload } = await jose.jwtVerify(token, jwks, { issuer: [ 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx index 135b527714..92075c83b9 100644 --- a/docs/content/docs/(guides)/concepts/jwt.mdx +++ b/docs/content/docs/(guides)/concepts/jwt.mdx @@ -86,6 +86,7 @@ Anonymous user tokens have the same shape, but: Restricted user tokens (e.g., users who haven't verified their email when verification is required) have: +- `iss` remains unchanged (same as regular users) - `aud` becomes `:restricted` - `is_restricted` is `true` - `restricted_reason` is `{ "type": "email_not_verified" }` From 7f02cacf86ef71707042e5d630a8d1c945e5a6f7 Mon Sep 17 00:00:00 2001 From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 23:27:06 +0000 Subject: [PATCH 4/5] Sync documentation updates --- docs/code-examples/concepts/jwt.ts | 4 ++-- docs/content/docs/(guides)/concepts/jwt.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/code-examples/concepts/jwt.ts b/docs/code-examples/concepts/jwt.ts index 64fd005bdf..ec6baca1d4 100644 --- a/docs/code-examples/concepts/jwt.ts +++ b/docs/code-examples/concepts/jwt.ts @@ -124,12 +124,12 @@ const jwks = jose.createRemoteJWKSet( new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true') ); -// Restricted (non-anonymous) users use the same issuer as regular users, -// so only two issuers are needed even though there are three audiences +// All three user types have different issuers const { payload } = await jose.jwtVerify(token, jwks, { issuer: [ 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID', + 'https://api.stack-auth.com/api/v1/projects-restricted-users/YOUR_PROJECT_ID', ], audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon', 'YOUR_PROJECT_ID:restricted'], });`, diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx index 92075c83b9..331a9dc475 100644 --- a/docs/content/docs/(guides)/concepts/jwt.mdx +++ b/docs/content/docs/(guides)/concepts/jwt.mdx @@ -86,7 +86,7 @@ Anonymous user tokens have the same shape, but: Restricted user tokens (e.g., users who haven't verified their email when verification is required) have: -- `iss` remains unchanged (same as regular users) +- `iss` becomes `https://api.stack-auth.com/api/v1/projects-restricted-users/` - `aud` becomes `:restricted` - `is_restricted` is `true` - `restricted_reason` is `{ "type": "email_not_verified" }` From e433f2920c4b98c4a71f917effffc353d664c956 Mon Sep 17 00:00:00 2001 From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:56:32 +0000 Subject: [PATCH 5/5] Sync documentation updates --- docs/content/docs/(guides)/concepts/jwt.mdx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx index 331a9dc475..ee1410ebe6 100644 --- a/docs/content/docs/(guides)/concepts/jwt.mdx +++ b/docs/content/docs/(guides)/concepts/jwt.mdx @@ -48,8 +48,8 @@ Stack Auth JWTs contain standardized headers and claims that power authenticatio - **`email_verified`**: Whether the user's email has been verified - **`selected_team_id`**: The currently selected team ID (nullable) - **`is_anonymous`**: Whether this is an anonymous user session -- **`is_restricted`**: Whether the user is restricted (e.g., unverified email or anonymous) -- **`restricted_reason`**: Why the user is restricted (nullable). The `type` field is either `anonymous` or `email_not_verified` +- **`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` ## Example JWT Payload @@ -91,6 +91,13 @@ Restricted user tokens (e.g., users who haven't verified their email when verifi - `is_restricted` is `true` - `restricted_reason` is `{ "type": "email_not_verified" }` +Users restricted by an administrator (e.g., via [sign-up rules](/docs/concepts/sign-up-rules)) have the same structure: + +- `iss` becomes `https://api.stack-auth.com/api/v1/projects-restricted-users/` +- `aud` becomes `:restricted` +- `is_restricted` is `true` +- `restricted_reason` is `{ "type": "restricted_by_administrator" }` + ## Working with JWTs ### Client-Side Usage