Skip to main content

Protect APIs on Envoy with ThunderID

This guide explains how to secure your APIs at the Envoy gateway layer using tokens issued by ThunderID. By retrieving public signing keys from the ThunderID JWKS endpoint, Envoy cryptographically verifies every incoming token at the edge before forwarding authorized requests to your backend services.

ApplicationThunderIDEnvoyBackend API Request Access token Issue Access token API request+ Bearer token Call JWKS endpoint to validate token signature Forward request

The guide covers three integration approaches available in Envoy:

  • Token Validation: Configure Envoy to validate incoming JWTs issued by ThunderID using the envoy.filters.http.jwt_authn HTTP filter, ensuring only authenticated requests access your APIs.
  • Scope-Based Authorization: Use scopes issued by ThunderID and Envoy's RBAC filter to enforce route-level access control on upstream APIs.
  • AuthZEN PDP Authorization: Call the ThunderID AuthZEN policy decision point (PDP) through an Envoy authorization adapter for request-time authorization decisions. Role and permission changes can take effect without waiting for an existing access token to expire.

Prerequisites

Before you begin, ensure you have the following:

  • A running ThunderID installation. Follow Get ThunderID for download, setup, and start commands.
  • The latest stable Envoy version installed.
  • A backend REST API service running and accessible to Envoy. The examples use http://localhost:9000.
  • curl available in your terminal.

The examples use the following local ports:

PortComponentUsed for
8080Envoy HTTP listenerClient-facing API gateway listener
8081Envoy gRPC listenerClient-facing listener for the gRPC ext_authz example
8090ThunderIDConsole, token endpoint, JWKS endpoint, and AuthZEN PDP
8181HTTP authorization adapterEnvoy HTTP ext_authz checks
8182gRPC authorization adapterEnvoy gRPC ext_authz checks
9000Backend REST APIUpstream service that Envoy forwards allowed requests to
9901Envoy adminEnvoy administration interface

Verify your Envoy version:

envoy --version

Configure JWT Token Validation in Envoy

JWT Token Validation is the foundation of API security with Envoy and ThunderID. In this section, you register an application in ThunderID to obtain credentials, configure Envoy to validate tokens using the ThunderID JWKS endpoint, and verify that unauthenticated requests are rejected.

Configure ThunderID

Create an Application

  1. Sign in to the ThunderID Console at https://<THUNDER_HOST>:<THUNDER_PORT>/console.
  2. Navigate to ApplicationsNew Application.
  3. Enter an Application Name.
  4. Select Backend Service as the application type.
  5. Click Create Application.
  6. Note the Client ID and Client Secret from the application details page.

Obtain an Access Token

Use the client credentials grant to request an access token. The examples use HTTP Basic authentication for the client credentials:

curl -kL -X POST 'https://<THUNDER_HOST>:8090/oauth2/token' \
--user '<CLIENT_ID>:<CLIENT_SECRET>' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials'

Save the returned access_token. You will use it in the Try It Out section.

Configure Envoy

Create an API

Create an Envoy configuration file. See the Envoy configuration overview for a full reference.

The listener defines the port Envoy exposes to clients. The route configuration defines the paths Envoy exposes. The cluster configuration defines the upstream service.

This example exposes Envoy on port 8080 and forwards requests for /resources to an upstream API on http://localhost:9000.

static_resources:
listeners:
- name: local_listener
address:
socket_address:
address: 127.0.0.1
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: resources_api_gateway
route_config:
name: local_route
virtual_hosts:
- name: resources_api
domains: ["*"]
routes:
- match:
prefix: "/resources"
route:
cluster: upstream_api
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

clusters:
- name: upstream_api
connect_timeout: 1s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: upstream_api
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 9000

Start Envoy to verify the API is reachable before adding authentication:

envoy --mode validate -c envoy.yaml
envoy -c envoy.yaml

Envoy starts on port 8080. Verify the endpoint responds:

curl -i --location 'http://localhost:8080/resources' \
--header 'Content-Type: application/json'

Expected response:

HTTP/1.1 200 OK

Add JWT Validation

Envoy validates JWTs using the jwt_authn HTTP filter. Envoy fetches public keys from the ThunderID JWKS endpoint and uses the keys to verify the signature on incoming tokens.

Add the jwt_authn filter before the router filter:

