Add RFC 8414 OAuth2 authorization server metadata endpoint - #82
Conversation
Adds two OAuth2 discovery endpoints so clients can find this plugin's endpoints and capabilities without hardcoding them: - /.well-known/oauth-authorization-server (RFC 8414) exposes the plugin's authorization and token endpoints, and the grant types it supports. - /.well-known/oauth-protected-resource (RFC 9728) advertises protected-resource metadata for this site. Any 401 REST response now also gets a `WWW-Authenticate: Bearer resource_metadata="..."` header, not scoped to a particular route, so any bearer-protected endpoint benefits rather than just one integration. This was ported and generalized from hm-rest-ability, where it didn't belong (that plugin's copy is being removed in humanmade/hm-rest-ability#42). One HM-specific behaviour was deliberately dropped in the port: the original defaulted its login-wall exemption to Human Made's own Require Login plugin. This plugin is meant to stay generic and potentially go upstream, so that default doesn't belong here. The exemption mechanism itself is kept as a filter, `oauth2.well_known_login_wall_exemptions`, but now defaults to empty. `code_challenge_methods_supported` (PKCE, RFC 8414) is intentionally left out for now, since PKCE support isn't merged into this repo's main yet (tracked in a separate open PR). Add it once that lands. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Composer dependencies and PHPUnit's cache file were getting installed locally but weren't ignored, risking an accidental commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop the RFC 9728 protected-resource metadata document and the WWW-Authenticate header that pointed to it. Both describe protecting a resource server, not this plugin's own OAuth2 authorization and token endpoints, so they don't belong here. That behaviour may return in a different plugin instead, most likely hm-rest-ability, but where it lives is still to be decided. RFC 8414 stays: the authorization-server metadata document at /.well-known/oauth-authorization-server only describes endpoints this plugin itself implements. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The oauth2.well_known_login_wall_exemptions filter let sites remove arbitrary init hooks via remove_action() before serving .well-known/ documents. That's a fragile pattern: it depends on knowing the exact hook, callback, and priority another plugin used, and breaks silently if any of those change. It also solved a problem nobody has hit yet. Drop it until there's a real need, and design for that need directly rather than guessing at a generic mechanism. With that hook gone, Well_Known only registers one action (parse_request), so its own bootstrap() wrapper added nothing. Bind that hook directly in inc/namespace.php's bootstrap(), matching how Endpoints\register is already bound there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
joehoyle
left a comment
There was a problem hiding this comment.
Nice, I think this is looking good. One comment and also I think we need to think about subsites
RFC 8414 §3 says the metadata location is formed "by inserting a well-known URI string into the authorization server's issuer identifier between the host component and the path component, if any". §3.1 gives the concrete example: for issuer https://example.com/issuer1 the client requests
GET /.well-known/oauth-authorization-server/issuer1 HTTP/1.1
Host: example.com
So for a site at https://example.com/blog, we advertise issuer: https://example.com/blog and a compliant client will fetch https://example.com/.well-known/oauth-authorization-server/blog. Can you confirm if that is going to work with this PR?
| $path = untrailingslashit( (string) wp_parse_url( $request_uri, PHP_URL_PATH ) ); | ||
|
|
||
| if ( '/.well-known/oauth-authorization-server' === $path ) { | ||
| return 'oauth-authorization-server'; |
There was a problem hiding this comment.
Made it a constant (AUTHORIZATION_SERVER_DOCUMENT), with the well-known path derived from it. Pushed in 801a3b9.
The identifier 'oauth-authorization-server' appeared three times as a literal string: as the returned document name, inside the /.well-known/ path, and in the equality check in maybe_serve_document(). Replace these with two constants, AUTHORIZATION_SERVER_DOCUMENT and AUTHORIZATION_SERVER_PATH, with the path derived from the document constant so they can't drift apart. tests/test-well-known.php deliberately keeps asserting the literal strings rather than the constants, so a typo in either constant still fails the tests instead of passing silently. Addresses review feedback from joehoyle on upstream PR WP-API#82. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The old path match only worked for a site at the domain root: it compared the request path against the exact string /.well-known/oauth-authorization-server. RFC 8414 §3 instead inserts the well-known path before the issuer's own path, so a site at https://example.com/blog is discovered at https://example.com/.well-known/oauth-authorization-server/blog. On a subdirectory multisite network that request always lands on the root site, which must then answer on the subsite's behalf rather than returning its own metadata. match_well_known_path() now recognises both this RFC form and the site's own path (/blog/.well-known/oauth-authorization-server, the OpenID Connect-style form), which is also the only form a subdirectory install can serve for itself. get_site_id_by_path() resolves a path to a site via an exact get_sites() lookup and returns null for unknown paths, so an unrecognised path 404s instead of silently falling back to the root site's metadata. get_metadata_for_site() switch_to_blog()s to the resolved site to build the document, so a subsite's issuer/authorization/token endpoints are its own, then restores the current blog. serve_authorization_server_metadata() was split into a pure get_authorization_server_metadata() (returns the array, still filterable via oauth2.well_known_authorization_server_metadata) and the existing send_json_document(), making the document contents unit-testable. Tests extended with multisite-only cases (skipped on single site via a require_multisite() helper) covering subsite resolution, the subsite's own issuer, and that the current blog is restored. Addresses review feedback from @joehoyle on WP-API#82 asking whether the RFC 8414 §3.1 issuer-path insertion form would resolve correctly for a site in a subdirectory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
No, this did not work before. It does now. The RFC form now works: a request for We also match a site's own path, An unknown path now 404s, instead of falling back to the root site's metadata. There are multisite tests covering all of this. They run in CI under |
Adds an RFC 8414 authorization server metadata endpoint at
/.well-known/oauth-authorization-server.This lets OAuth2 clients discover this plugin's authorization and token endpoints, and its supported grant types, from a standard JSON document instead of hardcoding URLs. Some clients, including MCP (Model Context Protocol) clients, expect this discovery document and use it to configure themselves automatically.
code_challenge_methods_supportedis left out on purpose. PKCE isn't supported in this repo yet, so advertising it would be misleading.phpcsand the fullphpunitsuite both pass.🤖 Generated with Claude Code