Skip to content

fix(federation)!: keep static client keys, RP choices and empty values within policy - #697

Merged
osanderson merged 3 commits into
mainfrom
fix/federation-chain-review
Oct 25, 2026
Merged

osanderson merged 3 commits into
mainfrom
fix/federation-chain-review

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

Three automatic-registration fixes, plus documentation of pre-trust fetching.

Static client keys (AutomaticClientKeySource). Keys used to fall back to a client's federation-published jwks whenever Dependencies.ClientKeys returned an error or no matching key, including for statically registered clients. A static client whose client_id is also an Entity Identifier resolving under a configured Trust Anchor could then be authenticated with a key its federation metadata publishes (which any superior in the chain can set), and get the static registration's scopes and grants. Keys now come from the same source as the registration:

  • A client the underlying ClientRepository resolves gets its keys from the underlying key source only. An error or an empty set from that source is returned as is.
  • A client that repository doesn't have gets its keys from its openid_relying_party metadata only.
  • storage.ErrStoreUnavailable from the repository is returned, not read as "not static".

RP Metadata Choices vs. a superior's policy. A one_of/value policy on token_endpoint_auth_signing_alg or token_endpoint_auth_method is checked only on that parameter, and only when it is present (Federation 1.0 §6.1.3.1.4). An RP could leave the singular parameter out and register a forbidden value through *_values_supported / token_endpoint_auth_methods_supported, or name no algorithm and get all of the OP's defaults. The changes:

  • Resolver now keeps the merged metadata policy on the ResolvedEntity in an unexported field. Resolved Metadata output is unchanged.
  • Automatic registration limits the plural values, and the default algorithms, to what the policy permits for the singular parameter. An RP left with no permitted value is refused, as RP Metadata Choices 1.0 §4 describes.
  • An empty plural array (which subset_of can leave) is now refused. Before, it was read as absent, so the defaults applied.

Registering every mutually supported method from token_endpoint_auth_methods_supported when AllowedClientAuthMethods is empty is unchanged. Federation 1.0 §12.1.4 says OPs "SHOULD accept any client authentication method that is mutually supported", because the OP can't know which one the RP will use. The only narrowing added is the policy limit on token_endpoint_auth_method described above.

Empty strings. cf067f6 made null count as absent in the policy engine and refused "" for the four encryption parameters. Everywhere else, "" still counted as present to essential (§6.1.3.1.7 only requires presence), while registration read it as unset, so subject_type: "" under an essential policy got the OP's default. Automatic registration now refuses "" for every string parameter it reads. The list comes from relyingPartyMetadata's json tags, so a newly read parameter is covered automatically. The policy engine is unchanged and stays spec-literal.

Docs only (pre-trust fetching). While a Trust Chain is being resolved, each untrusted superior's federation_fetch_endpoint is fetched before anything is verified against a Trust Anchor. On an AS, that happens for any client_id an unauthenticated caller names. This is now documented on:

  • Resolver.Resolve
  • federation.Dependencies.HTTP
  • AutomaticClientRepository
  • server.Dependencies.FederationHTTP
  • fapihttp.Config.AllowedPrivateHosts: an allow-listed host is reachable through this path by unauthenticated callers.
  • The OpenID Federation guide.

Breaking

This is fix!, and UPGRADING.md has a v0.61.0 section for each behavior change:

  • A static client can no longer authenticate with a key only its federation metadata publishes.
  • A federation client no longer uses Dependencies.ClientKeys.
  • An RP whose plural values or defaults fall outside the singular parameter's policy is narrowed or refused.
  • An empty plural array is refused.
  • "" is refused for any string parameter registration reads.

Tests

  • server/automatic_registration_static_keys_test.go: a static client also resolvable through federation still gets a token with its static key. An assertion signed with the federation-published key is now refused. This test fails on main.
  • federation: static and federation key-source separation (empty, error and unavailable-store cases); policy limits on plural algorithms and methods and on defaults, plus the subset_of-emptied list; "" refusal for several parameters under essential.
  • internal/federation: PermittedStrings.

GOTOOLCHAIN=go1.26.9 go test ./..., go vet ./... (including GOARCH=arm), golangci-lint run ./... and the federated-union example tests all pass.

🤖 Generated with Claude Code

osanderson and others added 2 commits October 25, 2026 21:04
Resolving a Trust Chain fetches each authority hint's Entity
Configuration and the Subordinate Statement from the
federation_fetch_endpoint it names before any statement on the path is
verified against a Trust Anchor, so the party controlling the subject
chooses those URLs (host, port, path and query, with only "sub" added).
On an authorization server with automatic registration this happens
for any client_id an unauthenticated caller names, before client
authentication.

Document this on Resolver.Resolve, Dependencies.HTTP,
AutomaticClientRepository, server.Dependencies.FederationHTTP and the
OpenID Federation guide, and on fapihttp.Config.AllowedPrivateHosts:
a host allow-listed there is reachable through this path by
unauthenticated callers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s within policy

AutomaticClientKeySource fell back to a client's federation-published
keys whenever the underlying key source returned an error or no key,
including for statically registered clients. A statically registered
client whose client_id is also an Entity Identifier resolving under a
configured Trust Anchor could therefore be authenticated with a key its
federation metadata publishes, which any superior in its Trust Chain can
set, and receive the static registration's scopes and grants. Keys now
come from the same source as the registration: a client the underlying
client repository resolves gets its keys from the underlying key source
alone, a federation client from its openid_relying_party metadata alone,
and an unavailable client store is an error.

A superior's one_of or value policy on token_endpoint_auth_signing_alg
or token_endpoint_auth_method only checked that parameter, and only when
present (OpenID Federation 1.0 section 6.1.3.1.4), so an RP could leave
it out and register a forbidden value through its OpenID Connect RP
Metadata Choices 1.0 plural sibling, or take the OP's default
algorithms. The resolver now keeps the merged metadata policy on the
ResolvedEntity, and automatic registration limits the plural values and
the defaults to what the policy permits for the singular parameter,
refusing an RP left with none. An empty plural array, which subset_of
can produce, is refused instead of being read as absent. Registering
every mutually supported method stays as section 12.1.4 recommends.

An empty string still counted as present to a metadata policy, so it
met an essential policy, while automatic registration read it as unset
for parameters other than the encryption algorithms. Automatic
registration now refuses an empty string for every string parameter it
reads.

BREAKING CHANGE: a statically registered client is authenticated only
with keys from Dependencies.ClientKeys, never with ones its federation
metadata publishes, and a federation client only with its federation
keys. Automatic registration no longer registers a client authentication
method or assertion algorithm outside a superior's policy on
token_endpoint_auth_method or token_endpoint_auth_signing_alg, refuses
an empty token_endpoint_auth_methods_supported or
token_endpoint_auth_signing_alg_values_supported, and refuses an empty
string for any string parameter it reads. See UPGRADING.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.38095% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
federation/automatic_registration.go 92.59% 3 Missing and 3 partials ⚠️
internal/federation/policy.go 88.88% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

…checks into helpers

Keeps both tests' cognitive complexity within the analyzer's limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@osanderson
osanderson merged commit 608c79d into main Oct 25, 2026
18 checks passed
@osanderson
osanderson deleted the fix/federation-chain-review branch October 25, 2026 14:03
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.

1 participant