http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
thunderid:
issuer: "https://localhost:8090"
remote_jwks:
http_uri:
uri: "https://127.0.0.1:8090/oauth2/jwks"
cluster: thunderid
timeout: 5s
cache_duration: 300s
from_headers:
- name: authorization
value_prefix: "Bearer "
rules:
- match:
prefix: "/resources"
requires:
provider_name: thunderid
- match:
prefix: "/"
requires:
allow_missing: {}
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

Add a cluster for the ThunderID JWKS endpoint:

clusters:
- name: thunderid
connect_timeout: 1s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: thunderid
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8090
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: localhost
common_tls_context:
tls_params:
tls_minimum_protocol_version: TLSv1_3
tls_maximum_protocol_version: TLSv1_3
validation_context:
trust_chain_verification: ACCEPT_UNTRUSTED
Local development only

trust_chain_verification: ACCEPT_UNTRUSTED skips certificate chain verification for local development with self-signed certificates. Remove this setting in production and configure Envoy with a certificate authority trusted by your deployment.

Configuration reference -- see the full list of options in the JWT authentication filter docs:

FieldValueDescription
issuerhttps://<THUNDER_HOST>:<THUNDER_PORT>Token issuer expected by Envoy
remote_jwks.http_uri.urihttps://<THUNDER_HOST>:<THUNDER_PORT>/oauth2/jwksThunderID JWKS endpoint for fetching public keys
cache_duration300sCaches JWKS keys to avoid fetching them on every request
from_headersAuthorization: BearerReads the JWT from the Authorization header
trust_chain_verificationACCEPT_UNTRUSTEDSkips TLS certificate chain verification. Use only in local development.

Restart Envoy to apply the changes:

pkill -f "envoy -c envoy.yaml"
envoy --mode validate -c envoy.yaml
envoy -c envoy.yaml

Try It Out

Request without a token, expect 401 Unauthorized:

curl -i --location 'http://localhost:8080/resources' \
--header 'Content-Type: application/json'

Expected response:

HTTP/1.1 401 Unauthorized

Jwt is missing

Request with a valid ThunderID token, expect 200 OK:

curl -i --location 'http://localhost:8080/resources' \
--header 'Authorization: Bearer <ACCESS_TOKEN>' \
--header 'Content-Type: application/json'

Expected response:

{
"resources": [
{
"id": "101",
"name": "Resource One",
"status": "active"
},
{
"id": "102",
"name": "Resource Two",
"status": "pending"
}
]
}

Configure Scope-Based Authorization in Envoy

JWT authentication confirms that a token is valid and was issued by ThunderID. It does not control what the token holder is allowed to do. With the configuration from the previous section, Envoy accepts any valid ThunderID token for the protected route.

Scope-based authorization adds a second layer of control. ThunderID embeds scopes in tokens based on the roles assigned to an application. Envoy then checks those scopes on every request and rejects tokens that do not carry the required permissions. This lets you enforce scope-based authorization, for example, restricting a read endpoint to tokens with resource:read, without changing the upstream service.

The steps below build on the JWT authentication configuration from the previous section. You will define a Resource Server, a Resource, and actions in ThunderID to model your API permissions. Then bundle them into a role, assign the role to your application, and update the Envoy configuration to enforce scope validation.

Configure ThunderID

Create a Resource Server

A Resource Server represents your API in ThunderID. The Resource Server groups all resources and actions under a single identifier and acts as the audience for tokens issued to clients of that API. For a full reference, see Resource Servers.

Using the Console:

  1. Sign in to the ThunderID Console at https://<THUNDER_HOST>:<THUNDER_PORT>/console.
  2. Navigate to Resource ServersNew Resource Server.
  3. Fill in the following fields:
    • Name: Resources API
    • Identifier: https://api.example.com/resources
    • Description: Handles resource operations
    • Delimiter: : (colon)
  4. Click Create.
  5. Note the Resource Server ID from the details page.

Create a Resource

A Resource represents a specific entity within the API, such as /resources. Resources let you model your API surface so that permissions can be granted at a granular level.

Using the Console:

  1. Open the Resources API Resource Server.
  2. Go to the Resources tab and click New Resource.
  3. Fill in the following fields:
    • Name: Resources
    • Handle: resource
  4. Click Create.

Create Resource Actions

Actions define the operations allowed on a resource. Each action produces a scope in the format <resource-handle>:<action-handle>. ThunderID includes these scopes in tokens issued to applications that hold the corresponding role.

