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/jwt.ts b/docs/code-examples/concepts/jwt.ts new file mode 100644 index 0000000000..ec6baca1d4 --- /dev/null +++ b/docs/code-examples/concepts/jwt.ts @@ -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
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') +); + +// 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[], +}; 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..ee1410ebe6 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, 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,6 +81,22 @@ 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: + +- `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" }` + +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 @@ -84,85 +104,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') -); +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', - }); + - 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'], -}); -``` + ### Signing Keys