Skip to content

Align endpoints with IndieAuth spec changes since 2020#311

Open
pfefferle wants to merge 5 commits into
trunkfrom
add/legacy-deprecation-warnings
Open

Align endpoints with IndieAuth spec changes since 2020#311
pfefferle wants to merge 5 commits into
trunkfrom
add/legacy-deprecation-warnings

Conversation

@pfefferle

Copy link
Copy Markdown
Member

Closes #294.

Audit result

Reviewed the code against the current IndieAuth living spec. Already implemented: iss parameter, metadata endpoint, introspection + revocation + userinfo endpoints, refresh tokens, optional me, JSON client metadata with h-app fallback. Non-PKCE clients are already rejected outright (stricter than spec). Revocation correctly advertises none auth per spec.

Deprecations (legacy behaviors removed from the spec — still work, now warn)

Legacy behavior Replacement
response_type=id / omitted response_type response_type=code
Token verification via GET on the token endpoint Introspection endpoint
Token revocation via action=revoke Revocation endpoint

Each emits a _doing_it_wrong() notice (logged with WP_DEBUG, hookable via doing_it_wrong_run) and sends Deprecation: true + Link: <…>; rel="deprecation" headers. Behavior is otherwise unchanged; custom actions via indieauth_token_action_handler are untouched.

New spec requirements implemented

  • redirect_uri verification: when scheme/host/port differ from client_id, the redirect_uri must match a redirect URL published by the client (client metadata redirect_uris, rel="redirect_uri" link tags, or Link headers). Enforced at the REST authorization endpoint and the consent form handler. Filters: pre_indieauth_client_redirect_uris (short-circuit/cache), indieauth_verify_redirect_uri (override).
  • Introspection authorization: the spec says the endpoint "MUST require some form of authorization". Default now requires an authenticated WordPress user (IndieAuth bearer token, application password, or session) and advertises Bearer; filter indieauth_introspection_auth_methods_supported to none to restore the old behavior. ⚠️ Breaking for unauthenticated introspection consumers.

Bug fixes (found by the new Client_Discovery tests)

  • Mf2\parse() inside namespace IndieAuth resolved to IndieAuth\Mf2\parse() — fatal for every HTML client since the namespacing refactor.
  • The DOMDocument fallback passed HTML to the constructor (which takes an XML version string) instead of loadHTML(), and crashed on missing titles/icons.

Testing

TDD throughout — every test was watched failing first. 81 tests / 210 assertions pass on PHP 7.4 and 8.3 (wp-env); phpcs clean.

pfefferle added 2 commits July 4, 2026 13:59
Addresses #294 for the paths that were removed from the specification
since 2020 but are still supported for older clients:

- response_type=id and omitted response_type on the authorization
  endpoint (removed in IndieAuth 1.1, response_type=code is required)
- Token verification via GET on the token endpoint (replaced by the
  introspection endpoint)
- Token revocation via action=revoke on the token endpoint (replaced
  by the revocation endpoint)

All three keep working unchanged, but now emit a _doing_it_wrong()
notice (logged when WP_DEBUG, visible in Query Monitor, hookable via
doing_it_wrong_run) and send Deprecation and Link headers so client
developers can discover the replacements.
Implements the spec requirements added since 2020 that were still missing:

- Authorization requests: if the scheme, host or port of redirect_uri
  differ from client_id, the redirect_uri must match one of the redirect
  URLs published by the client (JSON client metadata redirect_uris,
  rel=redirect_uri link tags, or Link headers). Enforced in the REST
  authorization endpoint and in the consent form handler, filterable
  via pre_indieauth_client_redirect_uris and
  indieauth_verify_redirect_uri.
- Introspection endpoint: the spec requires some form of authorization.
  Any authentication that establishes a WordPress user is accepted
  (IndieAuth bearer token, application password, session). Filtering
  indieauth_introspection_auth_methods_supported to none restores the
  previous unauthenticated behavior.
- Client_Discovery now extracts published redirect_uris and fixes two
  latent bugs from the namespacing refactor: Mf2\parse() resolved to
  IndieAuth\Mf2\parse() (fatal for HTML clients) and the DOMDocument
  fallback passed HTML to the constructor instead of loadHTML().
@pfefferle pfefferle requested a review from dshanske July 4, 2026 12:04
@pfefferle pfefferle self-assigned this Jul 4, 2026
@pfefferle pfefferle requested a review from a team July 8, 2026 10:54
@pfefferle pfefferle requested a review from snarfed July 8, 2026 10:55
- Add IndieAuth\same_origin() and use it in verify_redirect_uri(), which
  also gains default-port normalization to match the FedCM origin check.
- Add IndieAuth\add_deprecation_headers() and replace the three
  copy-pasted Deprecation/Link header blocks.
- Collapse the legacy response_type branch in the authorization GET
  handler into the single code() dispatch path.
- Cache discovered client redirect URIs in a short-lived transient; the
  authorization flow verifies twice per flow and re-fetched the client
  document each time.
- Merge the two identical deprecation tests into one data-provider test.

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.

Pull request overview

Updates the plugin’s IndieAuth endpoints to match post-2020 spec changes by (1) deprecating legacy behaviors while keeping backward compatibility, (2) tightening security requirements around redirect_uri verification and introspection authorization, and (3) fixing client discovery parsing issues uncovered via tests.

Changes:

  • Add deprecation notices + response headers for legacy response_type / token verification GET / action=revoke behaviors.
  • Enforce spec-required redirect_uri verification for cross-origin redirects during authorization + consent handling.
  • Require authorization for introspection by default; expand client discovery to extract published redirect URIs (JSON/HTML/Link headers) and fix parsing bugs.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
