External Authorization (Beta)
| Block name | Context | Label |
|---|---|---|
beta_external_authz | Definitions Block | ⚠ required |
The beta_external_authz block lets you delegate the authorization decision for client
requests to an external service. Like all access control
types, the beta_external_authz block is defined in the
definitions block and can be referenced in all
configuration blocks by its required label.
For every protected request Couper sends a POST request with a JSON body describing the
client request to the configured authorization service:
{
"client_request": {
"method": "GET",
"url": "https://couper.example.com/protected",
"headers": {
"Authorization": ["Bearer ..."]
}
}
}
With include_tls = true the TLS connection information of the client request is added. In a
client-facing mTLS setup the client_certificate carries the fields an authorization service
keys on — this is the full payload such a service can expect:
{
"client_request": { "...": "..." },
"metadata_tls": {
"version": "TLS 1.3",
"cipher_suite": "TLS_AES_128_GCM_SHA256",
"server_name": "couper.example.com",
"client_certificate": {
"subject": "CN=my-client,O=Example",
"issuer": "CN=my-ca",
"serial_number": "1267",
"fingerprint_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"not_before": "2026-01-01T00:00:00Z",
"not_after": "2027-01-01T00:00:00Z",
"dns_names": ["client.example"],
"uris": ["spiffe://example.org/mcp-client"],
"email_addresses": ["mcp@example.org"],
"ip_addresses": ["10.0.0.7"]
}
}
}
serial_number is hex-encoded and fingerprint_sha256 is the hex SHA-256 of the DER
certificate — use either for allow lists or pinning. The subject alternative names
(dns_names, uris, email_addresses, ip_addresses) appear only when the certificate
carries them and often hold the identity to authorize on, e.g. a SPIFFE ID in uris.
Couper calls the authorization service on the hot path of every protected request, so the
connection to it should be persistent. This is the recommended setup: a (typically local)
authorization service behind a backend with http2 = true — callouts are then multiplexed
over a single persistent HTTP/2 connection instead of paying a round trip per request.
HTTP/2 is negotiated via TLS (ALPN), so http2 applies to https origins. For a trusted
cleartext (http) authorization service, http2_prior_knowledge = true establishes the
same multiplexed HTTP/2 connection without TLS (h2c) — the service must speak HTTP/2.
Without either, Couper still reuses connections (HTTP/1.1 keep-alive), just without
multiplexing.
definitions {
beta_external_authz "authz" {
backend {
origin = "https://localhost:4000"
http2 = true
}
}
}
Couper does not cache authorization decisions: whether a decision may be reused is only known to the authorization service, which can cache internally whenever its decision allows it.
The response status code of the authorization service determines the decision:
| Status | Result |
|---|---|
200 | The request is allowed. |
401 | Denied with error type external_authz_invalid_credentials, default response status 401. |
403 | Denied with error type external_authz_insufficient_permissions, default response status 403. |
| any other | Denied with error type external_authz, default response status 401. |
The 200 response is exposed as the request.context.<label> variable:
the properties of a JSON object body (Content-Type: application/json) — the place for validated
claims, the resolved identity or granted permissions — plus the response headers under
request.context.<label>.headers (lower-cased names, first value, like request.headers).
A malformed JSON body denies the request, as downstream permission checks may rely on this data.
A body property literally named headers is shadowed by the response headers.
An upstream backend can trust a resolved identity or a re-signed internal token (created with
jwt_sign()) the authorization service returns as a header, by
copying it onto the request with set_request_headers — which overwrites any client-provided
value:
api {
endpoint "/**" {
access_control = ["authz"]
proxy {
backend = "protected_api"
set_request_headers = {
x-resolved-identity = request.context.authz.headers["x-resolved-identity"]
}
}
}
}
With permissions_property the authorization service can grant permissions
evaluated by required_permission in api or endpoint
blocks: the named response body property — a space-separated string or a list of strings, like the
jwt block’s permissions_claim — is added to request.context.granted_permissions.
If the configured property is absent from the 200 response, the request is denied rather than allowed
without permissions, matching the fail-closed handling of a malformed body.
definitions {
beta_external_authz "authz" {
url = "https://authz.example.com/check"
permissions_property = "granted_permissions"
}
}
On a 401 response the authorization service’s WWW-Authenticate challenge — for example an
RFC 9728 resource_metadata pointer for OAuth 2.0 clients — is forwarded to the client by a
default error_handler, and its value is available to custom handlers as
request.context.<label>.www_authenticate. Defining an
error_handler block for
external_authz_invalid_credentials replaces the default:
definitions {
beta_external_authz "authz" {
url = "https://authz.example.com/check"
error_handler "external_authz_invalid_credentials" {
set_response_headers = {
www-authenticate = "Bearer resource_metadata=\"https://couper.example.com/.well-known/oauth-protected-resource\""
}
}
}
}
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
backend | string | - | References a backend in definitions for the authorization callout. Mutually exclusive with backend block. |
custom_log_fields | object | - | Log fields for custom logging. Inherited by nested blocks. |
include_tls | bool | false | Include TLS connection information of the client request in the authorization request. |
permissions_property | string | - | Name of the response body property containing the granted permissions. The property value must either be a string containing a space-separated list of permissions or a list of string permissions. |
url | string | - | URL of the authorization service. Relative URL references are resolved against the origin of a referenced or nested backend block. |
Nested Blocks
| Name | Description |
|---|---|
backend | Configures a backend for the authorization callout (zero or one). Mutually exclusive with backend attribute. |
error_handler | Configures an error handler (zero or more). |