Skip to content

oauthex: add MatchesResource helper (RFC 9728/8707 audience comparison)#970

Open
BorisTyshkevich wants to merge 2 commits into
modelcontextprotocol:mainfrom
Altinity:oauthex-matches-resource
Open

oauthex: add MatchesResource helper (RFC 9728/8707 audience comparison)#970
BorisTyshkevich wants to merge 2 commits into
modelcontextprotocol:mainfrom
Altinity:oauthex-matches-resource

Conversation

@BorisTyshkevich
Copy link
Copy Markdown

Summary

  • Add an exported `oauthex.MatchesResource(claims []string, resource string) bool` helper that compares an `aud` claim slice against an expected resource URL with the canonical-form (trailing slash) tolerance required by RFC 9728 and RFC 8707.
  • Tests cover the common shape combinations (canonical/trimmed in either direction, surrounding whitespace, multiple claims) plus negative cases ensuring scheme + path mismatches still fail.

Motivation

RFC 9728 §3.3 canonicalises the protected-resource identifier with a trailing slash, but RFC 8707 resource indicators frequently omit it. Upstream IdPs vary in which form they emit in the `aud` claim (Google trims, Auth0 retains, some clients round-trip whatever the IdP sent). Strict byte equality therefore fails routinely on legitimate setups.

Resource servers writing this comparison by hand all converge on the same trim-trailing-slash + whitespace logic; exporting a canonical helper saves every consumer from re-deriving it.

Test plan

  • `go test ./oauthex/... -run MatchesResource` passes.
  • Positive cases: exact match, trailing slash on either side, surrounding whitespace, multi-claim slice with one match.
  • Negative cases: empty slice, no match, path mismatch, scheme mismatch (HTTPS vs HTTP must still fail).

Real-world OAuth identity providers emit the protected-resource identifier
in both canonical (trailing slash, per RFC 9728 §3.3) and trimmed (no slash,
per common RFC 8707 setups) forms across `aud` claims. Strict byte equality
fails routinely on legitimate setups — claude.ai/ChatGPT round-trip the
form the upstream IdP sent them, which may differ from the operator's
configured form.

Adds an exported MatchesResource helper that accepts either form while
preserving exact-equality semantics for the rest of the URL (scheme, host,
path). Tests cover the four common shape combinations plus negative cases
where scheme/path mismatches must still fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review feedback from a re-read of RFC 9728 / RFC 3986:

- RFC 9728 §3.3 normatively cites RFC 3986 §6.2.1 "simple string
  comparison" (byte-equal), not the trailing-slash form. The spec
  basis for slash tolerance is RFC 3986 §6.2.3 (scheme-based
  normalization), which lists empty-path → "/" among the normalizations
  permitted *but not required*. Reframe the doc accordingly so reviewers
  don't have to re-derive the citation.

- Drop strings.TrimSpace. No RFC permits whitespace around a JWT aud
  URI; tolerating it silently accepts a malformed claim instead of
  surfacing it. If a real IdP ever emits whitespace, that's a bug in
  the IdP, not something a generic helper should paper over.

- Document the §6.2.3 features deliberately NOT applied (host case
  fold, default-port elision) so callers know two distinct registered
  resources cannot collide via these normalizations. Added negative
  tests pinning each boundary.

- Point byte-equal-strict callers at the direct == comparison so they
  know they don't need a separate helper.

Behavior change: claims with surrounding whitespace now return false
(previously returned true). The only realistic caller — token
validation — already gets malformed-aud rejection elsewhere via
jose/oidc parsers, so this surfaces upstream bugs rather than hiding
them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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