-
Notifications
You must be signed in to change notification settings - Fork 513
docs: Document isRestricted and restrictedReason JWT claims #1136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0b873ea
38d495f
b9eb62d
6c2dc5a
7f02cac
e433f29
f760afe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { jwtExamples } from './jwt'; | ||
|
|
||
| export const conceptsExamples = { | ||
| 'jwt': jwtExamples, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { CodeExample } from '../../lib/code-examples'; | ||
|
|
||
| export const jwtExamples = { | ||
| 'client-side-usage': [ | ||
| { | ||
| language: 'JavaScript', | ||
| framework: 'Next.js', | ||
| code: `import { useUser } from '@stackframe/stack'; | ||
|
|
||
| export function UserProfile() { | ||
| const user = useUser(); | ||
|
|
||
| if (!user) { | ||
| return <div>Please sign in</div>; | ||
| } | ||
|
|
||
| return <div>Welcome, {user.displayName}!</div>; | ||
| }`, | ||
| 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 <div>Please sign in</div>; | ||
| } | ||
|
|
||
| return <div>Welcome, {user.displayName}!</div>; | ||
| }`, | ||
| 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') | ||
| ); | ||
|
|
||
| // 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'], | ||
| });`, | ||
| highlightLanguage: 'typescript', | ||
| filename: 'verify-jwt.ts' | ||
| }, | ||
| ] as CodeExample[], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, 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 | ||
|
|
||
|
|
@@ -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,92 +81,65 @@ Anonymous user tokens have the same shape, but: | |
| - `iss` becomes `https://api.stack-auth.com/api/v1/projects-anonymous-users/<project-id>` | ||
| - `aud` becomes `<project-id>: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: | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Citation: Documented restricted user token structure including the
vercel[bot] marked this conversation as resolved.
|
||
| - `iss` becomes `https://api.stack-auth.com/api/v1/projects-restricted-users/<project-id>` | ||
| - `aud` becomes `<project-id>:restricted` | ||
| - `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/<project-id>` | ||
| - `aud` becomes `<project-id>:restricted` | ||
| - `is_restricted` is `true` | ||
| - `restricted_reason` is `{ "type": "restricted_by_administrator" }` | ||
|
|
||
| ## Working with JWTs | ||
|
|
||
| ### Client-Side Usage | ||
|
|
||
| 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 <div>Please sign in</div>; | ||
| } | ||
|
|
||
| return <div>Welcome, {user.displayName}!</div>; | ||
| } | ||
| ``` | ||
| <PlatformCodeblock | ||
| document="concepts/jwt" | ||
| examples={["client-side-usage"]} | ||
| /> | ||
|
|
||
| ### 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... | ||
| }); | ||
| } | ||
| ``` | ||
| <PlatformCodeblock | ||
| document="concepts/jwt" | ||
| examples={["server-side-usage"]} | ||
| /> | ||
|
|
||
| ### 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'; | ||
| <PlatformCodeblock | ||
| document="concepts/jwt" | ||
| examples={["manual-jwt-verification"]} | ||
| /> | ||
|
|
||
| // 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') | ||
| ); | ||
| To support anonymous sessions, include those keys and allow both issuers and audiences: | ||
|
|
||
| // 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', | ||
| }); | ||
| <PlatformCodeblock | ||
| document="concepts/jwt" | ||
| examples={["manual-jwt-verification-anonymous"]} | ||
| /> | ||
|
|
||
| console.log('JWT is valid:', payload); | ||
| } catch (error) { | ||
| console.error('JWT verification failed:', error); | ||
| } | ||
| ``` | ||
| To support restricted users (e.g., users who haven't verified their email), add `include_restricted=true`: | ||
|
|
||
| To support anonymous sessions, include those keys and allow both issuers and audiences: | ||
|
|
||
| ```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'], | ||
| }); | ||
| ``` | ||
| <PlatformCodeblock | ||
| document="concepts/jwt" | ||
| examples={["manual-jwt-verification-restricted"]} | ||
| /> | ||
|
|
||
| ### Signing Keys | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.