Skip to main content

authz-keycloak

authz-keycloak

Authorizes requests against a Keycloak authorization server using the UMA 2.0 permission check. For each request the plugin takes the caller's bearer access token and asks Keycloak whether it grants the configured permissions, using the urn:ietf:params:oauth:grant-type:uma-ticket grant with response_mode=decision. A 200 decision allows the request; anything else denies it. Place it after any token-issuing step and before the upstream node.

Configuration

KeyTypeDefaultDescription
token_endpointstringRequired. Keycloak token endpoint URL (.../protocol/openid-connect/token).
client_idstringRequired. OAuth client id, sent as the UMA audience.
permissionsarray of strings[]Requested permissions, each resource or resource#scope.
policy_enforcement_modestringENFORCINGENFORCING denies when permissions is empty; PERMISSIVE allows without a callout.
http_method_as_scopebooleanfalseAppend the request method as the scope of each permission.
ssl_verifybooleantrueVerify the endpoint's TLS certificate.
timeoutinteger (ms)3000Callout timeout.
- id: authz
type: authz-keycloak
config:
token_endpoint: https://kc.example.com/realms/myrealm/protocol/openid-connect/token
client_id: my-api
permissions: ["Default Resource#read"]
policy_enforcement_mode: ENFORCING
ssl_verify: true
timeout: 3000

Behavior

The bearer token is read from the Authorization header (a missing Bearer prefix is added). The plugin POSTs application/x-www-form-urlencoded body grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&audience=<client_id>&response_mode=decision&permission=<...> to token_endpoint, forwarding the caller's token as the Authorization header.

Only a status Keycloak uses to express an access verdict is treated as a verdict:

  • 200 (permissions granted) → success port, request continues.
  • 401 or 403 (Keycloak evaluated the request and refused it), a missing bearer token, or an empty permissions list under ENFORCING → deliberate rejection, exits on the denied port:
    • context.response.status_code = 403
    • Body: {"error":"access_denied","error_description":"not_authorized"}
  • Any other status5xx, or a 4xx that means the request to Keycloak was wrong (400 invalid_grant, a 404 from a misconfigured token_endpoint path) — and any genuine callout failure (endpoint unreachable, timed out, untransportable) → the node never obtained a decision, so it exits on the ordinary error port instead (a 502 {"error": "provider_error", "message": "<reason>"} response — not the 403 denied shape — with error code AUTHZ_KEYCLOAK_ERROR appended to context.errors). Reporting a broken deployment as a 403 would hide it behind a plausible-looking denial.
Breaking change

Before v0.8 this provider-failure response reused the denied shape (403 {"error": "access_denied"}), so an unreachable Keycloak was indistinguishable from a denied permission. It is now the shared 502 {"error": "provider_error", "message": "<reason>"} response with no challenge header, the same shape every provider-backed auth plugin prepares (openid-connect, cas-auth, ldap-auth, authz-keycloak, authz-casdoor). Match on the error port / the error code, or on the 502, instead of the old status.

With an empty permissions list, ENFORCING denies and PERMISSIVE allows (no callout).

Limitations

The plugin covers the static-permission UMA check. The following features are intentionally not supported:

  • Discovery. Endpoints are not resolved from a discovery URL; token_endpoint must be configured directly.
  • lazy_load_paths / resource resolution. No dynamic URI→resource lookups against the resource-registration endpoint, and no service-account (client_credentials) token acquisition. Only statically configured permissions are checked.
  • Password grant. password_grant_token_generation_incoming_uri token minting is not supported.
  • Caching & redirects. No discovery/token caching and no access_denied_redirect_uri (307) redirect — denials are always 403.
  • Error status normalization. Keycloak's own 401/403 decision statuses are normalized to 403 on the denied port; the plugin does not relay Keycloak's status verbatim.

Ports

authz-keycloak declares three output ports: success, denied (a 403 rejection is prepared — missing bearer token, no permission configured under ENFORCING, or Keycloak refusing the UMA decision with a 401/403), and error (the callout failed, or the token endpoint answered with a status that is not a decision — see Behavior above). denied is a mandatory port, same as success: the policy compiler rejects any policy that leaves it unwired.

edges:
- from: authz-keycloak.success
to: upstream.in
- from: authz-keycloak.denied
to: client.in

Errors

The node returns the Context with an error, so the graph engine routes through the error port and appends the error to context.errors. The status below is the one prepared on context.response; wire error to client (or an error-handler) for the caller to see it.

CodeStatusWhen
AUTHZ_KEYCLOAK_ERROR502The UMA permission callout to Keycloak failed, or returned an unexpected status.