OAUTH 2.0 · LAB
Bind an access token to a client certificate
Compute your certificate's x5t#S256 thumbprint, plan a token whose cnf holds it and a resource that compares it, and see today why an unbound client credentials token works for anyone.
PlannedUses your lab tenant
The lesson
Builds on: Authenticating a client with a certificate.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.
- G8 Client authentication beyond `client_secret_basic` and `none`
- G27 PKI / X.509 certificates
- G57 mTLS edge (per-tenant host that requests client certificates)
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
Setup
Keep the CA,
orders.key,orders.crtandORDERS_IDfrom Authenticate the order service with a client certificate. Readlab-print-orders's secret:read -rs ORDERS_SECRET; export ORDERS_SECRET.Add the Base64url helper.
b64url() { base64 | tr '+/' '-_' | tr -d '=\n'; }
Planned (G8): on
lab-tmp-mtls-orders, turn on Certificate-bound access tokens (tls_client_certificate_bound_access_tokens: true) and use an access token manager with thejwtformat.Planned (G3, G57): the sample photo API offers a print jobs resource on the mutual TLS alias host,
$MTLS_RESOURCE/print-jobs.
Planned walkthrough
Compute the thumbprint you expect. This step runs today.
openssl x509 -in orders.crt -outform DER | openssl dgst -sha256 -binary | b64url; echo
Why it matters: "Binding the token to the certificate". x5t#S256 is the SHA-256 hash of the certificate's DER encoding, in Base64url without padding.
Get a token over the alias with the certificate, as in the previous lab.
token_typeis stillBearer. Decode it withbtl-lab decode "$TOKEN":cnf["x5t#S256"]equals step 1. Discovery showstls_client_certificate_bound_access_tokens: true.
Why it matters: mutual TLS adds no new token type. The binding lives in cnf, and the connection the token must travel on.
Introspect it with
btl-lab introspect "$TOKEN". The response carries the samecnf.
Why it matters: an opaque token carries the binding through introspection.
Call the resource over mutual TLS with the same certificate.
curl -s --cert orders.crt --key orders.key "$MTLS_RESOURCE/print-jobs" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"order":"book-5562","product":"hardcover-a4"}'
The job is accepted.
Why it matters: "Checking the certificate at the API". The API compares one thumbprint and leaves chain validation to the authorization server.
Fill in the lesson's DPoP and mutual TLS table from what you saw in this lab and the DPoP labs.
Do today
Run Planned walkthrough step 1, then compare it with OpenSSL's own fingerprint.
openssl x509 -in orders.crt -noout -fingerprint -sha256
It is the same hash, written in hex with colons instead of Base64url.
Get an ordinary client credentials token for
lab-print-orderswith its secret.
TOKEN=$(curl -s -u "$ORDERS_ID:$ORDERS_SECRET" "$ISSUER/oauth/token" -d grant_type=client_credentials -d scope=prints.create | jq -r .access_token)
btl-lab decode "$TOKEN"
The claims have no cnf.
Copy the token to a second environment that never had your certificate or secret, such as another machine or a cloud shell, and introspect it there with
btl-lab introspect "$TOKEN". It isactive. The resource has no way to tell who presented the token: this is the bearer property certificate binding removes.See the rule from "When a proxy ends the TLS connection" from the tenant's side. Send your own certificate in a
Client-Certheader, the way a proxy would forward it.
curl -s "$ISSUER/oauth/token" -H "Client-Cert: :$(openssl x509 -in orders.crt -outform DER | base64 | tr -d '\n'):" \
-d grant_type=client_credentials -d client_id=$ORDERS_ID -d scope=prints.create | jq .
The answer is 401 invalid_client, and Audit shows oauth.token rejected unsupported_auth_method. The tenant gives a client-supplied certificate header no authority at all, which is exactly what the lesson asks of anything behind a proxy.
Break it
These run once the gaps close.
Call the resource with the bound token over the ordinary host, with no certificate. Expect
401withWWW-Authenticate: Bearer error="invalid_token".Call it over mutual TLS with the renewed certificate from the next lab. Expect
401 invalid_token: a different certificate has a different thumbprint.
Check your work
Today, Audit shows oauth.token succeeded for lab-print-orders, the introspection by lab-photo-api, and oauth.token rejected unsupported_auth_method for the header attempt. Once the gaps close, Audit also shows a token issued with an x5t#S256 binding and resource requests accepted and refused.
Cleanup
Revoke the client credentials token:
curl -s -u "$ORDERS_ID:$ORDERS_SECRET" "$ISSUER/oauth/revoke" -d token=$TOKEN.Keep the certificate files for the next lab.
Missing infrastructure
G8 and G27: mutual TLS client authentication, certificate validation and the
tls_client_certificate_bound_access_tokenssetting.G57: the mutual TLS alias host. The edge must remove any client-supplied certificate header, send its own only when a certificate was presented, and the tenant must accept that value only from the edge. These rules come straight from the lesson's proxy section and need their own tests.
G3: a sample resource that compares the connection's certificate thumbprint with
cnf.Once these exist, the Planned walkthrough runs as written.