OPENID CONNECT · LAB
Correlate two clients through a public subject, then hand-build a per-sector one
Join the printer's and the collage app's records about Ava through her public sub, see why a prefix does not stop it, and approximate a pairwise sub with a hashed per-sector expression.
Partly readyUses your lab tenant
The lesson
Builds on: The UserInfo endpoint.
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.
- G18 Pairwise subject identifiers
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.
Create a temporary ID token manager for the printer
Recorded as
tenant.oauth.id_token_managers.createsucceeded.Assign it to lab-printer
Recorded as
tenant.oauth.id_token_managers.assignsucceeded.Change how a manager builds sub
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.Test the per-sector expression before saving
Recorded as
tenant.oauth.id_token_managers.testsucceeded.Sign Ava in to lab-collage with the new sub
Recorded as
oauth.tokensucceeded forlab-collageabout[email protected].Sign Ava in to lab-printer with the new sub
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].
Setup
You need
lab-collagewith itslab-collageID token manager,lab-printerwithPRINTER_IDandPRINTER_SECRET, Ava,rp_discoverand the redefined helpers, andlinks.jsonfrom the several providers lab. Runrp_discover "$ISSUER"and keepbtl-lab callbackrunning.Press Start.
In OAuth > ID Token Management, create a manager named
lab-tmp-printer-idwith the default settings and assign it tolab-printeronly. It is temporary and is deleted in Cleanup.Define a sign-in for the printer alongside the collage helpers:
printer_signin_url() { eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; echo "$AUTHORIZATION_ENDPOINT?response_type=code&client_id=$PRINTER_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"; }
printer_exchange() { PRINTER_IDT=$(curl -s -u "$PRINTER_ID:$PRINTER_SECRET" "$TOKEN_ENDPOINT" -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" | jq -r .id_token); btl-lab decode "$PRINTER_IDT"; }
Walkthrough
One identifier for everyone. The discovery document says
"subject_types_supported": ["public"]. Sign Ava in throughlab-collage(signin_url 'openid',exchange) and throughlab-printer(printer_signin_url,printer_exchange '<code>'), and compare the twosubvalues: identical.
Correlation. Write each company's records and join them on nothing but the value the provider gave both:
jq -n --arg s '<sub>' '{sub: $s, orders: ["photo book", "calendar"]}' > printer.json
jq -n --arg s '<sub>' '{sub: $s, collages: ["summer 2026"]}' > collage.json
jq -s 'group_by(.sub) | map(add)' printer.json collage.json
One profile, built by two unrelated companies.
Why it matters: between unrelated companies, a public sub is a ready-made join key. Your print orders and your collages, given to two separate businesses, end up in one record.
A prefix is not pairwise. On the
lab-tmp-printer-idmanager, set the standard claimsubto the subject ID with the prefixprinter-. On thelab-collagemanager, use the prefixcollage-. Sign in through both clients. The values now differ, but stripping the prefixes joins the records again, just as the lesson's picture address carried the account number.
A per-sector
sub, the lesson's hash of sector, account and secret salt. Generate a salt withopenssl rand -hex 16. Onlab-tmp-printer-id, setsubto this expression, with your salt in place of<salt>:
(async () => { const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('printer.example|' + context.subject.id + '|<salt>'));
return Array.from(new Uint8Array(d), b => b.toString(16).padStart(2, '0')).join('').slice(0, 32); })()
Use the same expression with the sector collage.example on the lab-collage manager. Run each manager's Test before saving. If a test reports a failure, stop and keep the prefix version from step 3.
Note: the salt is visible to tenant administrators in the expression. A real provider would keep it secret. This step approximates the derivation; it is not the pairwise feature itself.
Sign in through both clients. The two
subvalues share nothing. Call UserInfo for thelab-collagesign-in and confirm it returns the samesubas that client's ID token.
Why it matters: only the provider can map these values back to Ava. The collage app and the printer have no common key.
Same sector, same value. Change the
lab-collagemanager's sector string fromcollage.exampletoprinter.example. Both clients now receive the samesub, as two registrations sharing onesector_identifier_uriwould.
Not anonymous. Sign in to both with
openid%20email: both receive Ava's email address, and it joins the records just as well as the publicsubdid.
Planned walkthrough
These steps run once pairwise subjects (G18) exist.
On
lab-printerandlab-collage, setsubject_typetopairwise. Discovery lists"subject_types_supported": ["public", "pairwise"], and each client receives its ownsubfor Ava with no expression and no visible salt.Publish a redirect URI list for the printer's sector and name it as
sector_identifier_urionlab-printerand on a temporary second printer client,lab-tmp-printer-app. Both receive the samesub. The tenant refuses the registration of a client whose redirect URIs are not in the file.Move
lab-printer's sector and see every returning customer arrive with a newsub, then move it back.
Break it
Move a sector. Change the
lab-tmp-printer-idsector string towww.printer.exampleand sign in throughlab-printer. Ava'ssubmatches no entry inprinter.json, so every returning customer would look new. The previous values stay bound to Ava in the tenant and will never be given to anyone else.
Restore: set sub on both managers back to the default (the user ID, with no prefix and no expression). That reconnects Ava to the original links.json entry.
Check your work
Press Check my progress. It looks for, in order: the lab-tmp-printer-id manager created and assigned, an update to how a manager builds sub, a successful manager test, then oauth.token succeeded for lab-collage and for lab-printer.
Your notes should show the sub each client received for public, prefixed, per-sector and moved-sector subjects.
Cleanup
Confirm that the
lab-collagemanager issues the defaultsub.Assign
lab-printerback to the tenant's default ID token manager, then deletelab-tmp-printer-id.Delete
printer.jsonandcollage.json. Rununset PRINTER_IDTandunset -f printer_signin_url printer_exchange.
Missing infrastructure
G18 (pairwise subject identifiers): the tenant cannot issue pairwise subjects. Discovery advertises
publiconly, clients have nosubject_typesetting, and there is nosector_identifier_urifetch or validation. The expression in step 4 only approximates the derivation: it is configured by hand per manager, it is not advertised, and its salt is readable by administrators. The planned walkthrough will replace it with real pairwise registrations and a sector file the tenant enforces.