Using the Console:

  1. Under the Resources API Resource Server, open the Resources resource.
  2. Go to the Actions tab and click New Action.
  3. Fill in the following fields:
    • Name: Read Resource
    • Handle: read
    • Description: Permission to read resources
  4. Click Create.
  5. Note the generated permission shown in the action details: resource:read.

Create a Role and assign permission

Roles bundle one or more permissions together. Assign roles to applications to control which scopes appear in their tokens.

  1. Navigate to RolesNew Role.
  2. Fill in the following fields:
    • Name: Resources API Reader
  3. Click Continue.
  4. Under Assign permissions, choose resource:read.
  5. Click Continue.

Assign a Role to the Application

Assign the role to the application you created in the previous section. ThunderID includes the role's scopes in every token issued for that application.

Using the Console:

  1. Navigate to Applications and open your Backend Service application.
  2. Go to the Roles tab.
  3. Click Assign Role, select Resources API Reader from the list, and confirm.

Configure Envoy

Add claim_to_headers to the jwt_authn provider you configured in the previous section, then use the Envoy RBAC filter to enforce the required scope. See the JWT authentication filter docs and RBAC filter docs for the full list of options.

Envoy copies the JWT scope claim into a request header. The required scope can then be checked before Envoy forwards the request.

Then add scope validation before the router filter:

http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
thunderid:
issuer: "https://localhost:8090"
remote_jwks:
http_uri:
uri: "https://127.0.0.1:8090/oauth2/jwks"
cluster: thunderid
timeout: 5s
cache_duration: 300s
from_headers:
- name: authorization
value_prefix: "Bearer "
claim_to_headers:
- header_name: x-jwt-scope
claim_name: scope
rules:
- match:
prefix: "/resources"
requires:
provider_name: thunderid
- match:
prefix: "/"
requires:
allow_missing: {}
- name: envoy.filters.http.rbac
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
rules:
action: ALLOW
policies:
resources-read:
permissions:
- and_rules:
rules:
- url_path:
path:
prefix: "/resources"
- header:
name: ":method"
string_match:
exact: "GET"
- header:
name: "x-jwt-scope"
string_match:
safe_regex:
regex: "(^|\\s)resource:read(\\s|$)"
principals:
- any: true
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

Add one scope policy for each protected route or operation. For example, an update endpoint can require a separate scope such as resource:update.

Scope configuration reference:

FieldValueDescription
claim_to_headers.claim_namescopeJWT claim that stores scopes
claim_to_headers.header_namex-jwt-scopeHeader Envoy uses to check the token scopes
string_match.safe_regex.regex(^|\\s)resource:read(\\s|$)Required scope to access the matching route. The regular expression matches scope token boundaries in a space-delimited scope claim.
actionALLOWAllows only requests that match one of the configured scope policies

Restart Envoy to apply the changes:

pkill -f "envoy -c envoy.yaml"
envoy --mode validate -c envoy.yaml
envoy -c envoy.yaml

Try It Out

Obtain an Access Token with the Required Scope

Request a token for the application that has the role assigned:

curl -kL -X POST 'https://<THUNDER_HOST>:8090/oauth2/token' \
--user '<CLIENT_ID>:<CLIENT_SECRET>' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=resource:read'

The response confirms the scope is present:

{
"access_token": "<ACCESS_TOKEN>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "resource:read"
}

Call the Protected API

With a token that has the required scope, expect 200 OK:

curl -i --location 'http://localhost:8080/resources' \
--header 'Authorization: Bearer <ACCESS_TOKEN>' \
--header 'Content-Type: application/json'

With a token missing the required scope, expect 403 Forbidden:

curl -i --location 'http://localhost:8080/resources' \
--header 'Authorization: Bearer <TOKEN_WITHOUT_REQUIRED_SCOPE>' \
--header 'Content-Type: application/json'

Expected response:

HTTP/1.1 403 Forbidden

RBAC: access denied

Configure AuthZEN PDP Authorization in Envoy

ApplicationThunderIDAuthZEN PDPEnvoyProtectedResource Server API request+ Bearer token Request service token Issue service token AuthZEN request + service token Allow / deny Forwardif allowed 403 if denied

JWT authentication and scope-based authorization make decisions from token contents. AuthZEN PDP authorization lets Envoy ask ThunderID for a policy decision on each request. Use this pattern when the gateway must evaluate the subject, resource, action, and request context before it forwards traffic to the upstream service.

For endpoint behavior and standalone PDP examples, see Policy Decision Point.

