mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
## Stack Auth → Hexclave rename — PR 5 (internal symbols, paths,
packages, brand strings)
PR 5 finishes the **internal / non-wire** half of the Stack→Hexclave
rename. It only touches things where nothing outside the repo depends on
the exact name: internal symbols, file/dir names, the
`@stackframe/template` package, and residual brand strings. Plan +
progress are in `HEXCLAVE-RENAME-PR5-PLAN.md`.
Every step was verified green (`pnpm typecheck` + `pnpm lint`, 28/28)
and committed as its own checkpoint, then a fan-out of review agents
audited all commits and the findings were fixed.
### What changed
- **Internal symbols** (`@hexclave/shared`, `packages/template`, apps):
`stack*`/`Stack*` → `hexclave*`/`Hexclave*` — incl.
`stackGlobalsSymbol`, the `_Stack*AppImpl` classes,
`stackAppInternalsSymbol`, `StackContext`, `getStackStripe`, etc. The
`stack*App` local-variable convention
(`stackServerApp`/`stackClientApp`/…) was renamed across 175
source/example/doc files.
- **File renames**: `hexclave-handler/provider/context.tsx`,
`backend/hexclave.tsx`, `internal-tool/hexclave.ts`,
`hexclave-app-internals.ts`.
- **Directory renames**: `lib/hexclave-app`, `hexclave-companion`,
`[...hexclave]` route segment, `skills/hexclave`,
`dashboard/src/hexclave`, and the package dirs
**`packages/{next,shared,ui,sc,cli}`** (dropping the `stack-` prefix to
match the `@hexclave/*` npm names).
- **Packages**: `@stackframe/template` → `@hexclave/template`; **deleted
`packages/init-stack`** (onboarding lives in `@hexclave/cli init`; the
published npm package is untouched).
- **Brand strings**: reworded `Stack Auth`/`Stack dashboard` prose in
code + docs-mintlify, renamed `hexclave-app.mdx`/`use-hexclave-app.mdx`
with redirects, regenerated OpenAPI, updated coupled e2e assertions;
`doctor`/`init` now prefer `hexclave.config.ts`.
### Intentionally kept (verified, not oversights)
Wire/compat identifiers (`x-stack-*` headers, `stack-*` cookies,
`STACK_*` env names, `*.stack-auth.com`, `stackauth_`, `ask_stack_auth`,
query params), public `Stack*` SDK aliases, crypto/JWT/vault
domain-separation tags, `*-brand-sentinel`s, the
`Symbol.for("StackAuth--…")` string, `_stack_sync_metadata`, Postgres
`stackframe` / docker image names, the `stack-auth-logo*.svg` (used by
the rebrand modal), and `migration.mdx` / "formerly known as Stack Auth"
notes. False positives (Phosphor `StackIcon`/`StackSimple`, `TanStack`,
`OrbStack`, `stackable`/`Stacked` charts) left alone.
### Review pass
Six review agents audited all commits. Found + fixed one real bug — a
build script (`bundle-type-definitions.ts`) hardcoded the old
`lib/stack-app` glob path (not an import, so typecheck/lint were blind),
silently emptying the dashboard AI type bundle — plus stale comments, a
dead CI env var, and stale `.gitignore`/`.dockerignore` entries.
Cross-cutting audit confirmed **zero wire-compat identifiers were
accidentally renamed**.
### ⚠️ Verification note
`typecheck` + `lint` are fully green locally. The **e2e suite was not
run** (needs a live backend+DB), so the brand-string assertion +
OpenAPI-regen changes are verified by grep/codegen only — please let CI
exercise e2e to confirm.
### Base-branch note
This branch was forked from the local-only `cl/friendly-lewin-72293f`
(not on origin, no separate PR), so this PR against `dev` also carries
that branch's ~11 preceding Hexclave-rename commits (config-file rename,
env-var dual-read, AI setup-prompt rebrand). If those should land
separately, re-parent before merge.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Finishes the internal Stack Auth → Hexclave rename and cleans up
remaining stragglers, including dev-tool and prompt copy. All changes
are internal-only; public/wire APIs remain unchanged. Re-merged `dev`
and resolved the payments create-purchase-url conflict.
- **Refactors**
- Internal symbols: stack*/Stack* → hexclave*/Hexclave* (e.g.,
`getHexclaveServerApp` via `@/hexclave`, `getHexclaveStripe`,
`hexclaveAppInternalsSymbol`, `hexclaveSchemaInfo`, Prisma
`__hexclave_*`, `data-hexclave-handler-page`, Stripe mock
`hexclavePortPrefix`).
- Files/dirs: moved to `lib/hexclave-app`; handler route
`[...hexclave]`; backend entry `src/hexclave.tsx`; dashboard internals
`hexclave-app-internals`; companion `hexclave-companion`; dropped
`stack-` prefix across package dirs
(`packages/{shared,ui,sc,cli,next}`); workflows/emulator paths now
`packages/cli`; Quetzal codegen env at `packages/next/.env.local`.
- Packages/docs: `@stackframe/template` → `@hexclave/template`; removed
`packages/init-stack`; regenerated OpenAPI and updated docs
slugs/redirects for hexclave-app/use-hexclave-app.
- Brand strings/prompts: reworded remaining “Stack” dashboard strings to
Hexclave; updated dev-tool copy and prompts; `doctor/init` now prefer
`hexclave.config.ts`. Kept all wire-compat identifiers and public
aliases (`x-stack-*`, `stack-*` cookies, `STACK_*` env,
`*.stack-auth.com`, `Stack*` SDK names).
- Rebased/merged onto latest `dev`: retained `@hexclave/template`, kept
`src` in published files, refreshed setup-prompt imports and docs JSON,
adopted 1.0.5 version bumps, and re-merged `dev` again (resolved
`create-purchase-url` with `getHexclaveStripe`).
- **Bug Fixes**
- Restored dashboard AI type bundle by pointing the glob to
`packages/template/src/lib/hexclave-app`.
- Addressed rename leftovers: updated lingering `@/stack` imports and
CSS selector, fixed schema/meta and port-prefix expansions, and aligned
emulator commands to `packages/cli`.
- CI/build: removed a dead env var and stale ignore entries; fixed
Docker by renaming `STACK_SKIP_TEMPLATE_GENERATION` →
`HEXCLAVE_SKIP_TEMPLATE_GENERATION`.
<sup>Written for commit 3c1af3bff3.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1547?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
284 lines
9.8 KiB
Plaintext
284 lines
9.8 KiB
Plaintext
---
|
|
title: "JWT Tokens"
|
|
description: "Understand how Hexclave 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. Hexclave uses JWTs for secure authentication and authorization.
|
|
|
|
You don't need to worry about JWTs if you're using Hexclave. However, if you are an expert user and want the full flexibility to manually verify JWTs for performance or other reasons, this page is for you.
|
|
|
|
## 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`
|
|
|
|
|
|
## Hexclave JWT Structure
|
|
|
|
Hexclave 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.hexclave.com/api/v1/projects/<project-id>` for regular users, or `https://api.hexclave.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)
|
|
|
|
### Hexclave Specific Claims
|
|
|
|
* **`project_id`**: Your Hexclave 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 Hexclave JWT payload looks like:
|
|
|
|
```json
|
|
{
|
|
"iss": "https://api.hexclave.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.hexclave.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.hexclave.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](/guides/apps/authentication/sign-up-rules)) have the same structure:
|
|
|
|
* `iss` becomes `https://api.hexclave.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
|
|
|
|
Hexclave 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 '@hexclave/next';
|
|
|
|
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 '@hexclave/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 Hexclave API:
|
|
|
|
```typescript
|
|
import { hexclaveServerApp } from '@/stack';
|
|
|
|
export async function GET() {
|
|
const user = await hexclaveServerApp.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 Hexclave'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 Hexclave
|
|
const jwks = jose.createRemoteJWKSet(
|
|
new URL('https://api.hexclave.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.hexclave.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.hexclave.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true')
|
|
);
|
|
|
|
const { payload } = await jose.jwtVerify(token, jwks, {
|
|
issuer: [
|
|
'https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID',
|
|
'https://api.hexclave.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.hexclave.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.hexclave.com/api/v1/projects/YOUR_PROJECT_ID',
|
|
'https://api.hexclave.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID',
|
|
'https://api.hexclave.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
|
|
* Hexclave handles secure token storage automatically
|
|
|
|
### Token Expiration
|
|
|
|
* JWTs have a limited lifetime (default is 10 minutes via `STACK_ACCESS_TOKEN_EXPIRATION_TIME`)
|
|
* Hexclave 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
|
|
* Hexclave SDKs handle verification automatically
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
1. **"JWT is expired"**: The token has passed its expiration time. Hexclave 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 a JWT viewer such as [jwt.io](https://jwt.io/) 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 Hexclave 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](/guides/apps/api-keys/overview) - Alternative authentication method for server-to-server communication
|
|
* [Setup](/guides/getting-started/setup) - How to verify user sessions in your backend
|
|
* [Permissions](/guides/apps/rbac/overview) - Understanding user permissions (not included in JWTs)
|
|
* [Teams](/guides/apps/teams/overview) - Understanding team context in JWTs
|