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

IDENTITY FUNDAMENTALS · LAB

Protect, read, validate and replay credentials in your tenant

Inspect your tenant's TLS connection, see why a password can only be set, rotate a leaked client secret, decode a token and then really validate it, and watch a replayed code revoke what it issued.

ReadyUses your lab tenant

The lesson

Builds on: How applications communicate.

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. See a breached password refused

    Recorded as tenant.users.credentials.set rejected.

  2. Present a wrong client secret

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

  3. Rotate the leaked client secret

    Recorded as tenant.oauth.credentials.rotate succeeded.

  4. Redeem the same authorization code twice

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

  5. See a token refused after replay, expiry or misuse

    Recorded as oidc.userinfo rejected (invalid_token).

Setup

You need Ava, Ben, lab-printer and the toolkit. Every "wrong" token in this lab is a real token your tenant issued that legitimately fails a check; nothing is forged or altered.

  1. On this lab page choose Lab Photos and press Start.

  2. Clients > Create client with the machine-to-machine preset: name lab-print-orders, confidential, grant client credentials, scope prints.create. This is the printer's back-office job.

  3. Access Token Management > create a manager named lab-tmp-short: JWT format, an ES256 key, lifetime 60 seconds. Do not assign it yet.

  4. Set the variables and helpers from the How applications communicate lab, plus the job's credentials. Each secret is shown once.

ISSUER=https://tenant-<id>.beyondthelogin.dev
HOST=${ISSUER#https://}
CLIENT_ID=<lab-printer client ID>
read -rs CLIENT_SECRET
ORDERS_ID=<lab-print-orders client ID>
read -rs ORDERS_SECRET
REDIRECT=http://127.0.0.1:8765/callback
authz() { echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(node -p 'encodeURIComponent(process.argv[1])' "$REDIRECT")&scope=$1&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"; }
redeem() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code --data-urlencode "code=$CODE" --data-urlencode "redirect_uri=$REDIRECT" -d code_verifier="$VERIFIER" "$ISSUER/oauth/token"; }
fresh() { eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; }

Walkthrough

  1. Protect data in transit. Look at the TLS connection to your tenant:

curl -sv -o /dev/null "$ISSUER/.well-known/openid-configuration" 2>&1 | grep -E 'SSL connection|subject:|issuer:|expire date|subjectAltName|verify ok'

Expected: a TLS 1.3 or 1.2 connection, a certificate whose subject alternative name covers $HOST, and SSL certificate verify ok.

Why it matters: HTTPS authenticated the server for this hostname and protected the connection. It says nothing about what the server does with data once it arrives. The Certificates and PKI labs open the certificate up.

  1. Store and handle secrets. On Ben's record choose Set password and try Password123!. It is refused because the password has appeared in a data breach. See how such a check can work without sending the password anywhere:

H=$(printf %s 'Password123!' | openssl sha1 | awk '{print toupper($NF)}')
curl -s "https://api.pwnedpasswords.com/range/${H:0:5}" | grep "${H:5}"

Only the first five characters of the hash leave your machine. Use only this sample password here, never a real one. Then set a strong password for Ben and store it.

Why it matters: the tenant stores a salted password hash, which supports comparison but cannot be turned back into the password. That is why the portal can set a password and never show one.

  1. Use the job's secret, then a wrong one:

curl -s -u "$ORDERS_ID:$ORDERS_SECRET" -d grant_type=client_credentials -d scope=prints.create "$ISSUER/oauth/token" | jq '{token_type, scope}'
curl -s -u "$ORDERS_ID:not-the-secret" -d grant_type=client_credentials "$ISSUER/oauth/token" | jq

Expected: a token, then 401 with "error":"invalid_client".

  1. Suppose the secret appeared in a shared screenshot. Keep the old value briefly, then Clients > lab-print-orders > Rotate secret and store the new one:

OLD_SECRET=$ORDERS_SECRET
read -rs ORDERS_SECRET
curl -s -u "$ORDERS_ID:$OLD_SECRET" -d grant_type=client_credentials "$ISSUER/oauth/token" | jq .error
unset OLD_SECRET

Expected: "invalid_client" for the old secret.

Why it matters: a leaked secret must be invalidated or replaced, not just deleted from the latest copy of a file. Here the old secret stops working at once.

  1. Reading. Run fresh; authz openid%20profile, open the URL, sign in as Ava, and redeem the code:

CODE=<code from the callback>
redeem > tokens.json
TOKEN=$(jq -r .access_token tokens.json); ID_TOKEN=$(jq -r .id_token tokens.json)
btl-lab decode "$TOKEN"

Expected: a header with "alg":"ES256", a kid and "typ":"at+jwt", and a readable payload with iss, sub, aud ($ISSUER/resource), exp, scope and client_id. The toolkit reminds you that decoding is not validating.

Why it matters: decoding needs no key. A signed token is readable by anyone who holds it.

  1. Validating:

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

Expected: each check passes and is listed separately: the key from the issuer's JWKS, the allowed algorithm, the signature, typ, iss, aud and the time checks.

Why it matters: validation is a trusted key from the issuer's key set plus issuer, audience, type and time checks, each one a separate question.

  1. A real token from somewhere else. If you set up Lab Mail in the Trust across systems lab, sign in as Lab Mail's Ava through $ISSUER2/token-decoder and keep that access token as OTHER with read -rs OTHER. Then:

btl-lab verify "$OTHER" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $OTHER" "$ISSUER/oidc/userinfo"

Expected: the key is not found in your issuer's set, iss does not match, and your tenant's UserInfo returns 401. Without Lab Mail, run the same verify command on $ID_TOKEN instead: the signature passes, while typ and aud fail.

Why it matters: the token is perfectly readable and correctly signed by its own issuer, yet it is not acceptable here. Reading it told you nothing about whether to trust it.

  1. Limit reuse. Run fresh; authz openid, sign in as Ava, and redeem the code twice:

CODE=<new code>
AT1=$(redeem | jq -r .access_token)
redeem | jq
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $AT1" "$ISSUER/oidc/userinfo"

Expected: the second redemption returns 400 with invalid_grant and the description "The authorization code was already used. Tokens issued from it have been revoked." Then UserInfo returns 401 for AT1.

Why it matters: a code is single use. Reuse looks like theft, so the tenant revokes what the code already produced.

Break it

  1. Expiry: assign lab-tmp-short as lab-printer's access token manager. Get a new token as in step 5, wait 61 seconds, and run step 6 against it: the time check fails. UserInfo returns 401.

Why it matters: a short lifetime limits how long a stolen token is useful, and the signature stays valid the whole time.

  1. Wrong token type: curl -si -H "Authorization: Bearer $ID_TOKEN" "$ISSUER/oidc/userinfo" | head -1 returns 401. An ID token is not an access token, even though the same issuer signed both.

Check your work

Press Check my progress. It looks for:

  • tenant.users.credentials.set rejected (the breached password).

  • oauth.token rejected with reason invalid_client for lab-print-orders.

  • tenant.oauth.credentials.rotate succeeded.

  • oauth.token rejected with reason code_replayed for lab-printer.

  • oidc.userinfo rejected with reason invalid_token.

Cleanup

  1. Assign lab-printer back to the default access token manager, then delete lab-tmp-short.

  2. Delete tokens.json and run unset TOKEN ID_TOKEN AT1 OTHER.

  3. Keep lab-print-orders and its current secret, and store Ben's new password.

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