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

End every step-up attempt cleanly, with photos deleted or clearly kept

Build a delete action that steps up at most once, never refreshes to satisfy a challenge, keeps everyday and stepped-up tokens apart, and tells Ava plainly when photos were not deleted.

Partly readyUses your lab tenant

The lesson

Builds on: Following a step-up challenge.

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. Refresh the everyday token

    Recorded as oauth.token succeeded for lab-printer-app.

  2. Step up once for a deletion Ava chose

    Recorded as oauth.authorize succeeded (user_signed_in) for lab-printer-app.

  3. Cancel a step-up at the consent screen

    Recorded as oauth.authorize rejected (access_denied) for lab-printer-app.

Setup

You need lab-printer-app with lab-tmp-at-signin, APP_ID, API_ID, API_SECRET, the everyday TOKEN and REFRESH from Follow a step-up challenge from challenge to retry.

  1. In its own terminal, start the local photo API with a two-minute deletion policy:

btl-lab resource --port 8766 --mode introspect --audience "$ISSUER/resource" --delete-max-age 120
  1. Save the app's delete action. It runs only when Ava chooses to delete, makes at most one step-up attempt, asks for photos.delete alone, and never refreshes in response to a challenge:

cat > delete-photo.sh <<'EOF'
try_delete() {  # try_delete TOKEN PHOTO: prints the status and keeps the headers for a challenge
  curl -s -o /dev/null -D hdr.txt -w '%{http_code}' -X DELETE -H "Authorization: Bearer $1" "http://127.0.0.1:8766/photos/$2"
}
delete_photo() {  # delete_photo PHOTO: one user action, at most one step-up
  local status; status=$(try_delete "${STEP_TOKEN:-$TOKEN}" "$1")
  [ "$status" = 204 ] && { echo "Deleted $1."; return 0; }
  grep -qi insufficient_user_authentication hdr.txt || { echo "Not deleted: the photo API refused the request ($status)."; return 1; }
  local maxage; maxage=$(sed -n 's/.*max_age="\([0-9]*\)".*/\1/p' hdr.txt)
  eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
  echo "Deleting photos needs a recent sign-in. Open this, sign in, then paste what the callback shows:"
  echo "$ISSUER/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=photos.delete&max_age=$maxage&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
  local code st iss err
  read -rsp 'code: ' code; echo; read -rp 'state: ' st; read -rp 'iss: ' iss; read -rp 'error: ' err
  [ "$iss" = "$ISSUER" ] && [ "$st" = "$STATE" ] || { echo "Not deleted: the sign-in response could not be verified."; return 1; }
  [ -z "$err" ] || { echo "Not deleted: sign-in was not completed. Your photos are unchanged."; return 1; }
  STEP_TOKEN=$(curl -s "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "client_id=$APP_ID" --data-urlencode "code=$code" \
    --data-urlencode redirect_uri=http://127.0.0.1:8765/callback --data-urlencode "code_verifier=$VERIFIER" | jq -r .access_token)
  status=$(try_delete "$STEP_TOKEN" "$1")
  [ "$status" = 204 ] && { echo "Deleted $1."; return 0; }
  echo "Not deleted: the new sign-in did not meet the requirement. Not retrying."; return 1
}
EOF
. ./delete-photo.sh
  1. Press Start on this page with Lab Photos selected.

Walkthrough

  1. Refreshing does not help. Refresh the everyday token, then try a deletion with it once Ava's sign-in is more than two minutes old:

RESP=$(curl -s "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "client_id=$APP_ID" --data-urlencode "refresh_token=$REFRESH")
TOKEN=$(jq -r .access_token <<<"$RESP"); REFRESH=$(jq -r .refresh_token <<<"$RESP"); unset RESP
try_delete "$TOKEN" 9137; echo

The status is 401: the refreshed token carries the original auth_time, so it fails the same check.

Why it matters: the delete action never refreshes to satisfy a challenge. Only a new sign-in changes auth_time.

  1. One step-up, then success. Start btl-lab callback, then:

unset STEP_TOKEN; delete_photo 9137

Open the printed URL, sign in as Ava with her password, approve and paste the listener's values. The action prints Deleted 9137.

Why it matters: the step-up started from an action Ava chose, and the request asked only for photos.delete, so the stepped-up token carries only the access that needed it.

  1. Two kinds of token. Browse with the everyday token and look at what each one carries:

curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8766/photos
btl-lab introspect "$STEP_TOKEN"

Browsing returns 200. The stepped-up token's scope is photos.delete alone. The app sends TOKEN for reading and STEP_TOKEN for deleting.

Why it matters: the app chooses by remembering why it obtained each token, not by reading acr or auth_time inside it. The token is opaque to the client.

  1. No loop. Restart the API with an impossible window, then try again with a fresh step-up:

btl-lab resource --port 8766 --mode introspect --audience "$ISSUER/resource" --delete-max-age 5
unset STEP_TOKEN; delete_photo 9137

By the time you sign in and paste the values, more than five seconds have passed. The API challenges the new token too, and the action stops with Not retrying.

Why it matters: one step-up attempt per action. A second challenge means trying again will not meet the requirement, and the app explains instead of redirecting forever.

  1. Ava cancels. Restart the API with --delete-max-age 120, run unset STEP_TOKEN; delete_photo 9137 once the sign-in is more than two minutes old, and choose Deny on the consent screen. Paste the values, including access_denied as the error. The issuer and state checks pass, and the action prints Not deleted: sign-in was not completed. Your photos are unchanged.

Why it matters: Ava knows the photos were kept, and nothing in the message hints at a way around the requirement.

  1. The API's order of checks:

curl -s -i -X DELETE http://127.0.0.1:8766/photos/9137 | grep -i www-authenticate
curl -s -i -X DELETE -H 'Authorization: Bearer not-a-token' http://127.0.0.1:8766/photos/9137 | grep -i www-authenticate
curl -s -i -X DELETE -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8766/photos/9137 | grep -i www-authenticate

No token gets a plain Bearer challenge, a value that is not a token gets error="invalid_token", and only a valid token with an old sign-in gets the insufficient_user_authentication challenge, built from the API's own policy.

Why it matters: requirements are revealed only to holders of a valid token. A challenge is a request for a better token, never a source of authority, and a missing claim counts as not meeting the requirement.

Planned walkthrough

These steps need tenant-defined acr values (G20) and acr in access tokens (G48).

  1. Remove Ava's passkey at $ISSUER/account/security and send a step-up request with acr_values=urn:btl:acr:phishing-resistant. Instead of signing her in with a password and issuing a token the API would refuse, Lab Photos returns error=unmet_authentication_requirements with state and iss, and Audit records oauth.authorize rejected unmet_authentication_requirements (planned reason). The delete action tells Ava that deleting needs a passkey and points to her account settings.

  2. Before redirecting, the action checks that the requested value appears in acr_values_supported in discovery, and stops if it does not.

  3. The administrator decides which methods satisfy each acr value and can count unmet requirements in Audit, a sign of an enrollment gap rather than a protocol problem.

Break it

  1. Steps 1, 4 and 5 are the failures: a refresh, an impossible window and a cancellation each end with the photo kept and a clear message.

Check your work

Press Check my progress. The checks look in Lab Photos for the refresh in step 1, Ava's step-up sign-in in step 2 and the cancelled step-up in step 5, all for lab-printer-app. The photo API's log shows challenged after the refresh, allowed once, and challenged after the five-second window.

Cleanup

  1. Stop the photo API and delete hdr.txt and delete-photo.sh if you do not want them.

  2. Assign lab-printer-app back to the tenant default access token manager and delete lab-tmp-at-signin.

  3. Optionally remove Ava's passkey at $ISSUER/account/security and set passkey back to how you found it in Authentication.

  4. Run unset TOKEN STEP_TOKEN REFRESH.

Missing infrastructure

  • G20 acr and acr_values: unmet_authentication_requirements when the requested context cannot be met, acr_values_supported for the precheck, and the tenant's mapping from acr values to methods.

  • G48 Authentication context in access tokens: acr in tokens and introspection, so the API's check includes strength as well as freshness.

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