JWT

How Permix validates JWTs, which algorithms and claims are supported, and how the ValidatorCache works in SaaS mode.

Edit this page on GitHub

Permix performs full JWT validation on every authenticated request — signature verification, expiry check, issuer resolution, and claim extraction.

Supported algorithms#

AlgorithmKey typeStatus
RS256RSA (2048-bit minimum)
ES256EC 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#

  1. Extract kid (key ID) and alg from the JWT header.
  2. Look up the JWKS endpoint — global URI in selfhost mode, per-issuer ValidatorCache in saas mode.
  3. Fetch and cache the public keys from the JWKS endpoint.
  4. Verify the JWT signature.
  5. Validate exp (expiration) and iat (issued at).
  6. 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:

env
ROLES_CLAIM=realm_access.roles
DOMAIN_CLAIM=dom
ADMIN_DOMAIN_CLAIM=adm
EXCLUDED_ROLES=offline_access,uma_authorization

SaaS mode — set per tenant via claim_config when registering an IdP:

json
{
  "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:

env
EXCLUDED_ROLES=offline_access,uma_authorization,default-roles-myrealm

These 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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

bash
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.