Skip to content

Add RFC 8414 OAuth2 authorization server metadata endpoint - #82

Merged
joehoyle merged 6 commits into
WP-API:mainfrom
humanmade:roborourke/oauth2-well-known-endpoints
Sep 17, 2026
Merged

joehoyle merged 6 commits into
WP-API:mainfrom
humanmade:roborourke/oauth2-well-known-endpoints

Conversation

@roborourke

@roborourke roborourke commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

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_supported is left out on purpose. PKCE isn't supported in this repo yet, so advertising it would be misleading.

phpcs and the full phpunit suite both pass.

🤖 Generated with Claude Code

roborourke and others added 4 commits September 16, 2026 16:47
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 joehoyle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread inc/well-known/namespace.php Outdated
$path = untrailingslashit( (string) wp_parse_url( $request_uri, PHP_URL_PATH ) );

if ( '/.well-known/oauth-authorization-server' === $path ) {
return 'oauth-authorization-server';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be a constant

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a constant (AUTHORIZATION_SERVER_DOCUMENT), with the well-known path derived from it. Pushed in 801a3b9.

roborourke and others added 2 commits September 16, 2026 19:32
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>
@roborourke

Copy link
Copy Markdown
Contributor Author

No, this did not work before. It does now.

The RFC form now works: a request for /.well-known/oauth-authorization-server/blog is handled. On a subdirectory network, that request lands on the root site. The root site switches to the subsite, so the issuer and endpoints in the response are the subsite's own.

We also match a site's own path, /blog/.well-known/oauth-authorization-server. This is the OpenID Connect style form, and it's the only form a subdirectory install can serve for itself.

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 WP_MULTISITE=1.

@joehoyle
joehoyle merged commit 994c3b4 into WP-API:main Sep 17, 2026
21 checks passed
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.

2 participants