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

Review how your tenant protects each OIDC message

Do the lesson's security review on your own tenant. Follow each message, record how it is protected today, ask for more protection, and read exactly what the tenant answers.

Partly readyUses your lab tenant

The lesson

Builds on: Connecting a sign-in to an account.

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. Ask for a request object by reference

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

  2. Ask for a signed authorization response

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

  3. Read the plain UserInfo response

    Recorded as oidc.userinfo succeeded (userinfo_served).

  4. Send the client secret in the body

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

Setup

The review runs against real messages. Request objects (G12), signed authorization responses (G13), JWT client assertions (G8), and signed or encrypted UserInfo and ID tokens (G64) are not available yet, so decisions that need them stay on paper.

  1. Start a review table with the columns Message, Created by, Protection today, Can it be more, and What the tenant answered.

  2. source ~/btl-oidc.sh, run btl-lab callback before each request, and press Start.

Walkthrough

  1. The authentication request. Run signin, open the URL, and look at the address bar and browser history: every parameter is there in clear. Then ask for a request object by reference:

signin request_uri=urn%3Aexample%3Aplaceholder

The listener prints error=request_uri_not_supported, and discovery says request_parameter_supported: false.

Why it matters: anything you put in the request, such as a claims request about a student discount, is visible wherever URLs are recorded.

  1. The authorization response. The listener shows code, state and iss in clear. Ask for a signed response anyway:

signin response_mode=query.jwt

The listener prints error=invalid_request, with a description saying the response mode must be query, fragment or form_post.

Why it matters: the response carries nothing worth hiding, and PKCE protects the code. That is the printer's reasoning too.

  1. The ID token. Complete a sign-in, redeem, and read the header:

part "$ID_TOKEN" 1

{"alg":"RS256","kid":"...","typ":"JWT"}, three parts: signed, not encrypted. It came from the token endpoint over your own TLS connection.

Why it matters: in the code flow, with no profile claims in the token, signing alone is enough.

  1. The UserInfo response.

curl -si -H "Authorization: Bearer $TOKEN" "$ISSUER/oidc/userinfo" | grep -i '^content-type'
curl -s "$ISSUER/.well-known/openid-configuration" | jq '{userinfo_signing_alg_values_supported, userinfo_encryption_alg_values_supported}'

application/json, and neither metadata field exists.

Why it matters: a second service you pass this JSON to cannot tell where it came from. Only a signed response could travel with its proof, and only an encrypted one stays unreadable past your load balancer.

  1. Client authentication. Send the secret in the body instead of the Authorization header:

curl -s "$ISSUER/oauth/token" -d grant_type=authorization_code -d code=unused -d "client_id=$CLIENT_ID" --data-urlencode "client_secret=$CLIENT_SECRET" | jq .
curl -s "$ISSUER/.well-known/openid-configuration" | jq .token_endpoint_auth_methods_supported

invalid_client, with a description naming HTTP Basic. Client authentication is checked before the code, so the code never mattered. The tenant lists only client_secret_basic and none.

Why it matters: a signed client assertion is not available, so the shared secret stays in the Authorization header and nowhere else.

  1. Write your decision for each message given what the tenant can do today, and mark the ones that would change once the gaps close: a request object signed and then encrypted, and UserInfo signed and then encrypted.

Why it matters: each protection should answer a question someone actually needs answered, and a provider that does not list an algorithm cannot be asked to use it.

Break it

  1. Put a claims request in the URL anyway:

signin "claims=$(enc '{"userinfo":{"email":null}}')"

There is no error and the parameter is ignored (claims_parameter_supported: false). Check your browser history: the request was recorded in clear regardless. That is the reviewer's first finding, and nothing was weakened to see it.

Check your work

Press Check my progress. The checks look for the refused request object, the refused signed response mode, one plain UserInfo response, and the refused client secret in the body. Your completed table has one row per message.

Cleanup

Nothing changed in the tenant.

Missing infrastructure

  • G12 (JAR). Signed and encrypted request objects, by value and by reference.

  • G13 (JARM). Signed authorization responses (query.jwt, form_post.jwt).

  • G8 (client authentication beyond client_secret_basic). private_key_jwt client assertions.

  • G64 (encrypted ID tokens, signed or encrypted UserInfo). Per-client id_token_encrypted_response_alg and _enc, userinfo_signed_response_alg, userinfo_encrypted_response_alg and _enc, a client JWKS for encryption keys, the matching *_values_supported metadata, and application/jwt UserInfo responses. The full lab would register UserInfo as signed then encrypted, and pass the signed inner JWT to a second local service that verifies it.

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