Skip to content

Return RFC 6749 error fields from the token endpoint - #89

Merged
roborourke merged 2 commits into
WP-API:mainfrom
humanmade:roborourke/oauth-token-errors
Oct 7, 2026
Merged

roborourke merged 2 commits into
WP-API:mainfrom
humanmade:roborourke/oauth-token-errors

Conversation

@roborourke

Copy link
Copy Markdown
Collaborator

Token endpoint errors now include the error and error_description fields from RFC 6749 section 5.2.

Before this change, errors used the WordPress shape only: code, message and data. A standard OAuth client looks for a top-level error like invalid_grant, so it could not tell why a request failed. The WordPress fields stay, so clients that read code keep working.

Each known error maps to an OAuth code. For example, an unknown client gives invalid_client, an expired code gives invalid_grant, and an unknown grant_type gives unsupported_grant_type. An error can also set its own code with an error key in its data. The PKCE errors in #85 already do this, so they will be covered once both PRs merge. Unknown errors become server_error.

Errors now use status 400, as the RFC asks. The exceptions are server_error, which stays 500, and a failed client login, which stays 401. That 401 now sends a WWW-Authenticate: Basic header.

Copilot flagged this on #85. It predates that PR, so the fix is here.

🤖 Generated with Claude Code

RFC 6749 section 5.2 says a token endpoint error is a JSON object with a top-level "error" code such as invalid_grant, plus an optional "error_description". The endpoint returned the WordPress error shape instead ({"code","message","data"}), so a standard OAuth client could not tell why a request failed.

A rest_request_after_callbacks filter on the token route now adds "error" and "error_description" to every error. That filter also sees argument validation errors, such as an unknown grant_type, which happen before the endpoint callback runs. rest_post_dispatch would work in production too, but WP_REST_Server::dispatch() does not run it, so it would be untested.

The WordPress fields are kept alongside the new ones, so existing clients that read "code" keep working. Replacing the body outright would be closer to the RFC but would break them.

Error codes come from a map of the endpoint's known WP_Error codes. An "error" key in the WP_Error data takes precedence, so new errors can set their OAuth code where they are created. The PKCE errors in WP-API#85 already do this. Unknown errors become server_error with a 500 status. Every other error is sent as a 400, except invalid_client, which keeps its 401 when the client failed to authenticate. A 401 now carries a WWW-Authenticate: Basic challenge, as section 5.2 requires. The resource server's Bearer challenge still stays off this route, so the WWW-Authenticate test now checks for the Basic challenge instead of no header.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Dispatched token errors are not formatted, and server_error can retain an incorrect status.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Updates token endpoint errors with RFC 6749 OAuth fields while preserving WordPress fields, status handling, and authentication challenges.

Changes:

  • Maps token errors to OAuth error codes.
  • Adds RFC-aligned statuses and Basic authentication challenges.
  • Expands token endpoint and header tests.
File Summary
tests/​test-www-authenticate.php Tests Basic authentication challenge behavior.
tests/​test-token-endpoint.php Tests OAuth error responses.
inc/​endpoints/​namespace.php Registers token endpoint hooks.
inc/​endpoints/​class-token.php Formats OAuth errors and statuses; contains unresolved response-hook and server_error status issues.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread inc/endpoints/class-token.php
Comment thread inc/endpoints/class-token.php
An unknown error that carried a 4xx status became server_error but kept its 4xx status, so the response said "server error" with a client-error status. Unknown errors now pick their OAuth code from the status: a 4xx becomes invalid_request with a 400, and anything else becomes server_error. server_error is always sent as a 500, including when an error sets it through its data.

Forcing every unknown error to 500 was the other option, but an error that already says it is the client's fault should not be reported as a server failure.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Three moderate findings remain in inc/endpoints/class-token.php.

Review effort: Lite
Findings: None

Resolved since last review (2)

@roborourke
roborourke requested a review from joehoyle October 7, 2026 10:18
@roborourke
roborourke merged commit 73f651c into WP-API:main Oct 7, 2026
44 checks passed
@roborourke
roborourke deleted the roborourke/oauth-token-errors branch October 7, 2026 12:01
roborourke added a commit to humanmade/OAuth2 that referenced this pull request Oct 7, 2026
Upstream WP-API#89 added a token endpoint filter that turns WP_Error responses into RFC 6749 section 5.2 errors. It uses the `error` key in the error data when present, otherwise a fixed map, and falls back to `invalid_request` for any other 4xx.

The PKCE verifier errors already set `error => invalid_grant`, so they come through unchanged. The fail-closed error for a stored challenge with no stored method did not, so after the merge it would have surfaced as `invalid_request`. The problem is the authorization code, not the request, so it now sets `invalid_grant`, matching how WP-API#89 maps the other corrupted-code error (`get_user.invalid_data`).

The array-valued verifier test only checked for a 400. With WP-API#89 in place the schema rejection becomes a proper `invalid_request` OAuth error, so the test now asserts that with the shared helper.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
roborourke added a commit to humanmade/OAuth2 that referenced this pull request Oct 7, 2026
…th-check

Upstream WP-API#89 added a token endpoint error formatter that adds the RFC 6749 section 5.2 error fields to every WP_Error from the token route, and sends a `Basic realm="oauth2"` challenge with every 401. This branch built its own challenge response, but only when the client authenticated with the Authorization header.

The formatter now owns the challenge. client_authentication_failed() always returns the plain WP_Error, so header and body failures both get the error fields and the same challenge. Before, a header failure came back as a pre-built WP_REST_Response that the formatter skipped, so it had a different realm and no `error` field. As a result, a failed body authentication now gets a challenge too. RFC 6749 allows this, and RFC 7235 requires a challenge on every 401, so the body-auth test now expects it.

Both sides added a request_token() test helper. Upstream's generic version keeps the name. This branch's code-exchange helper is renamed to exchange_code().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
roborourke added a commit to humanmade/OAuth2 that referenced this pull request Oct 7, 2026
Upstream WP-API#89 sends every token endpoint error except a 401 invalid_client as a 400, as RFC 6749 section 5.2 requires. Reusing an unknown code used to return 404, so the burn test's retry assertion broke after the merge and failed across the whole CI matrix.

The retry exists to prove the code was deleted, so it fails as an unknown code rather than as a bad verifier. The status no longer tells those apart, so the test now checks the invalid_grant OAuth error and the unknown-code WP_Error code.

Co-Authored-By: Claude Opus 5.5 (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.

3 participants