Fix FedCM authentication, scope consent, and code-binding issues#322
Open
pfefferle wants to merge 2 commits into
Open
Fix FedCM authentication, scope consent, and code-binding issues#322pfefferle wants to merge 2 commits into
pfefferle wants to merge 2 commits into
Conversation
- Exempt FedCM requests (Sec-Fetch-Dest: webidentity, fedcm routes only) from the REST cookie nonce check so the browser's credentialed FedCM fetches are not treated as unauthenticated. - Clamp RP-requested scopes to profile/email; the FedCM browser UI never shows scopes, so nothing beyond identity scopes may be granted there. - Enforce client_id binding on code redemption at the token endpoint and redirect_uri binding for non-FedCM codes; allow FedCM codes (issued without a redirect) to be redeemed without a redirect_uri at both endpoints, and stop storing the unused OOB placeholder redirect_uri. - Treat a missing port as the scheme default when validating the Origin header against the client_id. - Return FedCM Error API shaped errors from the assertion endpoint. - Serve the public config and client_metadata endpoints with Access-Control-Allow-Origin: * instead of reflected credentialed CORS.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens and unblocks the FedCM (Federated Credential Management) flow in the IndieAuth WordPress plugin by ensuring cookie-authenticated FedCM REST requests work without a REST nonce, clamping scopes to identity-safe values, and binding authorization codes to their original client parameters during redemption.
Changes:
- Exempt FedCM REST requests (scoped to the plugin’s FedCM routes) from WordPress REST cookie nonce enforcement.
- Clamp RP-requested scopes for FedCM assertion to
profile/email, and align assertion error responses with the FedCM Error API shape. - Enforce
client_idand conditionalredirect_uribinding during authorization code redemption (token + authorization endpoints), with corresponding PHPUnit coverage.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/phpunit/tests/includes/rest/class-test-token-controller.php | Adds tests for client_id/redirect_uri mismatch rejection and FedCM redemption without redirect_uri at the token endpoint. |
| tests/phpunit/tests/includes/rest/class-test-fedcm-controller.php | Adds tests for public CORS behavior, FedCM error shape, default-port origin handling, scope clamping, and REST nonce exemption behavior. |
| tests/phpunit/tests/includes/rest/class-test-authorization-controller.php | Adds a test ensuring FedCM codes can be redeemed without redirect_uri at the authorization endpoint. |
| includes/rest/class-token-controller.php | Makes redirect_uri optional at request-parse time and enforces client_id/redirect_uri binding during code verification (with FedCM exception). |
| includes/rest/class-fedcm-controller.php | Implements REST nonce exemption for FedCM routes, default-port origin equality, public CORS for non-user-data endpoints, FedCM Error API response shape, and scope clamping. |
| includes/rest/class-authorization-controller.php | Makes redirect_uri conditionally required based on whether the stored code is FedCM-marked. |
| includes/class-indieauth.php | Registers the FedCM REST-auth exemption filter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
423
to
427
| $token = $this->get_code( $code ); | ||
| $scopes = isset( $token['scope'] ) ? array_filter( explode( ' ', $token['scope'] ) ) : array(); | ||
|
|
||
| if ( ! $token ) { | ||
| return new OAuth_Response( 'invalid_grant', \__( 'Invalid authorization code', 'indieauth' ), 400 ); |
Comment on lines
+319
to
+335
| public function rest_authentication_errors( $result ) { | ||
| if ( null !== $result ) { | ||
| return $result; | ||
| } | ||
|
|
||
| if ( ! isset( $_SERVER['HTTP_SEC_FETCH_DEST'] ) || 'webidentity' !== $_SERVER['HTTP_SEC_FETCH_DEST'] ) { | ||
| return $result; | ||
| } | ||
|
|
||
| // Only exempt this plugin's FedCM routes. | ||
| $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; | ||
| if ( false === strpos( $request_uri, $this->namespace . '/' . $this->rest_base . '/' ) ) { | ||
| return $result; | ||
| } | ||
|
|
||
| return true; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A review of the FedCM implementation (#299) turned up one functional blocker and several security/spec issues. Every fix was developed test-first; the failing test reproducing each issue was written before the fix.
Fixes
FedCM endpoints saw logged-in users as logged out (functional blocker)
accountsandassertionrely onis_user_logged_in(), but core'srest_cookie_check_errors()treats any cookie-authenticated REST request without a_wpnonce/X-WP-Nonceas unauthenticated. The browser's FedCM machinery can never send a REST nonce, so in a real browser the accounts endpoint always answered 401 and the flow dead-ended. (PHPUnit couldn't catch it:wp_set_current_user()doesn't set$wp_rest_auth_cookie, so the nonce path never ran in tests.)The fix is a
rest_authentication_errorsfilter that exempts requests bearingSec-Fetch-Dest: webidentity— scoped to the plugin'sfedcm/routes only.Sec--prefixed headers are forbidden for cross-site JavaScript, so this does not open a CSRF hole; non-FedCM routes keep core's strict behavior (covered by tests).RPs could obtain arbitrary-scope tokens without scope consent
The assertion endpoint accepted whatever
params.scopethe RP sent and the token endpoint then issued a real access token for it — but the FedCM browser dialog never displays scopes, so a one-click sign-in could silently become acreate updatetoken. Requested scopes are now clamped toprofile/email; theindieauth_fedcm_scopefilter still allows deliberate opt-up.Code redemption never verified
client_id/redirect_uriverify_local_authorization_code()only checked PKCE — a code could be exchanged with a mismatchedclient_idorredirect_uri. Both are now enforced at the token endpoint. FedCM codes are issued without a redirect, so they are redeemable without aredirect_uriat both the token and authorization endpoints (keyed off the server-setfedcmmarker), and the unusedurn:ietf:wg:oauth:2.0:oobplaceholder is no longer stored.Smaller fixes
client_idofhttps://app.example.com:443/matches the browser'sOrigin: https://app.example.com.{"error": {"code": "invalid_request" | "unauthorized_client" | "access_denied"}}.config.jsonandclient_metadataendpoints sendAccess-Control-Allow-Origin: *without credentials; reflected credentialed CORS remains only onaccounts/assertionwhere the spec requires it.Testing instructions
npm run test:wp-env— 108 tests, includes new coverage for the nonce exemption (positive + two negative cases), scope clamping, code binding (mismatch rejection + FedCM redemption without redirect_uri), default-port origins, error shape, and public CORS.curl -b <cookies> -H "Sec-Fetch-Dest: webidentity" <site>/wp-json/indieauth/1.0/fedcm/accountsreturns the account list without a nonce; the same request without the header is still rejected, and core routes (e.g.
/wp/v2/users/me) still 401 without a nonce even with the header.Follow-ups (not in this PR)
IdentityProvider.register()still fires on every dashboard load; it should probably move behind an explicit button on the settings page.SameSite=None.