OPENID CONNECT · LAB
Link a second provider only after both sides sign in
Add a Lab Mail sign-in to an existing account only after a fresh Lab Photos sign-in proves control of it, record how each side was proven, then remove the link without stranding the account.
Partly readyUses both lab tenants
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.
- G26 Inbound federation (external OIDC or social IdP) and account linking
- G66 Second lab tenant for every learner: additional tenants need a paid subscription or a BTL grant, so labs that use Lab Mail cannot be completed by an ordinary learner yet
Needs a second tenant. This lab also uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so you may not be able to do the Lab Mail steps yet (gap G66).
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.
Prove control of Ava's account with a fresh sign-in
Recorded as
oauth.authorizesucceeded (user_signed_in) forlab-collageabout[email protected].Receive the code for the proving sign-in
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collageabout[email protected].Exchange the proving sign-in's code
Recorded as
oauth.tokensucceeded forlab-collage.Prove control again before removing the link
Recorded as
oauth.authorizesucceeded (user_signed_in) forlab-collageabout[email protected].
Setup
The relying-party side of linking runs for real: Lab Photos is the account's existing way in and Lab Mail is the provider being added. Your tenant cannot yet act as a relying party that links an external provider to one of its own users (G26), so your terminal plays the collage app.
Finish Key accounts on issuer and subject first. You need
accounts.dbwithacct-8812, the helpers in~/btl-oidc.sh, andID_TOKEN_AVA.In Lab Mail, open Users and create
[email protected], display name Ava Archer, with a password. This is Ava Archer's own account at the second provider. Lab Mail's[email protected](Ava Lin) stays a different person.Add an audit table to the relying party and load the helpers:
source ~/btl-oidc.sh
sqlite3 accounts.db "CREATE TABLE IF NOT EXISTS link_audit(at TEXT, account_id TEXT, issuer TEXT, subject TEXT, action TEXT, proof TEXT);"
STORED_SUB=$(sqlite3 accounts.db "SELECT subject FROM sign_in_identities WHERE account_id='acct-8812' AND issuer='$ISSUER';")
Walkthrough
The pending registration. Point the helpers at Lab Mail, sign in as
[email protected], validate, and keep the pair only in shell variables, never in a form field or URL.
ISSUER_A=$ISSUER CLIENT_A=$CLIENT_ID SECRET_A=$CLIENT_SECRET
ISSUER=$ISSUER2 CLIENT_ID=$MAIL_CLIENT_ID CLIENT_SECRET=$MAIL_CLIENT_SECRET
signin
redeem '<code>'
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE" && \
{ PEND_ISS=$(part "$ID_TOKEN" | jq -r .iss); PEND_SUB=$(part "$ID_TOKEN" | jq -r .sub); }
Why it matters: the identity being linked must come from the validated ID token, by a route the browser cannot touch.
Press Start, then prove control of the existing account. Switch back to Lab Photos and ask for a sign-in that cannot be answered from the session, naming Ava with her earlier ID token.
ISSUER=$ISSUER_A CLIENT_ID=$CLIENT_A CLIENT_SECRET=$SECRET_A
signin max_age=0 "id_token_hint=$ID_TOKEN_AVA"
Lab Photos shows its password form although the browser is signed in. Sign in as Ava, redeem, validate, then require the stored subject and a sign-in no older than 60 seconds:
redeem '<code>'
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE" && \
part "$ID_TOKEN" | jq --arg s "$STORED_SUB" --argjson now "$(date +%s)" \
'if .sub == $s and ($now - .auth_time) <= 60 then "OK proof for acct-8812" else "FAIL no fresh proof" end'
Why it matters: the account to link is the one this sign-in proves, not the one an email match suggests or a form names.
Check for conflicts. The pending pair must not already open another account.
sqlite3 accounts.db "SELECT account_id FROM sign_in_identities WHERE issuer='$PEND_ISS' AND subject='$PEND_SUB';"
Nothing, or acct-8812, lets you continue. Any other account means stop and explain.
Why it matters: moving a way in from one account to another needs proof for both accounts.
Confirm, then write the identity row and the audit entry in one transaction, recording how each side was authenticated.
sqlite3 accounts.db "BEGIN; INSERT INTO sign_in_identities VALUES('acct-8812','$PEND_ISS','$PEND_SUB',datetime('now'));
INSERT INTO link_audit VALUES(datetime('now'),'acct-8812','$PEND_ISS','$PEND_SUB','linked','Lab Photos max_age=0 password; Lab Mail validated ID token'); COMMIT;"
Why it matters: every row is a new way into the account, so it is recorded like enrolling a new security key.
Sign in with Lab Mail as
[email protected]again and look up its pair:acct-8812. Two providers now open one account.
Why it matters: identity rows are ways into the account, and the account outlives any one of them.
Remove the link with care. Ask Lab Photos for another fresh sign-in (
signin max_age=0 "id_token_hint=$ID_TOKEN_AVA") and run the step 2 check. Then make sure a way in remains before deleting:
sqlite3 accounts.db "SELECT count(*) FROM sign_in_identities WHERE account_id='acct-8812' AND NOT (issuer='$PEND_ISS' AND subject='$PEND_SUB');"
sqlite3 accounts.db "BEGIN; DELETE FROM sign_in_identities WHERE issuer='$PEND_ISS' AND subject='$PEND_SUB';
INSERT INTO link_audit VALUES(datetime('now'),'acct-8812','$PEND_ISS','$PEND_SUB','removed','Lab Photos max_age=0 password'); COMMIT;"
Delete only when the count is at least 1.
Why it matters: removing a way in deserves nearly as much care as adding one, and must never leave an account nobody can open.
Compare the one place your tenant does match by email. In Provisioning, read the Match existing users setting:
link_by_emaillets an authenticated provisioning client you configured adopt a user created by hand. Note who is trusted there, compared with an unauthenticatedemailclaim at sign-in.
Why it matters: the lesson's rule is about sign-in claims. The source of trust changes the answer.
Break it
Auto-link by email, the pre-hijacking shape. Sign in at Lab Mail as Ava Lin (
[email protected]), validate, and run the shortcut the lesson warns about:
sqlite3 accounts.db "INSERT INTO sign_in_identities SELECT id, '$ISSUER2', '<Ava Lin sub>', datetime('now') FROM accounts WHERE contact_email='[email protected]';"
It "works", and Ava Lin now opens acct-8812, which belongs to Ava Archer.
Restore: delete that row at once with DELETE FROM sign_in_identities WHERE issuer='$ISSUER2' AND subject='<Ava Lin sub>';. The flow in the walkthrough never consulted the email.
Stale proof. Repeat walkthrough step 2 without
max_age=0. Lab Photos answers silently from its session,auth_timeis old, and your 60-second check printsFAIL no fresh proof. That refusal is correct.
Check your work
Press Check my progress. The checks look for a real password sign-in by Ava to lab-collage with its code and exchange, then a second fresh sign-in before the removal. A silent answer from the session would have no user_signed_in event.
sqlite3 accounts.db "SELECT action, issuer, proof FROM link_audit; SELECT account_id, issuer FROM sign_in_identities WHERE account_id='acct-8812';"
One linked and one removed entry with proof notes, and acct-8812 still has its Lab Photos row.
Cleanup
Delete any row left from Break it.
Keep
accounts.db,[email protected]in Lab Mail and the helpers.
Missing infrastructure
G26 (inbound federation and account linking). A tenant cannot accept sign-in from an external OpenID Provider and has no end-user account linking. With it, this lab would connect Lab Mail as an inbound provider of Lab Photos, sign in to Lab Photos with Lab Mail, see Lab Photos' own "connect to an existing account" page require Ava's password and second step, find the link under
/account/securitywith a Remove button, and read link and unlink events in Audit, with a notice sent to her confirmed address.G66 Second lab tenant: this lab uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so an ordinary learner can do only the Lab Photos steps until every learner can have a second lab tenant.