Because Envoy requests a new decision for each request, changes to a subject's roles or permissions can affect authorization without waiting for an existing access token to expire.

ThunderID currently exposes the AuthZEN PDP over HTTP. Envoy can integrate with the PDP in two ways:

  • HTTP adapter: Envoy calls an HTTP authorization adapter through the ext_authz filter. The adapter converts the Envoy authorization request into an AuthZEN access evaluation request.
  • gRPC adapter: Envoy calls an adapter that implements the Envoy external authorization gRPC service. The adapter still calls the ThunderID AuthZEN PDP over HTTP.

In both patterns, Envoy validates the JWT and delegates the authorization check to an adapter. The adapter builds the AuthZEN request, calls the ThunderID PDP, and returns an allow or deny response to Envoy.

Envoy's ext_authz filter delegates the authorization check to an external service. The adapter is required because Envoy does not manage the adapter's own service token lifecycle and, in local development, the adapter can handle ThunderID's self-signed TLS setup when calling the PDP.

Configure ThunderID

Before you continue, complete Configure ThunderID in the scope-based authorization section. The AuthZEN PDP evaluates the same Resource Server, Resource, Action, permission, and role assignments.

The adapter sends this AuthZEN request for GET /resources/<RESOURCE_ID>:

{
"subject": {
"id": "<SUBJECT_ID>"
},
"resource": {
"type": "https://api.example.com/resources",
"id": "<RESOURCE_ID>"
},
"action": {
"name": "resource:read"
}
}

The action name must match a permission registered in ThunderID. In this guide, the Resource handle and Action handle produce resource:read; if your adapter sends a different action name, the PDP returns decision: false.

Configure the Authorization Adapter

Run the authorization adapter as a separate service next to Envoy. Envoy sends external authorization checks to the adapter, and the adapter sends AuthZEN access evaluation requests to ThunderID.

Configure the adapter with the ThunderID base URL and the Direct Auth Secret that can invoke the AuthZEN PDP. These credentials authorize the adapter to call /access/v1/evaluation; the API caller token remains the token that Envoy receives in the Authorization header.

Set server.security.direct_auth_secret in the ThunderID deployment configuration and restart ThunderID. Store the same value as THUNDERID_DIRECT_AUTH_SECRET when you start the adapter. A missing or incorrect secret causes ThunderID to reject the PDP call with 401.

Use AUTHZEN_ADAPTER_PORT when you configure Envoy's HTTP ext_authz service. Use AUTHZEN_GRPC_ADAPTER_PORT when you configure Envoy's gRPC ext_authz service.

Local development adapter

The adapter bridges Envoy ext_authz and the ThunderID AuthZEN PDP. It reads the identity and request context forwarded by Envoy, builds an AuthZEN access evaluation request, calls /access/v1/evaluation, and returns allow or deny to Envoy.

For local development, the sample adapter can also handle Direct Auth Secret configuration, self-signed TLS, HTTP and gRPC ext_authz endpoints, health checks, and structured logging.

Set the required environment variables first. Then start the adapter:

python authzen_adapter.py

The adapter exposes:

  • http://localhost:8181/ext-authz/* - Envoy HTTP ext_authz endpoint
  • grpc://localhost:8182 - Envoy gRPC ext_authz endpoint
  • http://localhost:8181/health - health check

Use the following configuration for the adapter. The minimized snippets below use the core settings. A fuller local adapter can also use the optional transport and gRPC startup settings.

  • Base URL setting. Required.
  • Direct Auth Secret: THUNDERID_DIRECT_AUTH_SECRET. Required.
  • TLS verification: THUNDERID_TLS_VERIFY.
  • Local transport switch: THUNDERID_HTTP_TRANSPORT.
  • HTTP adapter port: AUTHZEN_ADAPTER_PORT. Defaults to 8181.
  • gRPC adapter port: AUTHZEN_GRPC_ADAPTER_PORT. Defaults to 8182.
  • gRPC startup switch: AUTHZEN_ENABLE_GRPC.
  • PDP call timeout: AUTHZEN_ADAPTER_TIMEOUT. Defaults to 10.

The adapter can be implemented in any language. It must do four things:

  1. Read the authenticated subject from the claims Envoy forwards after JWT validation.
  2. Map the incoming method and path to the permission name that ThunderID evaluates.
  3. Call the ThunderID AuthZEN PDP at /access/v1/evaluation with the Direct Auth Secret.
  4. Return allow to Envoy only when the PDP response contains decision: true.

