mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-16 21:08:38 +08:00
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test With Custom Base Port / restart-dev-and-test-with-custom-base-port (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests with custom base port / setup-tests-with-custom-base-port (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
285 lines
9.8 KiB
Plaintext
285 lines
9.8 KiB
Plaintext
---
|
|
title: "JWT Tokens"
|
|
description: "Understand how Stack Auth uses JSON Web Tokens for authentication"
|
|
---
|
|
|
|
JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between two parties. Stack Auth uses JWTs for secure authentication and authorization.
|
|
|
|
## What is a JWT?
|
|
|
|
A JWT is a string that consists of three parts separated by dots (`.`):
|
|
|
|
1. **Header**: Contains metadata about the token, such as the signing algorithm
|
|
2. **Payload**: Contains the claims (data) about the user or entity
|
|
3. **Signature**: Used to verify the token's authenticity
|
|
|
|
The structure looks like this: `header.payload.signature`
|
|
|
|
## JWT Viewer
|
|
|
|
Use the interactive JWT viewer below to decode and inspect JWT tokens. If you're signed in, it will automatically load and display your current session token:
|
|
|
|
## Stack Auth JWT Structure
|
|
|
|
Stack Auth JWTs contain standardized headers and claims that power authentication throughout the platform.
|
|
|
|
### Header
|
|
|
|
* **`alg`**: Always `ES256`
|
|
* **`kid`**: Identifies which public key from the JWKS should be used for verification
|
|
|
|
### Standard Claims
|
|
|
|
* **`iss` (Issuer)**: `https://api.stack-auth.com/api/v1/projects/<project-id>` for regular users, or `https://api.stack-auth.com/api/v1/projects-anonymous-users/<project-id>` for anonymous sessions
|
|
* **`sub` (Subject)**: The user ID this token represents
|
|
* **`aud` (Audience)**: The intended recipient of the token — `<project-id>` for regular sessions, `<project-id>:anon` for anonymous sessions
|
|
* **`exp` (Expiration)**: When the token expires (Unix timestamp)
|
|
* **`iat` (Issued At)**: When the token was issued (Unix timestamp)
|
|
|
|
### Stack Auth Specific Claims
|
|
|
|
* **`project_id`**: Your Stack Auth project ID
|
|
* **`branch_id`**: The project branch (currently always `main`)
|
|
* **`refresh_token_id`**: ID of the associated refresh token
|
|
* **`role`**: Always set to `authenticated` for valid users
|
|
* **`name`**: The user's display name (nullable)
|
|
* **`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)
|
|
* **`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
|
|
|
|
Here's what a typical Stack Auth JWT payload looks like:
|
|
|
|
```json
|
|
{
|
|
"iss": "https://api.stack-auth.com/api/v1/projects/project_abcdef",
|
|
"sub": "user_123456",
|
|
"aud": "project_abcdef",
|
|
"exp": 1735689600,
|
|
"iat": 1735603200,
|
|
"project_id": "project_abcdef",
|
|
"branch_id": "main",
|
|
"refresh_token_id": "refresh_xyz789",
|
|
"requires_totp_mfa": false,
|
|
"role": "authenticated",
|
|
"name": "John Doe",
|
|
"email": "john@example.com",
|
|
"email_verified": true,
|
|
"selected_team_id": "team_789",
|
|
"is_anonymous": false,
|
|
"is_restricted": false,
|
|
"restricted_reason": null
|
|
}
|
|
```
|
|
|
|
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:
|
|
|
|
* `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:
|
|
|
|
**Next.js:**
|
|
|
|
```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>;
|
|
}
|
|
```
|
|
|
|
**React:**
|
|
|
|
```tsx
|
|
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>;
|
|
}
|
|
```
|
|
|
|
### Server-Side Usage
|
|
|
|
On the server side, you can access the JWT and its claims through the Stack Auth API:
|
|
|
|
```typescript
|
|
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',
|
|
});
|
|
|
|
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:
|
|
|
|
```typescript
|
|
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'],
|
|
});
|
|
```
|
|
|
|
To support restricted users (e.g., users who haven't verified their email), add `include_restricted=true`:
|
|
|
|
```typescript
|
|
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'],
|
|
});
|
|
```
|
|
|
|
### Signing Keys
|
|
|
|
* Private keys are deterministically derived from your project ID, optional anonymous audience, and the `STACK_SERVER_SECRET` environment variable. This means no key material is ever stored in the database.
|
|
* The JWKS currently exposes both the latest key pair and a legacy compatibility key. Verification libraries automatically pick the correct key by matching the `kid` provided in the JWT header.
|
|
* Tokens are always signed server-side; client SDKs never receive the private keys.
|
|
|
|
## Security Considerations
|
|
|
|
### Token Storage
|
|
|
|
* **Never store JWTs in localStorage** for sensitive applications
|
|
* Use secure, httpOnly cookies when possible
|
|
* Stack Auth handles secure token storage automatically
|
|
|
|
### Token Expiration
|
|
|
|
* JWTs have a limited lifetime (default is 10 minutes via `STACK_ACCESS_TOKEN_EXPIRATION_TIME`)
|
|
* Stack Auth automatically refreshes tokens before they expire
|
|
* Always check the `exp` claim when manually handling JWTs
|
|
|
|
### Signature Verification
|
|
|
|
* Always verify JWT signatures using the public key
|
|
* Never trust the contents of a JWT without verification
|
|
* Stack Auth SDKs handle verification automatically
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
1. **"JWT is expired"**: The token has passed its expiration time. Stack Auth will automatically refresh it.
|
|
|
|
2. **"Invalid signature"**: The token was tampered with or signed with a different key.
|
|
|
|
3. **"Invalid audience"**: The token was issued for a different project or environment.
|
|
|
|
### Debugging JWTs
|
|
|
|
Use the JWT viewer above to inspect tokens and verify their contents. Pay special attention to:
|
|
|
|
* Expiration times (`exp` claim)
|
|
* Audience (`aud` claim) matching your project
|
|
* Required claims are present
|
|
|
|
## Best Practices
|
|
|
|
1. **Let Stack Auth handle tokens**: Use the provided SDKs instead of manual JWT handling
|
|
2. **Validate on the server**: Always verify JWTs on your backend
|
|
3. **Check expiration**: Ensure tokens haven't expired before using them
|
|
4. **Use HTTPS**: Always transmit JWTs over secure connections
|
|
5. **Monitor token usage**: Log authentication events for security monitoring
|
|
|
|
## Related Concepts
|
|
|
|
* [API Keys](/docs/apps/api-keys) - Alternative authentication method for server-to-server communication
|
|
* [Backend Integration](/docs/concepts/backend-integration) - How to verify JWTs in your backend
|
|
* [Permissions](/docs/apps/permissions) - Understanding user permissions (not included in JWTs)
|
|
* [Teams](/docs/apps/orgs-and-teams) - Understanding team context in JWTs
|