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
5 changes: 5 additions & 0 deletions docs/code-examples/concepts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { jwtExamples } from './jwt';

export const conceptsExamples = {
'jwt': jwtExamples,
};
140 changes: 140 additions & 0 deletions docs/code-examples/concepts/jwt.ts
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',
Comment thread
vercel[bot] marked this conversation as resolved.
'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[],
};
2 changes: 2 additions & 0 deletions docs/code-examples/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,6 +10,7 @@ import { viteExamples } from './vite-example';
const allExamples: Record<string, Record<string, Record<string, CodeExample[]>>> = {
'setup': setupExamples,
'apps': {...apiKeysExamples, ...paymentsExamples },
'concepts': conceptsExamples,
'getting-started': viteExamples,
'others': selfHostExamples,
'customization': customizationExamples,
Expand Down
109 changes: 43 additions & 66 deletions docs/content/docs/(guides)/concepts/jwt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
```

Expand All @@ -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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Citation: Documented restricted user token structure including the :restricted audience suffix. See apps/backend/src/lib/tokens.tsx for audience derivation logic and apps/backend/src/app/api/latest/users/crud.tsx for the computeRestrictedStatus() function that determines when users are restricted.
View source

Comment thread
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

Expand Down
Loading