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 SECURITY · LAB

Write a scope, run negative tests against your own tenant and check detection

Write a test scope for your lab tenant, run a small negative test script whose every request should be refused, confirm each refusal is recorded with a reason, and measure how long detection takes.

Partly readyUses your lab tenant

The lesson

Builds on: Reviewing identity security posture.

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

Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.

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. A code redeemed with the wrong verifier is refused

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

  2. A refresh that widens scope is refused

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

  3. Revoking another client's token is refused

    Recorded as oauth.revoke rejected (other_client_token) for lab-photo-api.

  4. A code redeemed twice is refused

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

  5. A wrong client secret is refused

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

  6. A stale session cannot add a sign-in method

    Recorded as account.security rejected (reauthentication_required).

Setup

  1. Press Start on this page.

  2. Write the test scope before sending anything, in the lesson's format:

    • Authorized by: you, as the owner of the Lab Photos tenant.

    • In scope: Lab Photos only, its test people (Ava, Ben, Cora, Mia) and its lab- clients.

    • Out of scope: every other tenant, the BTL sites and services themselves, real people's accounts, and anything that sends many requests or tries to overload a service.

    • Stop and report: anything that looks like real data or real activity you did not cause.

Why it matters: having an account on a service does not authorize testing the service. Every request here is an ordinary request to a tenant you own, with test people, and a handful of requests per test.

  1. Make sure these are in your shell: CLIENT_ID (lab-printer-app), ORDERS_ID and ORDERS_SECRET (lab-print-orders), API_ID and API_SECRET (lab-photo-api).

  2. Define a small helper that compares what you expected with what you got:

expect() { if [ "$2" = "$3" ]; then echo "PASS  $1"; else echo "FAIL  $1: expected $2, got $3"; fi; }

Walkthrough

  1. Test: a code redeemed with the wrong verifier. Run the lab-printer-app authorization flow as Ava (with eval "$(btl-lab pkce)" and eval "$(btl-lab state)"), read the code from the callback page into CODE, then make a fresh pair so the verifier no longer matches, and redeem.

read -rs CODE
eval "$(btl-lab pkce)"
got=$(curl -s -d grant_type=authorization_code -d client_id="$CLIENT_ID" --data-urlencode code="$CODE" \
  --data-urlencode redirect_uri=https://beyondthelogin.dev/lab/callback/ -d code_verifier="$VERIFIER" "$ISSUER/oauth/token" | jq -r .error)
expect "code with wrong verifier" invalid_grant "$got"

Why it matters: most tests confirm the right thing works. This one confirms the wrong thing fails, which is the path an attacker would take.

  1. Run the flow again for code B and redeem it correctly, keeping the tokens. Then test that a refresh cannot widen scope beyond Ava's grant.

read -rs CODE
RESPONSE=$(curl -s -d grant_type=authorization_code -d client_id="$CLIENT_ID" --data-urlencode code="$CODE" \
  --data-urlencode redirect_uri=https://beyondthelogin.dev/lab/callback/ -d code_verifier="$VERIFIER" "$ISSUER/oauth/token")
TOKEN=$(echo "$RESPONSE" | jq -r .access_token); REFRESH=$(echo "$RESPONSE" | jq -r .refresh_token)
got=$(curl -s -d grant_type=refresh_token -d client_id="$CLIENT_ID" --data-urlencode refresh_token="$REFRESH" -d "scope=photos.read photos.write" "$ISSUER/oauth/token" | jq -r .error)
expect "refresh widening scope" invalid_scope "$got"
  1. Test: another client trying to revoke Ava's token, and then the same code redeemed a second time.

got=$(curl -s -u "$API_ID:$API_SECRET" --data-urlencode token="$TOKEN" "$ISSUER/oauth/revoke" | jq -r .error)
expect "revoke another client's token" unauthorized_client "$got"
got=$(curl -s -d grant_type=authorization_code -d client_id="$CLIENT_ID" --data-urlencode code="$CODE" \
  --data-urlencode redirect_uri=https://beyondthelogin.dev/lab/callback/ -d code_verifier="$VERIFIER" "$ISSUER/oauth/token" | jq -r .error)
expect "code redeemed twice" invalid_grant "$got"

Why it matters: each refusal must be specific and must hold when the request goes straight to the endpoint, without any page in front of it.

  1. Test: a wrong client secret for the print job.

got=$(curl -s -u "$ORDERS_ID:not-the-secret" -d grant_type=client_credentials -d scope=prints.create "$ISSUER/oauth/token" | jq -r .error)
expect "wrong client secret" invalid_client "$got"
  1. Test: a stale session adding a sign-in method. Sign in as Cora, wait more than 10 minutes, and try to add an authenticator app at $ISSUER/account/security. Record PASS if the page asks you to confirm it is you.

Why it matters: the lesson's table also lists a fresh session from a code sign-in where policy requires a passkey. Your tenant cannot express that case yet, so it stays as a written test that currently has no control to pass.

  1. Check what each refusal left behind. In Audit, source Protocol activity, filter rejected and find one event per test, each with its reason: pkce_failed, scope_not_granted, other_client_token, code_replayed, invalid_client and reauthentication_required. Open one and confirm it holds no secret, code or token.

Why it matters: a refusal is worth checking for what it records as well as what it returns. A refusal that leaves no trace cannot be investigated.

  1. Detection test. Re-run your Rule 2 from the detection lab against step 5's events: did Cora sign in and add a method within 10 minutes? Write down how long it took you to notice by reading Audit by hand.

Why it matters: detection breaks silently. Measuring time to notice, every time you run the tests, shows whether it is getting faster or quietly stopped working.

  1. Rehearse the incident: spend 15 minutes on a tabletop with the lesson's scenario applied to Ava. Who in your tenant can lock her, reset her methods and revoke her app's tokens, and how long does each take? Add the complication: the password was reset, and the attacker is back an hour later. Write each gap with an owner and a date.

Why it matters: some parts of a response depend on people knowing what to do. A step only one person knows how to do is a finding, recorded like any other.

Break it

  1. Change one expectation to the wrong value, for example expect "wrong client secret" access_token_found "$got", and run step 4 again. The helper prints FAIL. A test that cannot fail proves nothing; correct the expectation and run it once more.

Check your work

  • Check my progress confirms all six refusals, in the order you ran them.

  • Every line your script printed says PASS, and your notes record step 5's result, the detection time and the tabletop gaps.

Cleanup

  1. Revoke Ava's remaining lab tokens and clear the shell: unset CODE TOKEN REFRESH RESPONSE.

  2. Keep the scope and the script; run them again after any change to the tenant's settings.

Missing infrastructure

  • G36: there are no alerts, so detection time is the time it takes you to read Audit. Once it exists, the lab will repeat step 5 and measure the time until the alert arrives.

  • G38: the tenant cannot require a passkey confirmation for sensitive changes, so the lesson's "fresh session from a code sign-in" case has no control to test. Once it exists, the test will expect a passkey prompt for Cora's new method.

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