includes/rest/class-token-controller.php Emits _doing_it_wrong() for deprecated token behaviors and adds deprecation headers on success responses.
includes/rest/class-introspection-controller.php Changes introspection to require authorization by default via a permission callback, with a filter to re-enable unauthenticated use.
includes/rest/class-authorization-controller.php Adds legacy response_type deprecation signaling, implements redirect_uri verification, and caches discovered redirect URIs.
includes/functions.php Introduces same_origin() and add_deprecation_headers() helpers.
includes/class-client-discovery.php Adds redirect URI discovery (JSON/mf2/Link headers) and fixes HTML parsing / namespacing issues.
tests/phpunit/tests/includes/rest/class-test-token-controller.php Adds tests asserting deprecation signals for legacy token verification/revocation behaviors.
tests/phpunit/tests/includes/rest/class-test-introspection-controller.php Updates introspection tests for new auth requirement and filter-based opt-out.
tests/phpunit/tests/includes/rest/class-test-authorization-controller.php Adds tests for legacy response_type deprecation headers and redirect URI verification behavior.
tests/phpunit/tests/includes/class-test-client-discovery.php New unit tests for redirect URI extraction from JSON/HTML/Link headers.
readme.md Documents the new deprecations and spec-alignment changes under “Unreleased”.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +110 to 114
if ( 'localhost' === \wp_parse_url( $this->client_id, PHP_URL_HOST ) ) {
return;
}
$response = self::parse( $client_id );
$response = self::parse( $this->client_id );
if ( \is_wp_error( $response ) ) {
Comment on lines 174 to +178
if ( ! $token ) {
return new OAuth_Response( 'invalid_token', \__( 'Invalid access token', 'indieauth' ), 401 );
}
$token['active'] = 'true';
return \rest_ensure_response( $token );
return add_deprecation_headers( \rest_ensure_response( $token ) );
Comment on lines 207 to 212
if ( isset( $params['token'] ) ) {
$this->delete_token( $params['token'] );
return \__( 'The Token Provided is No Longer Valid', 'indieauth' );
return add_deprecation_headers( \rest_ensure_response( \__( 'The Token Provided is No Longer Valid', 'indieauth' ) ) );
} else {
return new OAuth_Response( 'invalid_request', \__( 'Revoke is Missing Required Parameter token', 'indieauth' ), 400 );
}
Comment thread includes/functions.php
Comment on lines +401 to +422
function same_origin( $url1, $url2 ) {
$parts1 = \wp_parse_url( $url1 );
$parts2 = \wp_parse_url( $url2 );

if ( ! isset( $parts1['scheme'], $parts1['host'], $parts2['scheme'], $parts2['host'] ) ) {
return false;
}

if ( $parts1['scheme'] !== $parts2['scheme'] || $parts1['host'] !== $parts2['host'] ) {
return false;
}

$defaults = array(
'http' => 80,
'https' => 443,
);
$default_port = isset( $defaults[ $parts1['scheme'] ] ) ? $defaults[ $parts1['scheme'] ] : null;
$port1 = isset( $parts1['port'] ) ? (int) $parts1['port'] : $default_port;
$port2 = isset( $parts2['port'] ) ? (int) $parts2['port'] : $default_port;

return $port1 === $port2;
}
Comment thread includes/functions.php
Comment on lines +435 to +440
function add_deprecation_headers( $response ) {
$response->header( 'Deprecation', 'true' );
$response->header( 'Link', '<https://github.com/indieweb/wordpress-indieauth/issues/294>; rel="deprecation"' );

return $response;
}
Comment on lines +293 to +297
$this->assertEquals( 200, $response->get_status(), 'Response: ' . wp_json_encode( $response ) );
$headers = $response->get_headers();
$this->assertArrayHasKey( 'Deprecation', $headers );
$this->assertStringContainsString( 'rel="deprecation"', $headers['Link'] );
}
Comment on lines +336 to +340
$this->assertEquals( 200, $response->get_status(), 'Response: ' . wp_json_encode( $response ) );
$headers = $response->get_headers();
$this->assertArrayHasKey( 'Deprecation', $headers );
$this->assertStringContainsString( 'rel="deprecation"', $headers['Link'] );
$this->assertFalse( self::get_access_token( $token ) );
Comment on lines +96 to +100
$this->assertEquals( 302, $response->get_status(), 'Response: ' . wp_json_encode( $response ) );
$headers = $response->get_headers();
$this->assertArrayHasKey( 'Deprecation', $headers );
$this->assertStringContainsString( 'rel="deprecation"', $headers['Link'] );
}
Comment on lines +131 to +145
add_filter(
'indieauth_introspection_auth_methods_supported',
function () {
return array( 'none' );
}
);
$token = self::set_access_token();
$response = $this->create_form( 'POST',
array(
'token' => $token
)
);
remove_all_filters( 'indieauth_introspection_auth_methods_supported' );
$this->assertEquals( 200, $response->get_status(), 'Response: ' . wp_json_encode( $response ) );
}
Comment on lines +143 to +161
add_filter(
'pre_indieauth_client_redirect_uris',
function () {
return array( 'https://other.example.net/redirect' );
}
);
$response = $this->create_get(
array(
'response_type' => 'code',
'client_id' => 'https://app.example.com',
'redirect_uri' => 'https://other.example.net/redirect',
'state' => '12345',
'code_challenge' => 'OfYAxt8zU2dAPDWQxTAUIteRzMsoj9QBdMIVEDOErUo',
'code_challenge_method' => 'S256',
)
);
remove_all_filters( 'pre_indieauth_client_redirect_uris' );
$this->assertEquals( 302, $response->get_status(), 'Response: ' . wp_json_encode( $response ) );
}
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.

Review updates to spec since 2020 for compatibility alerts

2 participants