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

OPENID CONNECT · LAB

Reshape the authentication request one parameter at a time

Remove openid, add a photo scope, send max_age and parameters the provider may ignore, and read how your tenant answers each version of the request.

ReadyUses your lab tenant

The lesson

Builds on: From delegated access to sign-in.

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. Redeem a code requested without openid

    Recorded as oauth.token succeeded for lab-collage about [email protected].

  2. UserInfo refuses a token without openid

    Recorded as oidc.userinfo rejected (insufficient_scope) for lab-collage.

  3. max_age makes the provider ask for Ava's password again

    Recorded as oauth.authorize succeeded (user_signed_in) for lab-collage about [email protected].

  4. A token-only response with openid is refused

    Recorded as oauth.authorize rejected (unauthorized_client) for lab-collage.

  5. A request object parameter is refused

    Recorded as oauth.authorize rejected (request_not_supported) for lab-collage.

  6. prompt=none combined with login is refused

    Recorded as oauth.authorize rejected (invalid_request) for lab-collage.

Setup

  1. You need lab-collage, Ava and the shell variables from the first lab in this track. Keep btl-lab callback running in a second terminal.

  2. Define the two helpers the rest of the track uses. signin_url builds an authentication request from fresh values, a scope and optional extra parameters. exchange redeems a code and summarizes the response without printing tokens.

signin_url() { eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; echo "$ISSUER/oauth/authorize?response_type=${RT:-code}&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=$1&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256$2"; }
exchange() { RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "code=$1" --data-urlencode "redirect_uri=http://127.0.0.1:8765/callback" --data-urlencode "code_verifier=$VERIFIER"); TOKEN=$(jq -r '.access_token // empty' <<<"$RESP"); ID_TOKEN=$(jq -r '.id_token // empty' <<<"$RESP"); jq '{token_type, expires_in, scope, error, id_token: (if .id_token then "present" else "absent" end)}' <<<"$RESP"; }
  1. Press Start.

For every sign-in below: run signin_url ... (it sets STATE, NONCE and VERIFIER in your shell), open the printed URL, check state and iss from the listener, then run exchange '<code>'.

Walkthrough

  1. Without openid. Run signin_url 'profile%20email', sign in as Ava and approve. exchange prints "id_token": "absent". Then call UserInfo with the access token:

curl -si "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $TOKEN" | head -5

Expect 403 with Bearer error="insufficient_scope".

Why it matters: without openid this is an ordinary OAuth request. The tenant still authenticated Ava and issued a code, but there is no ID token and no sign-in the collage app can count on.

  1. With openid. Run signin_url 'openid%20profile%20email'. The tenant asks nothing new (consent is remembered for these scopes), and exchange prints "id_token": "present".

  1. Sign-in plus access. Run signin_url 'openid%20profile%20email%20photos.read'. The consent page now lists the photo scope too. exchange shows scope with all four values, and btl-lab decode "$TOKEN" shows photos.read in the access token's scope claim.

Why it matters: one response can carry both an ID token for the sign-in and an access token for the photo API. Compare the two consent pages: the second asks far more of someone who only wanted to sign in.

  1. Sign in now, ask for access later. Run step 2's request, then a separate signin_url 'photos.read' at the moment Ava "starts choosing photos". Two smaller consents instead of one large one.

  1. Parameters the provider may ignore. Run signin_url 'openid' '&ui_locales=fr-CA&display=popup'. The request succeeds and the pages look exactly as before.

Why it matters: every parameter in the authentication request is a request, not a result. The provider is allowed to ignore ui_locales and display.

  1. Ask for fresh authentication and check it yourself. Wait at least two minutes after your last sign-in, then run signin_url 'openid' '&max_age=60'. The tenant asks for Ava's password even though her session is still valid. After exchange, check the result instead of assuming it:

btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE" --max-age 60

The run ends in ACCEPT, and auth_time is within a few seconds of iat.

Why it matters: the relying party sent max_age, and it still checks auth_time in the token rather than trusting that the provider honored the request.

Break it

  1. Ask for an access token alone with openid. Run RT=token signin_url 'openid' and open the URL. The tenant redirects back with error=unauthorized_client, because its flow policy does not allow token responses at all. Token responses return in the URL fragment, which the browser never sends to the listener, so read the error in the address bar. Even where a provider allows them, OpenID Connect forbids openid with response_type=token, since a sign-in always yields an ID token. Run unset RT afterwards.

  1. Send a request object. Run signin_url 'openid' '&request=not-a-request-object'. The callback carries error=request_not_supported. Packaging the request as a signed object is JWT-Secured Authorization Requests, in Advanced OAuth.

  1. Ask for two incompatible things. Run signin_url 'openid' '&prompt=none%20login'. The callback carries error=invalid_request, because prompt=none cannot be combined with any other value.

Check your work

Press Check my progress. The checks look for, in order: oauth.token succeeded for lab-collage (the request without openid), oidc.userinfo rejected with insufficient_scope, oauth.authorize with user_signed_in from the max_age=60 request, then oauth.authorize rejected with unauthorized_client, request_not_supported and invalid_request.

The difference between steps 1 and 2 is visible only in the token responses: Audit records a successful token request either way.

Cleanup

Nothing to restore. Keep the signin_url and exchange helpers for the rest of the track, and run unset TOKEN ID_TOKEN RESP.

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