feat(auth): OIDC userinfo fallback for email / email_verified #105

Closed
opened 2026-06-18 12:18:38 +00:00 by james · 0 comments
Owner

Problem

Carol's OIDC verification (#85 / ADR-0017) reads email and email_verified exclusively from the id_token. Authentik (and several other IdPs) gate those claims behind a per-provider "Include claims in id_token" toggle — when off, the id_token carries only standard claims and verified-email lives only at the userinfo endpoint.

A self-hoster running default Authentik who hasn't found and flipped that toggle hits ?error=oidc_claim with the log line id_token email_verified is not true even though their user IS verified and Authentik IS willing to attest it — just not on the id_token. The fix today is "find the right toggle in the IdP UI." That's friction we should absorb in code.

Hit when deploying #85 against Authentik on carol.int.wynning.tech: Authentik's JWT preview showed email_verified: true, but the live id_token didn't carry the claim.

Scope

  • Refactor verifyIdToken in lib/auth/oidc-verify.ts to return {providerUserId, email?, emailVerified?} (no throws on missing email / email_verified). Signature + iss + aud + exp + nbf + nonce + sub checks stay.
  • Add fetchUserinfo(args) helper in lib/auth/oidc-verify.ts. HTTPS, Bearer access_token, 5s timeout, and sub must match the id_token sub (OIDC §5.3.2 token-substitution defence). Returns {email?, emailVerified?}.
  • Update the OIDC provider's extractProfile in lib/auth/oidc-providers.ts to orchestrate: verify id_token → if email or email_verified missing, fetch userinfo → merge → apply strict email_verified === true check.
  • Tests: verify (unit) for the new return shape + fetchUserinfo (sub match, non-200, missing claims). End-to-end (tests/api/oauth.test.ts): id_token lacks email_verified + userinfo provides it → success; id_token lacks both → success; userinfo sub mismatch → oidc_claim; id_token lacks claim and no userinfo configured → oidc_claim.
  • ADR-0017 §4.5 documenting the userinfo fallback rationale and the trust chain (id_token sig anchors trust, userinfo binds to same subject via sub-match, IdP authenticates Carol via access_token bearer).
  • docs/oidc-self-hoster-guide.md Troubleshooting entry for the oidc_claim + email_verified log line, pointing at userinfo-endpoint configuration as one fix.

Out of scope

  • Relaxing email_verified === true. Still load-bearing (ADR-0015 §3).
  • userinfo_endpoint-only IdPs (no sub claim in userinfo) — we still require sub-match. Such IdPs are non-conformant with OIDC §5.3.

Acceptance

A self-hoster running default Authentik on a user marked email-verified in the IdP gets ?error=oidc_claim to clear without changing any IdP setting, provided userinfo_endpoint resolves (via discovery or override).

## Problem Carol's OIDC verification (#85 / ADR-0017) reads `email` and `email_verified` exclusively from the id_token. Authentik (and several other IdPs) gate those claims behind a per-provider "Include claims in id_token" toggle — when off, the id_token carries only standard claims and verified-email lives only at the userinfo endpoint. A self-hoster running default Authentik who hasn't found and flipped that toggle hits `?error=oidc_claim` with the log line `id_token email_verified is not true` even though their user IS verified and Authentik IS willing to attest it — just not on the id_token. The fix today is "find the right toggle in the IdP UI." That's friction we should absorb in code. Hit when deploying #85 against Authentik on `carol.int.wynning.tech`: Authentik's JWT preview showed `email_verified: true`, but the live id_token didn't carry the claim. ## Scope - [ ] Refactor `verifyIdToken` in `lib/auth/oidc-verify.ts` to return `{providerUserId, email?, emailVerified?}` (no throws on missing email / email_verified). Signature + iss + aud + exp + nbf + nonce + sub checks stay. - [ ] Add `fetchUserinfo(args)` helper in `lib/auth/oidc-verify.ts`. HTTPS, Bearer `access_token`, 5s timeout, and **`sub` must match the id_token sub** (OIDC §5.3.2 token-substitution defence). Returns `{email?, emailVerified?}`. - [ ] Update the OIDC provider's `extractProfile` in `lib/auth/oidc-providers.ts` to orchestrate: verify id_token → if email or email_verified missing, fetch userinfo → merge → apply strict `email_verified === true` check. - [ ] Tests: verify (unit) for the new return shape + fetchUserinfo (sub match, non-200, missing claims). End-to-end (`tests/api/oauth.test.ts`): id_token lacks `email_verified` + userinfo provides it → success; id_token lacks both → success; userinfo sub mismatch → `oidc_claim`; id_token lacks claim and no userinfo configured → `oidc_claim`. - [ ] ADR-0017 §4.5 documenting the userinfo fallback rationale and the trust chain (id_token sig anchors trust, userinfo binds to same subject via sub-match, IdP authenticates Carol via access_token bearer). - [ ] `docs/oidc-self-hoster-guide.md` Troubleshooting entry for the `oidc_claim` + `email_verified` log line, pointing at userinfo-endpoint configuration as one fix. ## Out of scope - Relaxing `email_verified === true`. Still load-bearing (ADR-0015 §3). - `userinfo_endpoint`-only IdPs (no `sub` claim in userinfo) — we still require sub-match. Such IdPs are non-conformant with OIDC §5.3. ## Acceptance A self-hoster running default Authentik on a user marked email-verified in the IdP gets `?error=oidc_claim` to clear without changing any IdP setting, provided `userinfo_endpoint` resolves (via discovery or override).
james closed this issue 2026-06-18 12:21:58 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#105
No description provided.