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
| Key | Type | Default | Description |
|---|---|---|---|
token_endpoint | string | — | Required. Keycloak token endpoint URL (.../protocol/openid-connect/token). |
client_id | string | — | Required. OAuth client id, sent as the UMA audience. |
permissions | array of strings | [] | Requested permissions, each resource or resource#scope. |
policy_enforcement_mode | string | ENFORCING | ENFORCING denies when permissions is empty; PERMISSIVE allows without a callout. |
http_method_as_scope | boolean | false | Append the request method as the scope of each permission. |
ssl_verify | boolean | true | Verify the endpoint's TLS certificate. |
timeout | integer (ms) | 3000 | Callout 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.401or403(Keycloak evaluated the request and refused it), a missing bearer token, or an emptypermissionslist underENFORCING→ deliberate rejection, exits on thedeniedport:context.response.status_code=403- Body:
{"error":"access_denied","error_description":"not_authorized"}
- Any other status —
5xx, or a4xxthat means the request to Keycloak was wrong (400 invalid_grant, a404from a misconfiguredtoken_endpointpath) — 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 (a502{"error": "provider_error", "message": "<reason>"}response — not the403deniedshape — with error codeAUTHZ_KEYCLOAK_ERRORappended tocontext.errors). Reporting a broken deployment as a403would hide it behind a plausible-looking denial.
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_endpointmust 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 configuredpermissionsare checked.- Password grant.
password_grant_token_generation_incoming_uritoken minting is not supported. - Caching & redirects. No discovery/token caching and no
access_denied_redirect_uri(307) redirect — denials are always403. - Error status normalization. Keycloak's own
401/403decision statuses are normalized to403on thedeniedport; 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.
| Code | Status | When |
|---|---|---|
AUTHZ_KEYCLOAK_ERROR | 502 | The UMA permission callout to Keycloak failed, or returned an unexpected status. |