OPENID CONNECT · LAB
Receive every sign-in error your tenant can return, and fail closed
Produce access_denied, login_required, consent_required and account_selection_required at your callback, handle each one, and refuse to sign anyone in from a response without an ID token.
ReadyUses your lab tenant
The lesson
Builds on: The authentication request.
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.
Ava denies the collage app at the consent page
Recorded as
oauth.authorizerejected (access_denied) forlab-collageabout[email protected].A silent request finds no one signed in
Recorded as
oauth.authorizerejected (login_required) forlab-collage.A silent request needs consent that cannot be asked for
Recorded as
oauth.authorizerejected (consent_required) forlab-collageabout[email protected].A silent request hints at another account
Recorded as
oauth.authorizerejected (account_selection_required) forlab-collageabout[email protected].UserInfo refuses the token from a response without an ID token
Recorded as
oidc.userinforejected (insufficient_scope) forlab-collage.
Setup
You need
lab-collage, Ava, Ben and thesignin_urlandexchangehelpers from the authentication request lab.lab-collageuses consent mode remember.Keep
btl-lab callbackrunning in a second terminal. It also printserroranderror_descriptionwhen a callback carries them.Press Start.
Walkthrough
access_denied. Runsignin_url 'openid%20profile%20email%20photos.read'and open it in a private window. Sign in as Ava and select Deny access on the consent page. The listener printserror=access_denied, yourstateandiss.
Handle it exactly like a success up to the point where a code would be used: confirm that state matches your attempt and iss equals $ISSUER, then mark the attempt finished so it cannot be resumed.
Why it matters: errors return through the same callback and get the same state and issuer checks as a code.
Write the message the person sees.
error_descriptionis developer text from the provider, so never show it as your own words or insert it into a page as markup. Write your own, for example: "We couldn't sign you in with your Lab Photos account. Try again, or sign in another way." A denial Ava chose deserves a calmer message than an unexpected failure.
login_required. Open a fresh private window with no tenant session and usesignin_url 'openid' '&prompt=none'. The listener printserror=login_requiredat once, without any page.
Why it matters: under prompt=none this is an answer to the question you asked ("is anyone signed in?"), not a failure. Your handler decides whether to show its own sign-in page or carry on without a signed-in person.
Run the same URL without
&prompt=nonein that window. You see the tenant's sign-in page instead. An interactive request that came back withlogin_requiredwould be surprising and worth investigating.
consent_required. In OAuth > Clients, setlab-collageconsent mode to always. In a window where Ava is signed in, runsignin_url 'openid' '&prompt=none'. The listener printserror=consent_required: Ava would have to approve again, and the request asked for no pages.
Restore: set lab-collage consent mode back to remember now.
account_selection_required. In the window where Ava is signed in, runsignin_url 'openid' '&prompt=none&login_hint=ben%40example.com'. The listener printserror=account_selection_required: the hint names another account, and choosing one needs a page.
Fail closed. Run
signin_url 'profile%20email'(noopenid), approve and runexchange '<code>'. The response succeeds, butid_tokenis absent. Your handler stops here. For completeness, see that the tempting fallback would not even work on this tenant:
curl -si "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $TOKEN" | head -5
Expect 403 with Bearer error="insufficient_scope".
Why it matters: no validated ID token, no sign-in. A UserInfo fallback would turn a broken response into a sign-in, because UserInfo answers for whoever holds a suitable access token and says nothing about this attempt.
Log the failures safely. Write one JSON line per failure with the stage, the error or failed check, the expected issuer and a correlation ID you generate. Your
stateis not a correlation ID, and nothing from the code, the tokens or the full callback URL belongs in the line:
jq -cn --arg e access_denied --arg i "$ISSUER" --arg c "$(openssl rand -hex 8)" \
'{event: "sign_in.provider_error", message: "Sign-in ended with a provider error", stage: "callback", error: $e, expected_issuer: $i, correlation_id: $c}'
Break it
A return address that is not registered. Run
signin_url 'openid'and replace the encodedredirect_uriwithhttps%3A%2F%2Fexample.com%2Fcb. The tenant shows its own error page and never redirects, because it cannot trust that address with an error or a code.
An error callback that matches no attempt. Take the
access_deniedcallback values from step 1 and present them again after the attempt is finished. Your handler finds no pending attempt in this browser and does nothing, exactly as for any other unexpected request to the callback.
Check your work
Press Check my progress. It looks for, in order: oauth.authorize rejected with access_denied, login_required, consent_required and account_selection_required for lab-collage, then oidc.userinfo rejected with insufficient_scope.
The unregistered return address in Break it never reaches Audit, because the tenant could not attribute it to a valid client and address. Find it in Logs as oauth.authorize rejected with invalid_redirect_uri. Audit also shows the two tenant.oauth.clients.update records from step 5.
Cleanup
Confirm that lab-collage consent mode is remember, and run unset TOKEN ID_TOKEN RESP.