Validate the iss authorization-response parameter (RFC 9207 / SEP-2468) - #2921
Conversation
The OAuth client now validates the RFC 9207 iss parameter returned on the authorization-response redirect and the issuer of discovered authorization server metadata. iss is compared against the authorization server issuer with simple string comparison (RFC 3986 6.2.1): a mismatch is rejected, and a missing iss is rejected when the server advertised authorization_response_iss_parameter_supported. The callback_handler passed to OAuthClientProvider now returns an AuthorizationCodeResult (code/state/iss) instead of a tuple[str, str | None] so the redirect's iss reaches the validation. AnyHttpUrl appends a trailing slash to a bare authority, which would defeat the byte-exact comparison the spec requires, so the recorded issuer is compared with that lone slash stripped. Flips the auth/metadata-issuer-mismatch conformance scenario green; the auth/iss-* scenarios validate iss correctly but remain baselined because they reach DCR and trip the unimplemented SEP-837 application_type check.
Kludex
left a comment
There was a problem hiding this comment.
Pointers to the substantive logic — the rest of the diff is mostly the mechanical tuple[str, str | None] → AuthorizationCodeResult rename across tests/examples.
…perties - migration.md: spell out that omitting iss raises OAuthFlowError against servers advertising authorization_response_iss_parameter_supported, rather than merely disabling the check. - simple-auth-client example: convert get_state/get_iss to @Property.
With url_preserve_empty_path on the OAuth metadata models (#2925), the issuer parsed from the wire keeps its empty path, so str(oauth_metadata.issuer) is already the byte-exact value the authorization server transmitted. Remove _strip_authority_trailing_slash / raw_issuer and compare directly. This also fixes the false rejection the heuristic introduced for an issuer that genuinely ends in a trailing slash (e.g. Auth0's https://tenant.auth0.com/): its redirect iss now matches its advertised issuer instead of being stripped.
Conformance scenarios this PR addresses (SEP-2468)Run against
The six Both conformance legs pass with no unexpected or stale baseline entries. |
|
This pull request is included in pre-release v2.0.0a3 |
…r metadata Backport of the discovery half of #3398, together with the SEP-2352 credential binding that main has had since 2.0 and that change relies on (the parts of #2921, #2933, #2936, #2946 and #3181 it needs). The discovery step now knows which issuer the authorization server metadata must carry before fetching it: the PRM-advertised server, or on the 2025-03-26 no-PRM fallback the resource server's origin, which is what that well-known URL is built from (RFC 8414 section 3.3). On this line there was no issuer check at all; it now runs on both paths (`validate_metadata_issuer`, with `issuers_match`: exact, except that a root issuer with and without its trailing slash is the same server, which matters here because every parsed issuer renders with the slash). Stored client credentials gain the binding main has: the SDK records the issuer a registration was made with (`OAuthClientInformationFull.issuer`, never read from the registration response), and before any metadata is fetched a registration bound to a different issuer is discarded together with its tokens so the flow re-registers instead of presenting another server's credentials (`credentials_match_issuer`); newly registered clients are bound to the expected issuer when metadata for it was found and the registration actually went to that server. When no protected resource metadata location answers and one of them failed with a server error (or 429), the flow stops rather than reading that as "no such metadata". A 403 insufficient_scope step-up takes the same path as a 401: it discovers first when no metadata is held (extract_resource_metadata_from_www_auth also reads the `resource_metadata` hint from a 403 challenge) and then re-authorizes with the challenged scope; metadata already discovered in this process is reused as before. A 403 that is not a scope challenge is handed back to the caller instead of being retried unchanged. The conformance harness pinned on this line (0.1.13) still serves, in its two path-based authorization server scenarios, metadata whose `issuer` omits the advertised tenant path; those two scenarios are listed as expected failures until the pin moves past conformance 0.1.15, where the mock was corrected. Differences from the main change: - RFC 9207 `iss` validation is not included; the callback contract on 1.x stays `tuple[str, str | None]`. - 1.x has no SEP-2350 scope union, so a step-up asks for the challenged scope as it did before. - The root-slash allowance lives in `validate_metadata_issuer` itself (new on this line) rather than only on the legacy path.
Implements the first item of #2902: validate the RFC 9207
issauthorization-response parameter (SEP-2468).What changed
issquery parameter returned on the authorization-response redirect, and theissuerof discovered authorization server metadata. Comparison is simple string comparison per RFC 3986 §6.2.1 (no normalization): a mismatchedissis rejected, and a missingissis rejected when the AS advertisedauthorization_response_iss_parameter_supported.callback_handlernow returnsAuthorizationCodeResult(code/state/iss) instead oftuple[str, str | None], so the redirect'sissreaches the validation. Breaking change — documented indocs/migration.md.authorization_response_iss_parameter_supportedtoOAuthMetadata.AnyHttpUrlappends a trailing slash to a bare authority (https://as→https://as/), which would defeat the byte-exact comparison the spec requires (iss-supportedmust accepthttps://hostwhileiss-normalizedmust rejecthttps://host/), so the recorded issuer is compared with that lone authority slash stripped.Conformance
Flips
auth/metadata-issuer-mismatchgreen (removed from both expected-failures baselines). Theauth/iss-*scenarios validateisscorrectly but stay baselined because they reach Dynamic Client Registration and trip the unimplemented SEP-837application_typecheck — re-grouped under SEP-837 with accurate comments. Both conformance legs pass with no stale entries.Out of scope (separate PRs, tracked by #2902)
SEP-837 (
application_type), SEP-2350 (scope-union step-up), SEP-2352 (AS migration / credential binding). SEP-2207 (offline_access) is already done.AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.