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.
Sign in to start this lab and check your progress. Log in or create an account.
Redeem a code requested without openid
Recorded as
oauth.tokensucceeded forlab-collageabout[email protected].UserInfo refuses a token without openid
Recorded as
oidc.userinforejected (insufficient_scope) forlab-collage.max_age makes the provider ask for Ava's password again
Recorded as
oauth.authorizesucceeded (user_signed_in) forlab-collageabout[email protected].A token-only response with openid is refused
Recorded as
oauth.authorizerejected (unauthorized_client) forlab-collage.A request object parameter is refused
Recorded as
oauth.authorizerejected (request_not_supported) forlab-collage.prompt=none combined with login is refused
Recorded as
oauth.authorizerejected (invalid_request) forlab-collage.
Setup
You need
lab-collage, Ava and the shell variables from the first lab in this track. Keepbtl-lab callbackrunning in a second terminal.Define the two helpers the rest of the track uses.
signin_urlbuilds an authentication request from fresh values, a scope and optional extra parameters.exchangeredeems 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"; }
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
Without
openid. Runsignin_url 'profile%20email', sign in as Ava and approve.exchangeprints"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.
With
openid. Runsignin_url 'openid%20profile%20email'. The tenant asks nothing new (consent is remembered for these scopes), andexchangeprints"id_token": "present".
Sign-in plus access. Run
signin_url 'openid%20profile%20email%20photos.read'. The consent page now lists the photo scope too.exchangeshowsscopewith all four values, andbtl-lab decode "$TOKEN"showsphotos.readin the access token'sscopeclaim.
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.
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.
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.
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. Afterexchange, 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
Ask for an access token alone with
openid. RunRT=token signin_url 'openid'and open the URL. The tenant redirects back witherror=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 forbidsopenidwithresponse_type=token, since a sign-in always yields an ID token. Rununset RTafterwards.
Send a request object. Run
signin_url 'openid' '&request=not-a-request-object'. The callback carrieserror=request_not_supported. Packaging the request as a signed object is JWT-Secured Authorization Requests, in Advanced OAuth.
Ask for two incompatible things. Run
signin_url 'openid' '&prompt=none%20login'. The callback carrieserror=invalid_request, becauseprompt=nonecannot 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.