|
1 | 1 | # CLAUDE-KNOWLEDGE.md |
2 | 2 |
|
3 | | -This file documents key learnings from implementing wildcard domain support in Stack Auth, organized in Q&A format. |
| 3 | +This file contains knowledge learned while working on the codebase in Q&A format. |
| 4 | + |
| 5 | +## Q: How do anonymous users work in Stack Auth? |
| 6 | +A: Anonymous users are a special type of user that can be created without any authentication. They have `isAnonymous: true` in the database and use different JWT signing keys with a `role: 'anon'` claim. Anonymous JWTs use a prefixed secret ("anon-" + audience) for signing and verification. |
| 7 | + |
| 8 | +## Q: How are anonymous user JWTs different from regular user JWTs? |
| 9 | +A: Anonymous JWTs have: |
| 10 | +1. Different kid (key ID) - prefixed with "anon-" in the generation |
| 11 | +2. Different signing secret - uses `getPerAudienceSecret` with `isAnonymous: true` |
| 12 | +3. Contains `role: 'anon'` in the payload |
| 13 | +4. Must pass `isAnonymous` flag to both `getPrivateJwk` and `getPublicJwkSet` functions for proper verification |
| 14 | + |
| 15 | +## Q: What is the X-Stack-Allow-Anonymous-User header? |
| 16 | +A: This header controls whether anonymous users are allowed to access an endpoint. When set to "true" (which is the default for client SDK calls), anonymous JWTs are accepted. When false or missing, anonymous users get an `AnonymousAuthenticationNotAllowed` error. |
| 17 | + |
| 18 | +## Q: How do you upgrade an anonymous user to a regular user? |
| 19 | +A: When an anonymous user (identified by `is_anonymous: true`) signs up or signs in through any auth method (password, OTP, OAuth), instead of creating a new user, the system upgrades the existing anonymous user by: |
| 20 | +1. Setting `is_anonymous: false` |
| 21 | +2. Adding the authentication method (email, password, OAuth provider, etc.) |
| 22 | +3. Keeping the same user ID so old JWTs remain valid |
| 23 | + |
| 24 | +## Q: How do you access the current user in smart route handlers? |
| 25 | +A: In smart route handlers, the user is accessed through `fullReq.auth?.user` not through the destructured `auth` parameter. The auth parameter only guarantees `tenancy`, while `user` is optional and needs to be accessed from the full request. |
| 26 | + |
| 27 | +## Q: How do user CRUD handlers work with parameters? |
| 28 | +A: The `adminUpdate` and similar methods take parameters directly, not wrapped in a `params` object: |
| 29 | +- Correct: `adminUpdate({ tenancy, user_id: "...", data: {...} })` |
| 30 | +- Wrong: `adminUpdate({ tenancy, params: { user_id: "..." }, data: {...} })` |
| 31 | + |
| 32 | +## Q: What query parameter filters anonymous users in user endpoints? |
| 33 | +A: The `include_anonymous` query parameter controls whether anonymous users are included in results: |
| 34 | +- Without parameter or `include_anonymous=false`: Anonymous users are filtered out |
| 35 | +- With `include_anonymous=true`: Anonymous users are included in results |
| 36 | +This applies to user list, get by ID, search, and team member endpoints. |
| 37 | + |
| 38 | +## Q: How does the JWKS endpoint handle anonymous keys? |
| 39 | +A: The JWKS (JSON Web Key Set) endpoint at `/.well-known/jwks.json`: |
| 40 | +- By default: Returns only regular user signing keys |
| 41 | +- With `?include_anonymous=true`: Returns both regular and anonymous user signing keys |
| 42 | +This allows systems that need to verify anonymous JWTs to fetch the appropriate public keys. |
| 43 | + |
| 44 | +## Q: What is the typical test command flow for Stack Auth? |
| 45 | +A: |
| 46 | +1. `pnpm typecheck` - Check TypeScript compilation |
| 47 | +2. `pnpm lint --fix` - Fix linting issues |
| 48 | +3. `pnpm test run <path>` - Run specific tests (the `run` is important to avoid watch mode) |
| 49 | +4. Use `-t "test name"` to run specific tests by name |
| 50 | + |
| 51 | +## Q: How do E2E tests handle authentication in Stack Auth? |
| 52 | +A: E2E tests use `niceBackendFetch` which automatically: |
| 53 | +- Sets `x-stack-allow-anonymous-user: "true"` for client access type |
| 54 | +- Includes project keys and tokens from `backendContext.value` |
| 55 | +- Handles auth tokens through the context rather than manual header setting |
| 56 | + |
| 57 | +## Q: What is the signature of a verification code handler? |
| 58 | +A: The handler function in `createVerificationCodeHandler` receives 5 parameters: |
| 59 | +```typescript |
| 60 | +async handler(tenancy, validatedMethod, validatedData, requestBody, currentUser) |
| 61 | +``` |
| 62 | +Where: |
| 63 | +- `tenancy` - The tenancy object |
| 64 | +- `validatedMethod` - The validated method data (e.g., `{ email: "..." }`) |
| 65 | +- `validatedData` - The validated data object |
| 66 | +- `requestBody` - The raw request body |
| 67 | +- `currentUser` - The current authenticated user (if any) |
| 68 | + |
| 69 | +## Q: How does JWT key derivation work for anonymous users? |
| 70 | +A: The JWT signing/verification uses a multi-step key derivation process: |
| 71 | +1. **Secret Derivation**: `getPerAudienceSecret()` creates a derived secret from: |
| 72 | + - Base secret (STACK_SERVER_SECRET) |
| 73 | + - Audience (usually project ID) |
| 74 | + - Optional "anon-" prefix for anonymous users |
| 75 | +2. **Kid Generation**: `getKid()` creates a key ID from: |
| 76 | + - Base secret (STACK_SERVER_SECRET) |
| 77 | + - "kid" string with optional "anon-" prefix |
| 78 | + - Takes only first 12 characters of hash |
| 79 | +3. **Key Generation**: Private/public keys are generated from the derived secret |
| 80 | + |
| 81 | +## Q: What is the JWT signing and verification flow? |
| 82 | +A: |
| 83 | +**Signing (signJWT)**: |
| 84 | +1. Derive secret: `getPerAudienceSecret(audience, STACK_SERVER_SECRET, isAnonymous)` |
| 85 | +2. Generate kid: `getKid(STACK_SERVER_SECRET, isAnonymous)` |
| 86 | +3. Create private key from derived secret |
| 87 | +4. Sign JWT with kid in header and role in payload |
| 88 | + |
| 89 | +**Verification (verifyJWT)**: |
| 90 | +1. Decode JWT without verification to read the role |
| 91 | +2. Check if role === 'anon' to determine if it's anonymous |
| 92 | +3. Derive secret with same parameters as signing |
| 93 | +4. Generate kid with same parameters as signing |
| 94 | +5. Create public key set and verify JWT |
| 95 | + |
| 96 | +## Q: What makes anonymous JWTs different from regular JWTs? |
| 97 | +A: Anonymous JWTs have: |
| 98 | +1. **Different derived secret**: Uses "anon-" prefix in secret derivation |
| 99 | +2. **Different kid**: Uses "anon-" prefix resulting in different key ID |
| 100 | +3. **Role field**: Contains `role: 'anon'` in the payload |
| 101 | +4. **Verification requirements**: Requires `allowAnonymous: true` flag to be verified |
| 102 | + |
| 103 | +## Q: How do you debug JWT verification issues? |
| 104 | +A: Common debugging steps: |
| 105 | +1. Check that the `X-Stack-Allow-Anonymous-User` header is set to "true" |
| 106 | +2. Verify the JWT has `role: 'anon'` in its payload |
| 107 | +3. Ensure the same secret derivation parameters are used for signing and verification |
| 108 | +4. Check that the kid in the JWT header matches the expected kid |
| 109 | +5. Verify that `allowAnonymous` flag is passed through the entire call chain |
| 110 | + |
| 111 | +## Q: What is the difference between getPrivateJwk and getPrivateJwkFromDerivedSecret? |
| 112 | +A: |
| 113 | +- `getPrivateJwk(secret, isAnonymous)`: Takes a base secret, may derive it internally, generates kid |
| 114 | +- `getPrivateJwkFromDerivedSecret(derivedSecret, kid)`: Takes an already-derived secret and pre-calculated kid |
| 115 | +The second is used internally for the actual JWT signing flow, while the first is for backward compatibility and special cases like IDP. |
| 116 | + |
| 117 | +## Q: How does the JWT verification process work with jose? |
| 118 | +A: The `jose.jwtVerify` function: |
| 119 | +1. Extracts the kid from the JWT header |
| 120 | +2. Looks for a key with matching kid in the provided JWK set |
| 121 | +3. Uses that key to verify the JWT signature |
| 122 | +4. If no matching kid is found, verification fails with an error |
| 123 | + |
| 124 | +## Q: What causes UNPARSABLE_ACCESS_TOKEN errors? |
| 125 | +A: This error occurs when JWT verification fails in `decodeAccessToken`. Common causes: |
| 126 | +1. Kid mismatch - the kid in the JWT header doesn't match any key in the JWK set |
| 127 | +2. Wrong secret derivation - using different parameters for signing vs verification |
| 128 | +3. JOSEError thrown during `jose.jwtVerify` due to invalid signature or key mismatch |
4 | 129 |
|
5 | 130 | ## OAuth Flow and Validation |
6 | 131 |
|
@@ -240,4 +365,4 @@ A: This happens when packages haven't been built yet. Run these commands in orde |
240 | 365 | ```bash |
241 | 366 | pnpm clean && pnpm i && pnpm codegen && pnpm build:packages |
242 | 367 | ``` |
243 | | -Then restart the dev server. This rebuilds all packages and generates the necessary TypeScript declarations. |
| 368 | +Then restart the dev server. This rebuilds all packages and generates the necessary TypeScript declarations. |
0 commit comments