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

Register the printer twice, as a confidential and a public client

Create lab-printer and lab-printer-app, then see that a client ID proves nothing, a secret authenticates the confidential client, and PKCE protects the public client's exchange.

ReadyUses your lab tenant

The lesson

Builds on: Grants, scopes, and consent.

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. Register the printer's clients

    Recorded as tenant.oauth.clients.create succeeded.

  2. A code presented with only the printer's client ID is refused

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

  3. The printer authenticates and receives a token

    Recorded as oauth.token succeeded for lab-printer.

  4. The public app's request without PKCE is refused

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

  5. The public app redeems its code with a PKCE verifier

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

Setup

  1. Press Start on this page.

  2. Open OAuth > Clients > Create client and start from Web application. Name it lab-printer, type Confidential, PKCE for confidential clients Required, Consent Ask once per set of scopes. Add two redirect URIs: http://127.0.0.1:8765/callback and https://beyondthelogin.dev/lab/callback/. Tick Enabled and save.

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

  4. Create a second client from Native or desktop application. Name it lab-printer-app, type Public, redirect URI http://127.0.0.1/callback, tick Enabled and save.

  5. Set the shell variables. REDIRECT_URI is where the printer's codes return: the hosted callback page displays code, state and iss for you to copy, or use the loopback address with btl-lab callback running in a second terminal.

export ISSUER="https://tenant-<id>.beyondthelogin.dev"
export CLIENT_ID="<lab-printer client ID>"
read -rs CLIENT_SECRET   # the lab-printer secret
export APP_ID="<lab-printer-app client ID>"
export REDIRECT_URI="https://beyondthelogin.dev/lab/callback/"   # or http://127.0.0.1:8765/callback
enc() { jq -rn --arg v "$1" '$v|@uri'; }   # URL-encodes one value

Walkthrough

  1. Open Access Token Management > Client authentication secret again. The selector lists lab-printer and lab-photo-api, but not lab-printer-app.

Why it matters: client type describes whether the deployment can protect a credential. Code delivered to a browser or packaged into every installed copy cannot, so the tenant issues no secret to the public client.

  1. Start an authorization for the printer. Anyone can build this address: the client ID is in plain sight.

eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(enc "$REDIRECT_URI")&scope=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"

Open the printed address, sign in as Ava and approve. Copy the code from the callback: read -r CODE.

  1. Present the code with only the client ID, as anyone who knew the ID could.

curl -s -i -d grant_type=authorization_code -d "code=$CODE" --data-urlencode "redirect_uri=$REDIRECT_URI" \
  -d "code_verifier=$VERIFIER" -d "client_id=$CLIENT_ID" "$ISSUER/oauth/token"

The answer is 401 with WWW-Authenticate: Basic realm="OAuth token" and {"error":"invalid_client",...}.

Why it matters: a client ID tells the server which registration is involved. Knowing it, even while holding a valid code, is not proof that the request came from the printer.

  1. Send the same exchange with the printer's credentials: replace -d "client_id=$CLIENT_ID" with -u "$CLIENT_ID:$CLIENT_SECRET". The answer is 200 with an access_token. The refused attempt in step 3 did not use up the code, because client authentication is checked first.

  1. Now the public app. Start its own loopback listener in a second terminal with btl-lab callback --port 8765, then start an authorization without PKCE.

eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=$(enc http://127.0.0.1:8765/callback)&scope=photos.read&state=$STATE"

The browser returns to the listener at once with error=invalid_request, error_description=This client must send a PKCE code_challenge., state and iss.

Why it matters: a public client has no secret to prove itself with, so the server requires a fresh proof for each attempt instead.

  1. Repeat with a challenge, sign in as Ava, approve, and exchange with no secret at all.

eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=$(enc http://127.0.0.1:8765/callback)&scope=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
read -r CODE   # from the listener's output
curl -s -d grant_type=authorization_code -d "code=$CODE" --data-urlencode "redirect_uri=http://127.0.0.1:8765/callback" \
  -d "code_verifier=$VERIFIER" -d "client_id=$APP_ID" "$ISSUER/oauth/token" | jq

The answer is 200 with an access token. The registration says http://127.0.0.1/callback with no port, and the request used port 8765: loopback addresses match on any port.

Why it matters: PKCE links the exchange to the app instance that started the attempt, so someone who copies the code without the verifier cannot redeem it. It does not identify the application the way the printer's secret does.

Break it

  1. Give the public app an invented secret: add -u "$APP_ID:made-up-secret-made-up-secret-12345" to a fresh step 6 exchange. The answer is 401 invalid_client. Inventing a secret does not make a public client confidential.

  2. Start a printer authorization without code_challenge. It is refused with pkce_required too, because this confidential client's PKCE policy is Required.

Check your work

Press Check my progress. In tenant Audit you can also find two tenant.oauth.clients.create events and tenant.oauth.credentials.rotate. The printer's successful oauth.token has actor lab-printer, an authenticated client; the public app's has no client actor, because a public client's client_id is only a claim.

Cleanup

  1. Keep both clients and the printer's secret. The authorization code labs use them.

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