The request mapping and PDP call can look like this. The example omits subject.type because the AuthZEN field is optional and the subject can be a user, application, or agent:

import json
import os
import ssl
import urllib.request

base_url = os.environ["THUNDERID" + "_BASE" + "_URL"].rstrip("/") # URL.
direct_auth_secret = os.environ["THUNDERID_DIRECT_AUTH_SECRET"] # Direct Auth Secret.
tls_verify = os.environ.get("THUNDERID_TLS_VERIFY", "false").lower() == "true" # TLS.
timeout = float(os.environ.get("AUTHZEN_ADAPTER_TIMEOUT", "10")) # Timeout.


def tls_context():
if tls_verify:
return ssl.create_default_context()
return ssl._create_unverified_context()


def resource_id_from_path(path):
return path.rsplit("/", 1)[-1]

def action_for(method, path):
if method == "GET" and path.startswith("/resources"):
return "resource:read"
return "resource:unknown"

def build_authzen_request(method, path, headers):
subject = {"id": headers.get("x-authenticated-subject", "").strip()}
if not subject["id"]:
raise ValueError("Envoy must provide x-authenticated-subject from a validated JWT")

properties = {}
client_id = headers.get("x-authenticated-client-id", "").strip()
if client_id:
properties["client_id"] = client_id

for name, value in headers.items():
lower_name = name.lower()
if lower_name.startswith("x-authzen-property-") and value:
properties[lower_name.removeprefix("x-authzen-property-")] = value

if properties:
subject["properties"] = properties

return {
"subject": subject,
"resource": {
"type": "https://api.example.com/resources",
"id": resource_id_from_path(path),
},
"action": {
"name": action_for(method, path),
},
}


def evaluate_with_thunderid(authzen_request):
body = json.dumps(authzen_request).encode()
request = urllib.request.Request(
f"{base_url}/access/v1/evaluation",
data=body,
method="POST",
headers={
"content-type": "application/json",
"Direct-Auth-Secret": direct_auth_secret,
},
)

with urllib.request.urlopen(
request, timeout=timeout, context=tls_context()
) as response:
return bool(json.loads(response.read()).get("decision"))

Then expose the adapter endpoint that matches the Envoy ext_authz protocol you configured:

For HTTP ext_authz, Envoy calls the adapter over HTTP. Return 200 to allow the request, or 403 to deny it:

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

EXT_AUTHZ_PREFIX = "/ext-authz"


def route_path_from_ext_authz_path(path):
if path == EXT_AUTHZ_PREFIX:
return "/"
if path.startswith(f"{EXT_AUTHZ_PREFIX}/"):
return path[len(EXT_AUTHZ_PREFIX):]
return path


class HttpAdapter(BaseHTTPRequestHandler):
# Send a JSON response.
def send_json(self, status, payload):
body = json.dumps(payload).encode()
self.send_response(status)
self.send_header("content-type", "application/json")
self.send_header("content-length", str(len(body)))
self.end_headers()
self.wfile.write(body)

def handle_ext_authz(self):
route_path = route_path_from_ext_authz_path(self.path.split("?", 1)[0])

try:
authzen_request = build_authzen_request(self.command, route_path, self.headers)
allowed = evaluate_with_thunderid(authzen_request)
except ValueError as error:
self.send_json(401, {"error": "missing_subject", "message": str(error)})
return
except Exception as error:
self.send_json(502, {"error": "adapter_failed", "message": str(error)})
return

if allowed:
self.send_response(200)
self.send_header("x-authzen-decision", "true")
self.send_header("content-length", "0")
self.end_headers()
return

self.send_json(403, {"error": "access_denied"})

def do_GET(self):
if self.path == "/health":
self.send_json(200, {"status": "ok"})
return
self.handle_ext_authz()

def do_POST(self):
self.handle_ext_authz()

def do_PUT(self):
self.handle_ext_authz()

def do_PATCH(self):
self.handle_ext_authz()

def do_DELETE(self):
self.handle_ext_authz()


if __name__ == "__main__":
port = int(os.environ.get("AUTHZEN_ADAPTER_PORT", "8181"))
ThreadingHTTPServer(("127.0.0.1", port), HttpAdapter).serve_forever()

Configure Envoy

Choose the adapter protocol that Envoy uses for the external authorization check.

