Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

OAUTH 2.0 · LAB

Get a machine-to-machine token and check it every way

Create a confidential client with one exclusive scope, request a token with client credentials, decode it, verify it against the JWKS, then introspect it as the photo API.

ReadyUses your lab tenant

The lesson

Builds on: Trust boundaries.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

Your progress

Press Start before you begin. Only events your tenant records after that count, in the order below. Checking reads your tenant's Audit, so you need Audit read access in it.

  1. Request a client credentials token

    Recorded as oauth.token succeeded for lab-print-orders.

  2. Introspect it as the photo API

    Recorded as oauth.introspect succeeded (other_client_token_found) for lab-photo-api.

  3. A scope outside the registration is refused

    Recorded as oauth.token rejected (invalid_scope) for lab-print-orders.

  4. The client revokes its own token

    Recorded as oauth.revoke succeeded (access_token_found) for lab-print-orders.

  5. A wrong client secret is refused

    Recorded as oauth.token rejected (invalid_client) for lab-print-orders.

  6. A public client cannot act as itself

    Recorded as oauth.token rejected (client_authentication_required) for lab-printer-app.

Request console

Requests in this lab can be sent from this page to your tenant: open one and choose Send. Fill in the values below first. They stay in this page's memory and are gone when you leave; secrets are never stored or sent anywhere except the request you send.

Setup

  1. Press Start on this page.

  2. Open OAuth > Flow policy, tick Client credentials under Allowed grants, and save.

  3. In OAuth > Scopes, confirm that prints.create exists with access Exclusive. The lesson's print-jobs.write plays the same part.

  4. Open OAuth > Clients > Create client and start from Machine to machine. Name it lab-print-orders, type Confidential, keep Restrict scopes ticked, assign prints.create, tick Enabled and save.

  5. Open Access Token Management > Client authentication secret, choose lab-print-orders and Rotate client secret. The secret is shown once.

  6. Set the shell variables. In this lab CLIENT_ID and CLIENT_SECRET hold lab-print-orders.

export ISSUER="https://tenant-<id>.beyondthelogin.dev"
export CLIENT_ID="<lab-print-orders client ID>"; read -rs CLIENT_SECRET
export API_ID="<lab-photo-api client ID>"; read -rs API_SECRET && export API_SECRET
export APP_ID="<lab-printer-app client ID>"

Walkthrough

  1. Request a token as the client itself. In the request console, type the lab-print-orders client ID and secret when asked.

POST$ISSUER/oauth/token Open in console
POST $ISSUER/oauth/token HTTP/1.1
Authorization: Basic base64($CLIENT_ID:$CLIENT_SECRET)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=prints.create

From a shell:

RESPONSE=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=client_credentials -d scope=prints.create "$ISSUER/oauth/token")
echo "$RESPONSE" | jq '{token_type, expires_in, scope, refresh_token}'
TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

The answer has token_type: "Bearer", an expires_in, scope: "prints.create" and no refresh token.

Why it matters: there is no code, redirect URI or verifier, because there was no browser interaction to connect to. The client's authentication is the whole request, and it can simply repeat it, so a refresh token would add risk without use.

  1. Decode it.

btl-lab decode "$TOKEN"

The header shows alg: ES256, typ: at+jwt and a kid. The payload shows sub and client_id both equal to $CLIENT_ID, scope: "prints.create", aud: "$ISSUER/resource", iat, exp and jti. The toolkit reminds you that decoding is not validating.

Why it matters: the token represents the printer, not a person. Nothing in it can tell an API which user approved anything, because no user did.

  1. Validate it against the issuer's published keys.

btl-lab verify "$TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt

It fetches the JWKS, finds the key with the token's kid, and prints each check: signature, alg allowlist, typ, iss, aud, and exp, iat and nbf with clock skew. All pass.

  1. Ask the authorization server, as the photo API. The console asks for the lab-photo-api client ID and secret.

POST$ISSUER/oauth/introspect Open in console
POST $ISSUER/oauth/introspect HTTP/1.1
Authorization: Basic base64($API_ID:$API_SECRET)
Content-Type: application/x-www-form-urlencoded

token=$TOKEN

The shell equivalent is btl-lab introspect "$TOKEN". Both answer active: true with client_id equal to $CLIENT_ID, the same sub and scope: "prints.create".

Why it matters: local validation proves where the token came from and that it is in date. Introspection also tells the API whether the authorization server still considers it active.

  1. Reuse it rather than requesting a new one for every job. Take exp from step 2's output: echo $(( <exp> - $(date +%s) )) shows the seconds left. The evening batch keeps this token until shortly before then.

  1. Keep the access narrow. Repeat step 1's shell request with different scopes.

    • -d scope=photos.read answers invalid_scope: a restricted client may use only the exclusive scopes assigned to it.

    • -d scope=openid answers invalid_scope, "...The client credentials grant has no user, so it cannot request openid."

    • No scope parameter answers 200 with no scope in the response: the token grants no scope at all.

  1. Use the token at an API that checks scopes. In a second terminal run btl-lab resource --mode introspect, then submit a print job and try to read photos.

curl -s -i -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"order":"book-5561"}' http://127.0.0.1:8766/prints
curl -s -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8766/photos

The print job is accepted. Reading photos answers 403 with Bearer error="insufficient_scope".

Why it matters: the API decides what this partner may do from the access granted to the client. Anyone holding the printer's secret could get the same tokens, so the client should hold only the scopes its job needs.

  1. Revoke the token as its own client: curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d "token=$TOKEN" "$ISSUER/oauth/revoke". btl-lab introspect "$TOKEN" now returns {"active": false}, while btl-lab verify still passes: a resource server that checks only locally keeps accepting it until exp.

  1. Optional, opaque tokens. In Access Token Management, create a temporary manager lab-tmp-opaque with Token format Opaque reference token, assign it to lab-print-orders, and request again. btl-lab decode cannot read the token; introspection is the only way to learn what it means. Assign Default access tokens back to lab-print-orders.

Break it

  1. A wrong secret: curl -s -i -u "$CLIENT_ID:wrong-secret-wrong-secret-wrong-secret" -d grant_type=client_credentials "$ISSUER/oauth/token" answers 401, WWW-Authenticate: Basic realm="OAuth token", {"error":"invalid_client",...}.

  2. A public client: curl -s -d grant_type=client_credentials -d "client_id=$APP_ID" "$ISSUER/oauth/token" answers 400 unauthorized_client, "Public clients cannot authenticate, so they cannot use this grant or endpoint."

  3. Tenant policy: in Flow policy, untick Client credentials and save, then repeat step 1. The answer is 400 unauthorized_client, because the tenant no longer allows the grant for anyone.

Restore: in Flow policy, tick Client credentials again and save.

Check your work

Press Check my progress. In tenant Audit, the successful oauth.token shows actor lab-print-orders with actor type OAuth client, and its subject is the client itself. You can also find tenant.oauth.clients.create, tenant.oauth.credentials.rotate and two tenant.oauth.policy.update events.

Cleanup

  1. Delete lab-tmp-opaque if you created it, after confirming lab-print-orders uses Default access tokens.

  2. Keep lab-print-orders, its secret and Client credentials in Flow policy. The Clients labs use them.

  3. Stop the resource server.

Back to all labs

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

The Lab