Your users sign in with Auth0 (or Keycloak, or Clerk). You want agents, delegation and an MCP OAuth server, and you do not want to move the login to get them. Configure the provider as an external issuer. theAuth verifies its tokens and maps each subject to a theAuth identity.
What gets checked, every time:
iss must equal a configured issuer exactly. Unknown issuers are rejected before any key is fetched.
aud must contain one of your configured audiences. There is no default, an empty audience is a configuration error.
- The algorithm must be in the allowlist (default
RS256). none and HMAC algorithms are refused at startup, which closes the classic key confusion attack.
exp, sub and iss are required. nbf and exp allow 30 seconds of clock skew by default.
- The JWKS URL must be https.
Failures come back as codes you can log: ISSUER_NOT_ALLOWED, CLAIM_REJECTED (issuer or audience), TOKEN_EXPIRED, ALG_REJECTED, SIGNATURE_INVALID, KEY_NOT_FOUND, TOKEN_MALFORMED.
Key caching and rotation
Keys are cached per issuer for 10 minutes (jwksTtlSec). When a token arrives with a kid you have not seen, theAuth refetches once, then waits at least 30 seconds (refetchCooldownSec) before it will refetch again. That handles a provider rotating keys, and stops someone sending random kid values from turning your service into a request cannon against your IdP.
Map people to theAuth identities on first sight
Opt in to just in time provisioning:
- The account is keyed on (issuer name, subject). Seeing the same subject again returns the same user.
- If a theAuth account already has that email, nothing is linked by default. Set
linkVerifiedEmail: true to link, and it only happens when the provider says email_verified: true. Never turn this on for a provider that lets users type an unverified email.
- To create an agent identity instead of a user, pass
provision: async (identity) => ({ id: await createMyAgent(identity) }).
onLogin never throws and never blocks a login. See Run both and migrate as users log in.
Cut over one route or tenant at a time
Routes you do not list use the fallback (incumbent). Setting percent to 0 sends every route, fully moved ones included, back to the incumbent.
Shadow mode
Before you move a decision, run both and compare.
The theAuth side uses the simulator, which has no side effects. Its answer is compared and logged, never returned as the decision. If the simulator throws, you see an error count in shadow.stats() and the caller sees nothing. Last modified on October 9, 2026