Use the listener, route, and upstream cluster from the previous Envoy examples. Replace the listener's http_filters block with the filter chain from the selected tab, and add the matching adapter cluster.

jwt_authn validates the bearer token and copies the sub claim into x-authenticated-subject. The adapter reads this header to build the AuthZEN subject.id. Without this handoff, the adapter has no verified identity to send to the PDP.

Configure Envoy for the HTTP Adapter

Configure Envoy to validate the JWT and call the HTTP adapter. The jwt_authn filter validates the JWT and forwards the claims the adapter needs. The ext_authz filter calls the HTTP adapter after JWT validation and before the router filter:

http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
thunderid:
issuer: "https://localhost:8090"
remote_jwks:
http_uri:
uri: "https://localhost:8090/oauth2/jwks"
cluster: thunderid
timeout: 5s
cache_duration: 300s
from_headers:
- name: authorization
value_prefix: "Bearer "
claim_to_headers:
- header_name: x-authenticated-subject
claim_name: sub
- header_name: x-authenticated-client-id
claim_name: client_id
rules:
- match:
prefix: "/resources"
requires:
provider_name: thunderid
- match:
prefix: "/"
requires:
allow_missing: {}
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
failure_mode_allow: false
status_on_error:
code: ServiceUnavailable
http_service:
server_uri:
uri: http://127.0.0.1:8181
cluster: authzen_ext_authz
timeout: 5s
path_prefix: /ext-authz
authorization_request:
allowed_headers:
patterns:
- exact: x-authenticated-subject
- exact: x-authenticated-client-id
- prefix: x-authzen-property-
authorization_response:
allowed_upstream_headers:
patterns:
- exact: x-authzen-decision
- exact: x-authzen-action
- exact: x-authzen-resource-type
- exact: x-authzen-resource-id
allowed_client_headers:
patterns:
- exact: content-type
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
note

The example uses failure_mode_allow: false. If the adapter is unreachable or returns an error, Envoy returns 503 Service Unavailable instead of forwarding the request without an authorization decision.

Add Envoy clusters for the ThunderID JWKS endpoint and the HTTP adapter:

clusters:
- name: thunderid
connect_timeout: 1s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: thunderid
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8090
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: localhost
common_tls_context:
tls_params:
tls_minimum_protocol_version: TLSv1_3
tls_maximum_protocol_version: TLSv1_3
validation_context:
trust_chain_verification: ACCEPT_UNTRUSTED
- name: authzen_ext_authz
connect_timeout: 1s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: authzen_ext_authz
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8181

If you configure JWT audience validation in Envoy, include the audience used by both user tokens and client credentials tokens that call the protected API.

Try It Out

Start the Adapter and Envoy

Start the authorization adapter with the ThunderID PDP settings:

export THUNDERID_BASE_URL="https://localhost:8090"
export THUNDERID_DIRECT_AUTH_SECRET="<direct-auth-secret>"
export AUTHZEN_ADAPTER_PORT=8181
export AUTHZEN_GRPC_ADAPTER_PORT=8182

python authzen_adapter.py

In another terminal, validate and start Envoy:

envoy --mode validate -c envoy.yaml
envoy -c envoy.yaml

Obtain an Access Token

Use a user token or client credentials token for a subject that has the resource:read permission through the Resources API Reader role.

For a client credentials token, request the corresponding scope from ThunderID:

curl -kL -X POST 'https://<THUNDER_HOST>:8090/oauth2/token' \
--user '<CLIENT_ID>:<CLIENT_SECRET>' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=resource:read'

Save the returned access_token as ACCESS_TOKEN. The token contains the scope associated with the assigned permission.

Call the HTTP listener with a token for a subject that has the required permission:

curl -i --location 'http://localhost:8080/resources' \
--header 'Authorization: Bearer <ACCESS_TOKEN>'

Expected response:

HTTP/1.1 200 OK

Verify the Deny Response

Call the protected API with a token for a subject that does not have the required permission. The token does not contain the corresponding scope:

curl -i --location 'http://localhost:8080/resources' \
--header 'Authorization: Bearer <TOKEN_WITHOUT_REQUIRED_SCOPE>'

Expected response:

HTTP/1.1 403 Forbidden

Explore with AI

ThunderID LogoThunderID Logo

Product

DocsAPIsSDKs
© Copyright Linux Foundation Europe.For web site terms of use, trademark policy and other project policies please see https://linuxfoundation.eu/en/policies.