Skip to content

feat: publish OIDC discovery metadata so clouds can federate to ZeroID (ADR 0028 D8) - #313

Merged
saucam merged 3 commits into
mainfrom
feat/oidc-discovery-document
Sep 3, 2026
Merged

feat: publish OIDC discovery metadata so clouds can federate to ZeroID (ADR 0028 D8)#313
saucam merged 3 commits into
mainfrom
feat/oidc-discovery-document

Conversation

@saucam

@saucam saucam commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Why

AWS IAM OIDC identity providers, Azure federated identity credentials and GCP workload identity pools all discover an external issuer by fetching /.well-known/openid-configuration. All three were written against OpenID Connect Discovery, not RFC 8414 — so ZeroID publishing only /.well-known/oauth-authorization-server left it undiscoverable to every one of them, even though the keyset they would verify against was already public at /.well-known/jwks.json.

That gap was the whole of "make our identity useful in someone else's cloud" (ADR 0028 D8). The blocker was a missing discovery document, not missing crypto.

This is also the shared top rung of the governance ladder: it is how a model-one agent reaches level 4 (customer configures their cloud to trust identities we issue) and how model two works outside our own walls. One piece of work, both models.

What

GET /.well-known/openid-configuration — the RFC 8414 document plus the two members OIDC Discovery §3 makes REQUIRED that RFC 8414 §2 does not define:

Member Value Why
subject_types_supported ["public"] ZeroID sub is the identity's WIMSE URI (credential.go:384-388), identical for every verifier. A pairwise subject would break the cloud role trust policies that pin a literal sub.
id_token_signing_alg_values_supported derived from the live keyset A verifier that trusts an alg we no longer sign with fails at token-verification time in someone else's cloud — the most expensive place to debug it. Serves ["ES256","RS256"] with an RSA key loaded, ["ES256"] without.

Built as a superset of the existing document rather than a second one — two hand-maintained documents describing one AS is how they drift. buildASMetadata is extracted for both and returns a fresh map per call, so OIDC-only members cannot leak backwards into the RFC 8414 response.

ZeroID is NOT an OpenID Provider

It issues no id_token, and this document must not become evidence otherwise. The structural guard is response_types_supported, inherited from the base document: it carries at most "code" and never an id_token response type, so no relying party can request one whatever it discovers here.

TestOIDCDiscovery_NeverAdvertisesIDToken asserts that absence. It is the test that should fail — and be deleted — by the PR that makes id_token issuance real. Not before. Documented in the COMPLIANCE.md scope note and normatively in spec §11.4.

Verification

  • Full integration suite green; full go test ./... green; go vet + golangci-lint clean on the changed packages. Re-verified after the main merge that brought in feat: coalesce concurrent CIMD document fetches for the same client_id #312.
  • The two non-obvious invariants were mutation-tested, not just asserted:
    • hardcoding a wrong alg list → TestOIDCDiscovery_SigningAlgsMatchPublishedJWKS fails (and confirmed the suite exercises both keys rather than passing vacuously);
    • caching the base map → TestOIDCDiscovery_OIDCOnlyMembersStayOutOfASMetadata fails on both leaked members.
  • Concurrency: jwk.Set is internally RWMutex-protected, and Key(i) returning !ok correctly handles a keyset that shrank mid-iteration.
  • New compliance suite follows the COMPLIANCE.md conventions (TestOIDCDiscovery1_0_S<section>_<assertion>, one MUST per test, clause quoted in the first comment). Coverage matrix updated.

Audit findings (second commit)

A post-hoc audit of this PR found a defect the original commit missed: spec §10.4 asserted "ZeroID does not publish an /.well-known/openid-configuration document" — in the one section of the normative spec specifically about ZeroID as a federation issuer to AWS/Azure/GCP. The endpoint was added and the README and COMPLIANCE.md updated, but the RFC-style document an independent implementer would read was left contradicting the code. Fixed, with a new §11.4 specifying the endpoint and updated §14.5/§14.6 registry cross-references.

Scope

Correction to an earlier version of this description, which said end-to-end federation had to wait on an audience story. That was too pessimistic. Per spec §10.4, issued tokens default aud to the ZeroID issuer URL, and a relying party that enforces an audience must be configured to accept that URL as the expected audience — Anthropic Workload Identity Federation is already a verified relying party on exactly this path, with AWS/Azure/GCP following the same configuration shape. So this PR makes ZeroID discoverable, which is the piece AWS and Azure structurally could not work without; #199/#258 (caller-chosen aud) would make it more ergonomic, not newly possible.

Out of scope by construction: OIDC §5 UserInfo, §2 WebFinger issuer discovery, and every ID Token clause.

Follow-ups

🤖 Generated with Claude Code

