JWT
How Permix validates JWTs, which algorithms and claims are supported, and how the ValidatorCache works in SaaS mode.
Edit this page on GitHubPermix performs full JWT validation on every authenticated request — signature verification, expiry check, issuer resolution, and claim extraction.
Supported algorithms#
| Algorithm | Key type | Status |
|---|---|---|
| RS256 | RSA (2048-bit minimum) | ✅ |
| ES256 | EC P-256 | ✅ |
The correct algorithm is detected automatically from the alg header in the JWT and matched against the public key fetched from the JWKS endpoint.
Validation flow#
- Extract
kid(key ID) andalgfrom the JWT header. - Look up the JWKS endpoint — global URI in
selfhostmode, per-issuerValidatorCacheinsaasmode. - Fetch and cache the public keys from the JWKS endpoint.
- Verify the JWT signature.
- Validate
exp(expiration) andiat(issued at). - Extract authorization claims (
roles,domain,admin_domain) using the configured claim paths.
If any step fails, the request is rejected with 401 Unauthorized.
Claim extraction#
Authorization data is read from the JWT payload using configurable claim paths.
Self-hosted mode — set via environment variables:
ROLES_CLAIM=realm_access.roles
DOMAIN_CLAIM=dom
ADMIN_DOMAIN_CLAIM=adm
EXCLUDED_ROLES=offline_access,uma_authorizationSaaS mode — set per tenant via claim_config when registering an IdP:
{
"claim_config": {
"roles_claim": "realm_access.roles",
"domain_claim": "dom",
"admin_domain_claim": "adm"
}
}Nested claim paths use dot-notation. realm_access.roles resolves to payload["realm_access"]["roles"].
Excluded roles#
The EXCLUDED_ROLES env var (self-hosted) lets you strip service-account roles from the Casbin evaluation that Keycloak and other IdPs inject by default:
EXCLUDED_ROLES=offline_access,uma_authorization,default-roles-myrealmThese roles are removed from the roles array before any policy check.
JWKS caching#
The ValidatorCache caches parsed public keys per issuer URL with a bounded TTL. On cache miss (first request per issuer, or TTL expiry), the service fetches the JWKS endpoint. If a fetch fails, the last known-good validator is used as a fallback — transient IdP downtime does not cause immediate authentication failures.
Keycloak token example#
A typical Keycloak RS256 token payload after extraction:
{
"sub": "user-uuid-1234",
"iss": "https://keycloak.example.com/realms/myrealm",
"exp": 1715000000,
"realm_access": {
"roles": ["finance", "offline_access"]
},
"dom": "tenant_prod",
"adm": null
}After applying ROLES_CLAIM=realm_access.roles and EXCLUDED_ROLES=offline_access, the resolved roles are ["finance"] and domain is tenant_prod.
Auth0 token example#
Auth0 uses custom namespace claims for non-standard fields:
{
"sub": "auth0|user-1234",
"iss": "https://your-tenant.us.auth0.com/",
"exp": 1715000000,
"https://your-app.com/roles": ["editor"],
"https://your-app.com/tenant": "acme"
}Register the IdP with:
{
"claim_config": {
"roles_claim": "https://your-app.com/roles",
"domain_claim": "https://your-app.com/tenant"
}
}Using JWTs in check requests#
The Java SDK forwards the inbound Authorization header automatically. For direct REST calls, the same token used to call your service should be passed to the authorization check:
curl -X POST https://api.permix.dev/api/v1/check \
-H "Authorization: Bearer $USER_JWT" \
-d '{
"subject": "user-uuid-1234",
"resource": "invoice:read",
"action": "read",
"domain": "tenant_prod"
}'The service validates the JWT and uses the extracted claims to perform the policy check.