Couper Documentation

v1.14

External Authorization (Beta)

Block nameContextLabel
beta_external_authzDefinitions 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:

StatusResult
200The request is allowed.
401Denied with error type external_authz_invalid_credentials, default response status 401.
403Denied with error type external_authz_insufficient_permissions, default response status 403.
any otherDenied 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

NameTypeDefaultDescription
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

NameDescription
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).