…D (ADR 0028 D8)

AWS IAM OIDC identity providers, Azure federated identity credentials and
GCP workload identity pools all discover an external issuer by fetching
/.well-known/openid-configuration. All three were written against OpenID
Connect Discovery, not RFC 8414, so ZeroID publishing only
/.well-known/oauth-authorization-server left it undiscoverable to every one
of them — even though the keyset they would verify against was already
public at /.well-known/jwks.json.

That gap was the whole of "make our identity useful in someone else's
cloud": the blocker was a missing discovery document, not missing crypto.

The new endpoint serves the RFC 8414 document plus the two members OIDC
Discovery §3 makes REQUIRED that RFC 8414 §2 does not define:

  - subject_types_supported: ["public"]. ZeroID `sub` is the stable identity
    ID, identical for every verifier. A pairwise subject would break the
    cloud role trust policies that pin a literal `sub`.
  - id_token_signing_alg_values_supported: derived from the live keyset
    rather than hardcoded. A verifier that trusts an alg we no longer sign
    with fails at token-verification time in someone else's cloud, which is
    the most expensive place to debug it.

Superset rather than a second document because two hand-maintained
documents describing one AS is how they drift; buildASMetadata is extracted
for both and returns a fresh map per call, so OIDC-only members cannot leak
backwards into the RFC 8414 document.

ZeroID is NOT an OpenID Provider and issues no id_token. The structural
guard is response_types_supported, which carries at most "code" and never
an id_token response type, so no relying party can request one whatever it
discovers here. TestOIDCDiscovery_NeverAdvertisesIDToken asserts that
absence and should be deleted by the PR that makes the claim true, not
before.

Both non-obvious invariants were mutation-tested: hardcoding a wrong alg
list and caching the base map each produce a failing test.

@highflame-oracle highflame-oracle Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔮 Oracle Review

🎯 Start Here

internal/handler/wellknown.go (~31 min) — Security changes in wellknown.go


📋 PR Summary

What this PR does: Implements OpenID Connect Discovery metadata endpoint (/.well-known/openid-configuration) to enable AWS, Azure, and GCP cloud federation with ZeroID, extending the existing RFC 8414 authorization server metadata as a superset.

Key changes:

  • Added GET /.well-known/openid-configuration endpoint with OIDC-required members (subject_types_supported, id_token_signing_alg_values_supported)
  • Extracted buildASMetadata function to share logic between RFC 8414 and OIDC Discovery endpoints, preventing OIDC-only members from leaking
  • Derives supported signing algorithms dynamically from live JWKS keyset (ES256/RS256 based on loaded keys)
  • Added mutation-verified tests ensuring OIDC members don't leak and signing algorithms match published JWKS
  • Compliance suite updated with OIDC Discovery 1.0 specification tests following COMPLIANCE.md conventions

Areas affected: internal/handler/wellknown.go, OIDC Discovery endpoint, Cloud federation integration

Testing notes: Full integration suite and go test ./... passed; mutation testing verified non-obvious invariants; compliance tests added per COMPLIANCE.md conventions with one MUST per test clause


🔍 Code Review

This is a well-architected implementation that thoughtfully extends the existing RFC 8414 metadata rather than duplicating logic, using a superset approach that prevents document drift. The author went beyond standard testing with mutation testing to verify critical invariants and properly scoped the work as unblocking provider registration rather than delivering full end-to-end flow.

What's good:

  • ✨ Excellent use of mutation testing to verify invariants (algorithm mismatch and member leakage) rather than relying solely on assertions
  • ✨ Thoughtful architectural choice to build as a superset of RFC 8414, extracting shared logic to prevent drift between two hand-maintained documents
  • ✨ Clear scope boundaries with explicit test (TestOIDCDiscovery_NeverAdvertisesIDToken) guarding against unintended ID token advertisement, documented in COMPLIANCE.md

Review Stats: suggestion:1


Generated by Oracle - Highflame's AI Code Reviewer

Comment thread internal/handler/wellknown.go
Section 10.4 asserted "ZeroID does not publish an
/.well-known/openid-configuration document" — a statement this PR's own
change makes false, in the one section of the normative spec that is
specifically about ZeroID as a federation issuer to AWS / Azure / GCP.

Found auditing the PR rather than writing it: the endpoint was added, the
README and COMPLIANCE.md were updated, and the RFC-style spec that an
independent implementer would read was left contradicting the code.

Fixes the claim, adds §11.4 specifying the document (both OIDC-required
members, why subject_types_supported is `public` rather than `pairwise`, and
a MUST NOT against reading it as advertising an id_token), and updates the
§14.5 / §14.6 registry cross-references.
@saucam
saucam merged commit d32aa68 into main Sep 3, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants