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.
Sign in to start this lab and check your progress. Log in or create an account.
See a breached password refused
Recorded as
tenant.users.credentials.setrejected.Present a wrong client secret
Recorded as
oauth.tokenrejected (invalid_client) forlab-print-orders.Rotate the leaked client secret
Recorded as
tenant.oauth.credentials.rotatesucceeded.Redeem the same authorization code twice
Recorded as
oauth.tokenrejected (code_replayed) forlab-printer.See a token refused after replay, expiry or misuse
Recorded as
oidc.userinforejected (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.
On this lab page choose Lab Photos and press Start.
Clients > Create client with the machine-to-machine preset: name
lab-print-orders, confidential, grant client credentials, scopeprints.create. This is the printer's back-office job.Access Token Management > create a manager named
lab-tmp-short: JWT format, an ES256 key, lifetime60seconds. Do not assign it yet.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
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.
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.
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".
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.
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.
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.
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-decoderand keep that access token asOTHERwithread -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.
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
Expiry: assign
lab-tmp-shortaslab-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 returns401.
Why it matters: a short lifetime limits how long a stolen token is useful, and the signature stays valid the whole time.
Wrong token type:
curl -si -H "Authorization: Bearer $ID_TOKEN" "$ISSUER/oidc/userinfo" | head -1returns401. 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.setrejected (the breached password).oauth.tokenrejected with reasoninvalid_clientforlab-print-orders.tenant.oauth.credentials.rotatesucceeded.oauth.tokenrejected with reasoncode_replayedforlab-printer.oidc.userinforejected with reasoninvalid_token.
Cleanup
Assign
lab-printerback to the default access token manager, then deletelab-tmp-short.Delete
tokens.jsonand rununset TOKEN ID_TOKEN AT1 OTHER.Keep
lab-print-ordersand its current secret, and store Ben's new password.