feat(jwt): apply JWT templates and pin the signing key - #32
Merged
gjtorikian merged 3 commits intoAug 4, 2026
Conversation
A customer evaluating the emulator for API integration tests hit two blockers. `PUT /user_management/jwt_template` accepted a template and echoed it back, but the claims never reached a signed token — the endpoint looked implemented while doing nothing, which is worse than not offering it at all. The stored shape was invented too: the real API takes `content`, a template string, and has no `custom_claims` field anywhere in the spec. They also asked for a per-environment test signing key. The emulator generated a fresh keypair on every boot, so a restart invalidated the published JWKS along with every token already issued, and `iss` moved with the port. Pinning the key and the issuer lets a verifier treat both as constants and keeps tokens valid across a restart, so the service under test needs no environment-specific auth branch — only its usual JWKS fetch. Templates are validated when set and when seeded, so a bad one fails the boot rather than the first sign-in, and a template that renders past the 3072-byte limit fails the authenticate call by name instead of silently dropping its claims. BREAKING CHANGE: `PUT /user_management/jwt_template` now requires `content` and rejects `custom_claims` with a 422 naming the replacement. `custom_claims` was emulator-only and never reached a token, so no working setup depends on it. `GET` returns the spec shape (`content`, `created_at`, `updated_at`) and 404s before a template is set.
Greptile SummaryThe PR adds JWT-template claims to AuthKit access tokens and supports stable signing keys and issuers. It also moves template rendering before session persistence, although invitation acceptance still occurs first.
Confidence Score: 4/5The PR is not yet safe to merge because a JWT-template rendering failure can still consume an invitation without completing authentication. The session-persistence issue has been addressed by rendering before session creation, but invitation acceptance and membership mutation still occur first and are not rolled back when rendering returns a 422. Files Needing Attention: src/workos/routes/auth.ts; src/workos/helpers.ts Important Files Changed
Reviews (3): Last reviewed commit: "docs(jwt): note that a failed template r..." | Re-trigger Greptile |
A template that could not render returned its 422 after the session had already been inserted and last_sign_in_at bumped, so a sign-in that never handed back a token still left an orphaned session behind — along with the session.created and user.updated webhooks that imply the login succeeded. Rendering first makes the failure leave no trace. The render has to stay after acceptInvitation, whose membership the template context reads for organization_membership.role. Reading the pre-update user record is equivalent, since no template variable exposes last_sign_in_at.
gjtorikian
deleted the
feat/jwt-template-claims-and-pinnable-signing-key
branch
August 4, 2026 19:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PUT /user_management/jwt_templateaccepted a template, stored it, and echoed it back from
GET— while nothing read it atsign time. The endpoint looked implemented and did nothing, which is worse than not
offering it. Templates now render into every AuthKit access token.
custom_claimsappears nowhere in@workos/openapi-spec; the real API takescontent, a template string, and returnscreated_at/updated_at. The route now matches the spec, andGET404s before atemplate is set.
conditionals, or loops.
published JWKS along with every token already issued, and
issmoved with the port.Pinning both lets a verifier treat them as constants, so the service under test needs no
environment-specific auth branch — only its usual JWKS fetch.
by
--validate-config, rather than surfacing at the first sign-in. A template renderingpast WorkOS's 3072-byte cap fails
authenticatewith a 422 naming the size instead ofquietly dropping claims.
Reserved claims
A template may not set
iss,sub,exp,iat,nbf, orjti— rejected when thetemplate is set, and stripped at sign time as a backstop.
aud,sid,org_id,role,roles, andpermissionsare deliberately not reserved: the docs don't reserve them, soa template may override them and the rendered value wins.
Fields the emulator has no data for —
organization.allow_profiles_outside_organizationand
organization_membership.custom_attributes— resolve to null rather than being filledwith a plausible-looking default.