Skip to content

feat(auth): support both insert and append metadata discovery (#4310)#4346

Open
reinkrul wants to merge 3 commits into
masterfrom
feature/4310-metadata-discovery-master
Open

feat(auth): support both insert and append metadata discovery (#4310)#4346
reinkrul wants to merge 3 commits into
masterfrom
feature/4310-metadata-discovery-master

Conversation

@reinkrul

@reinkrul reinkrul commented Jun 10, 2026

Copy link
Copy Markdown
Member

Closes #4310

Problem

Metadata discovery derived URLs using only the RFC 8414 insert convention (well-known segment at the authority root, issuer/AS path appended: https://host/.well-known/<doc>/<path>). Servers that publish metadata only under the OIDC Discovery append convention (https://host/<path>/.well-known/<doc>) returned 404/401/403/405, so issuance and token flows could not complete without per-deployment nginx rewrite rules.

Affected:

  • Credential Issuer Metadata (openid-credential-issuer) — auth/openid4vci
  • OAuth Authorization Server Metadata (oauth-authorization-server) — auth/client/iam

Change

Discovery tries both placements in priority order and takes the first matching document:

  1. insert (RFC 8414) — unchanged happy path, spec-preferred
  2. append (OIDC Discovery)

When the identifier has no path the two collapse to one URL, so spec-compliant servers still make a single request — no added latency on the happy path.

Implemented by consolidating the previously-duplicated per-type fetch loops into one generic helper oauth.FetchMetadata[T]:

  • Each metadata type declares its well-known path via WellKnownPath() and its issuer via GetIssuer(); the helper derives the candidate URLs (preserving the %2F/RawPath handling), fetches the first that returns 200 and JSON-decodes, and validates the issuer match.
  • Identifier match tolerates a trailing-slash difference (some servers, e.g. IdentityServer, normalize the issuer with a trailing /).
  • Each candidate shares the identifier's host and is SSRF-validated via core.ParsePublicURL; the identifier-match check (issuer / credential_issuer must equal the requested identifier) is enforced on the accepted document, so the fallback cannot be steered to an attacker-chosen file.
  • On exhaustion the error names the identifier and lists the tried locations, reporting only non-404 failures (a 404 is just "not here"); a >= 500 failure maps to 502 Bad Gateway.
  • IdentifiersMatch is now package-internal. The signed-JWT OpenIDConfiguration keeps its own (master) implementation.

Compatibility

Fully additive. Insert-convention servers behave identically with a single request. No new config keys; operators can remove per-deployment nginx metadata-rewrite rules.

Note: the iam path now enforces the issuer identifier-match check on the accepted AS metadata document (previously absent). Nuts-served metadata sets this correctly, but a partner whose metadata issuer field does not equal the requested identifier (modulo trailing slash) would now be rejected rather than silently accepted.

Tests

  • auth/oauth: FetchMetadata — insert single-request, append fallback, identifier-mismatch-falls-through, trailing-slash match, all-404 not-found (names the tried locations), non-404 preserved, upstream 5xx surfaced as core.HttpError, invalid JSON, invalid identifier; plus wellKnownCandidates ordering and IssuerIdToWellKnown.
  • auth/openid4vci: insert-only, append-only, both-404, identifier-mismatch-falls-through, non-404 preserved.
  • auth/client/iam: same matrix plus the >= 500502 mapping.

Assisted by AI

reinkrul added 2 commits June 10, 2026 08:04
Metadata discovery derived URLs using only the RFC 8414 insert convention
(well-known segment at the authority root, issuer/AS path appended). Servers
that publish metadata only under the OIDC Discovery append convention
(well-known after the path) returned 404 and could not complete issuance or
token flows without per-deployment nginx rewrites.

Discovery now tries candidate locations in priority order and takes the first
matching document:
  1. insert (RFC 8414, unchanged happy path)
  2. append (OIDC Discovery)
  3. append openid-configuration (AS metadata only)

When the identifier has no path both forms collapse to one URL, so
spec-compliant servers still make a single request. Each candidate shares the
identifier's host and is SSRF-validated via core.ParsePublicURL, and the
existing identifier-match check (credential_issuer / issuer must equal the
requested identifier) is enforced on the accepted document, so the fallback
cannot be steered to an attacker-chosen file. On exhaustion the error names the
identifier and reports only non-404 failures; a >= 500 failure still maps to
502 Bad Gateway.

Assisted by AI

(cherry picked from commit f01c39a0827a90dbba95530169123a367c74006a)
Some authorization servers (e.g. IdentityServer) normalize the metadata
issuer with a trailing slash, so a server reached at the append location with
issuer "https://host/oauth/" was rejected against the requested identifier
"https://host/oauth". Discovery then fell back to the credential issuer and
surfaced an unrelated 404.

Compare issuer / credential_issuer with a trailing-slash tolerance via
oauth.IdentifiersMatch. Still rejects genuinely different identifiers, so the
fallback cannot be steered to another document.

Assisted by AI

(cherry picked from commit b416b3d)
@qltysh

qltysh Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on master by 0.07%.

Modified Files with Diff Coverage (5)

RatingFile% DiffUncovered Line #s
Coverage rating: B Coverage rating: B
auth/oauth/types.go100.0%
Coverage rating: C Coverage rating: C
auth/openid4vci/client.go100.0%
Coverage rating: B Coverage rating: B
auth/client/iam/client.go100.0%
New Coverage rating: A
auth/oauth/metadata.go94.1%85-86, 89-90
New Coverage rating: A
auth/openid4vci/types.go100.0%
Total95.7%
🤖 Increase coverage with AI coding...
In the `feature/4310-metadata-discovery-master` branch, add test coverage for this new code:

- `auth/oauth/metadata.go` -- Lines 85-86 and 89-90

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

…iscovery

Replace the duplicated well-known metadata fetch-loops in iam/client.go and
openid4vci/client.go with a single generic oauth.FetchMetadata[T]. Each metadata
type provides its own well-known path via WellKnownPath() and its issuer via
GetIssuer(); the helper derives the candidate URLs (insert + append placements),
fetches the first match, validates the issuer, and lists the tried locations on
failure. IdentifiersMatch is now package-internal. The signed-JWT OpenIDConfiguration
keeps its own implementation.

Assisted by AI
@reinkrul reinkrul marked this pull request as ready for review June 12, 2026 08:39
@reinkrul reinkrul requested a review from Dirklectisch June 17, 2026 06:42
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.

Metadata discovery: support both insert (RFC 8414) and append (OIDC Discovery) well-known forms

1 participant