From 5111c915ada032c34f1ce570183b39a795a97bb4 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 11 Feb 2026 14:03:38 -0600 Subject: [PATCH 01/11] WordPress.org: Add application password authorization flow to wp-login.php. Provides a login-based authorization flow for creating application passwords, gated by a UUID-based app allowlist. After approval, renders the MCP client configuration for easy copy-paste setup. --- .../pub/authorize-application-login.php | 588 ++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100644 wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php new file mode 100644 index 0000000000..772086bbea --- /dev/null +++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php @@ -0,0 +1,588 @@ + Map of app_id UUID => app config. + */ +function wporg_get_allowed_apps(): array { + return array( + 'c4c73a54-96d7-47b9-9bdc-1a66b9b04505' => array( + 'name' => 'WordPress.org MCP', + 'hosts' => array(), + ), + ); +} + +/** + * Handles the authorize_application action on wp-login.php. + */ +function wporg_handle_authorize_application_login(): void { + if ( ! is_user_logged_in() ) { + $authorize_url = add_query_arg( + array( + 'action' => 'authorize_application', + 'app_id' => sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) ), + 'success_url' => sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ), + 'reject_url' => sanitize_url( wp_unslash( $_REQUEST['reject_url'] ?? '' ) ), + ), + site_url( 'wp-login.php', 'login' ) + ); + + wp_safe_redirect( wp_login_url( $authorize_url ) ); + exit; + } + + $error = null; + $new_password = ''; + + // Handle form submission. + if ( 'POST' === $_SERVER['REQUEST_METHOD'] + && isset( $_POST['wp_action'] ) && 'authorize_application_password' === $_POST['wp_action'] + ) { + check_admin_referer( 'authorize_application_password' ); + + $app_id = sanitize_text_field( wp_unslash( $_POST['app_id'] ) ); + $success_url = sanitize_url( wp_unslash( $_POST['success_url'] ) ); + $reject_url = sanitize_url( wp_unslash( $_POST['reject_url'] ) ); + $redirect = ''; + + // Re-validate POST values (don't trust hidden form fields). + $user = wp_get_current_user(); + $allowed_apps = wporg_get_allowed_apps(); + $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : ''; + $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' ); + + $is_valid = wporg_validate_authorize_app_request( $request, $user ); + if ( is_wp_error( $is_valid ) ) { + wp_die( + implode( ' ', $is_valid->get_error_messages() ), + __( 'Cannot Authorize Application' ) + ); + } + + if ( isset( $_POST['reject'] ) ) { + $redirect = $reject_url ?: admin_url(); + } elseif ( isset( $_POST['approve'] ) ) { + // Revoke any existing password for this app_id. + if ( $app_id ) { + $existing = WP_Application_Passwords::get_user_application_passwords( get_current_user_id() ); + foreach ( $existing as $item ) { + if ( isset( $item['app_id'] ) && $item['app_id'] === $app_id ) { + $deleted = WP_Application_Passwords::delete_application_password( get_current_user_id(), $item['uuid'] ); + if ( is_wp_error( $deleted ) ) { + $error = $deleted; + break; + } + } + } + } + + if ( ! is_wp_error( $error ) ) { + $created = WP_Application_Passwords::create_new_application_password( + get_current_user_id(), + array( + 'name' => $app_name, + 'app_id' => $app_id, + ) + ); + + if ( is_wp_error( $created ) ) { + $error = $created; + } else { + list( $new_password ) = $created; + + if ( $success_url ) { + $redirect = add_query_arg( + array( + 'site_url' => urlencode( 'https://wordpress.org' ), + 'user_login' => urlencode( wp_get_current_user()->user_login ), + 'password' => urlencode( $new_password ), + ), + $success_url + ); + } + } + } + } + + if ( $redirect ) { + // Explicitly not using wp_safe_redirect b/c sends to arbitrary domain. + wp_redirect( $redirect ); + exit; + } + } + + // Extract and validate request parameters. + $app_id = sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) ); + $success_url = sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ) ?: null; + + if ( ! empty( $_REQUEST['reject_url'] ) ) { + $reject_url = sanitize_url( wp_unslash( $_REQUEST['reject_url'] ) ); + } elseif ( $success_url ) { + $reject_url = add_query_arg( 'success', 'false', $success_url ); + } else { + $reject_url = null; + } + + $user = wp_get_current_user(); + $allowed_apps = wporg_get_allowed_apps(); + $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : ''; + $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' ); + + $is_valid = wporg_validate_authorize_app_request( $request, $user ); + if ( is_wp_error( $is_valid ) ) { + wp_die( + implode( ' ', $is_valid->get_error_messages() ), + __( 'Cannot Authorize Application' ) + ); + } + + if ( ! wp_is_application_passwords_available_for_user( $user ) ) { + if ( wp_is_application_passwords_available() ) { + $message = __( 'Application passwords are not available for your account. Please contact the site administrator for assistance.' ); + } else { + $message = __( 'Application passwords are not available.' ); + } + + wp_die( + $message, + __( 'Cannot Authorize Application' ), + array( + 'response' => 501, + 'link_text' => __( 'Go Back' ), + 'link_url' => $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url(), + ) + ); + } + + // Render the authorization form. + $title = __( 'Authorize Application' ); + $wp_error = new WP_Error(); + + if ( is_wp_error( $error ) ) { + $wp_error->add( 'authorize_error', $error->get_error_message() ); + } + + add_filter( 'login_site_html_link', '__return_empty_string' ); + + login_header( $title, '', $wp_error ); + + ?> +
+ + + + + + + +
+

+ +

+ +

+
+ + +

+ + +

+ ' . esc_html( $app_name ) . '' + ); + ?> +

+ +

+ + + ID, true ); + $blogs_count = count( $blogs ); + + if ( $blogs_count > 1 ) : + ?> +

+ the %2$s site in this installation that you have permissions on.', + 'This will grant access to all %2$s sites in this installation that you have permissions on.', + $blogs_count + ); + + if ( is_super_admin() ) { + /* translators: 1: URL to My Sites, 2: Number of sites. */ + $message = _n( + 'This will grant access to the %2$s site on the network as you have Super Admin rights.', + 'This will grant access to all %2$s sites on the network as you have Super Admin rights.', + $blogs_count + ); + } + + printf( $message, admin_url( 'my-sites.php' ), number_format_i18n( $blogs_count ) ); + ?> +

+ + +

+ + +

+ +

+ ' . esc_html( $host ) . '' + ); + } else { + _e( 'You will be given a password to manually enter into the application in question.' ); + } + ?> +

+ +
+ ' . esc_html( $app_name ) . '' + ); + } else { + /* translators: %s: Website name. */ + $message = sprintf( + __( 'Please log in to %s to proceed with authorization.' ), + get_bloginfo( 'name', 'display' ) + ); + } + + $errors->add( 'authorize_application', $message, 'message' ); + + return $errors; +} +add_filter( 'wp_login_errors', 'wporg_authorize_application_login_message', 10, 2 ); + +/** + * Outputs custom CSS for the authorize_application action on the login page. + */ +function wporg_authorize_application_login_styles(): void { + global $action; + + if ( 'authorize_application' !== $action ) { + return; + } + + ?> + + add( 'invalid_redirect_url', __( 'The provided URL is not valid.' ) ); + } elseif ( 'https' !== wp_parse_url( $request[ $key ], PHP_URL_SCHEME ) && ! $is_local ) { + $error->add( 'invalid_redirect_scheme', __( 'The URL must be served over a secure connection.' ) ); + } + } + + // Validate app_id format. + if ( empty( $request['app_id'] ) ) { + $error->add( 'missing_app_id', __( 'An application ID is required.' ) ); + } elseif ( ! wp_is_uuid( $request['app_id'] ) ) { + $error->add( 'invalid_app_id', __( 'The application ID must be a UUID.' ) ); + } + + // Validate app_id against the allowlist and check callback domains. + $allowed_apps = wporg_get_allowed_apps(); + + if ( ! empty( $request['app_id'] ) && wp_is_uuid( $request['app_id'] ) ) { + if ( ! isset( $allowed_apps[ $request['app_id'] ] ) ) { + $error->add( 'unauthorized_app', __( 'This application is not authorized.' ) ); + } else { + $allowed_hosts = $allowed_apps[ $request['app_id'] ]['hosts']; + + if ( empty( $allowed_hosts ) ) { + if ( ! empty( $request['success_url'] ) || ! empty( $request['reject_url'] ) ) { + $error->add( 'redirect_not_allowed', __( 'This application does not support callback URLs.' ) ); + } + } else { + foreach ( array( 'success_url', 'reject_url' ) as $key ) { + if ( empty( $request[ $key ] ) ) { + continue; + } + + $host = wp_parse_url( $request[ $key ], PHP_URL_HOST ); + if ( ! $host || ! in_array( $host, $allowed_hosts, true ) ) { + $error->add( 'unauthorized_redirect', __( 'The callback URL does not point to an allowed domain.' ) ); + } + } + } + } + } + + /** This action is documented in wp-admin/includes/user.php */ + do_action( 'wp_authorize_application_password_request_errors', $error, $request, $user ); + + if ( $error->has_errors() ) { + return $error; + } + + return true; +} + +/** + * Renders the MCP client configuration after application password creation. + * + * @param string $new_password The newly created application password. + * @param array $request The request data. + * @param WP_User $user The user who authorized the application. + */ +function wporg_render_mcp_config( string $new_password, array $request, WP_User $user ): void { + $mcp_config = array( + 'mcpServers' => array( + 'wporg-mcp-server' => array( + 'command' => 'npx', + 'args' => array( '-y', '@automattic/mcp-wordpress-remote@latest' ), + 'env' => array( + 'WP_API_URL' => rest_url( 'mcp/wporg' ), + 'WP_API_USERNAME' => $user->user_login, + 'WP_API_PASSWORD' => WP_Application_Passwords::chunk_password( $new_password ), + ), + ), + ), + ); + $json = wp_json_encode( $mcp_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + $json = str_replace( ' ', ' ', $json ); // Use 2-space indentation instead of PHP's default 4-space. + + ?> +
+

+ + + + +
+ + Date: Wed, 11 Feb 2026 14:35:37 -0600 Subject: [PATCH 02/11] WordPress.org: Only render MCP config for the MCP app. Guard wporg_render_mcp_config with an app_id check so it only renders for the WordPress.org MCP application, not for any other app that may be added to the allowlist in the future. --- .../wp-content/mu-plugins/pub/authorize-application-login.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php index 772086bbea..4b47cb03c1 100644 --- a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php +++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php @@ -529,6 +529,10 @@ function wporg_validate_authorize_app_request( array $request, WP_User $user ): * @param WP_User $user The user who authorized the application. */ function wporg_render_mcp_config( string $new_password, array $request, WP_User $user ): void { + if ( ( $request['app_id'] ?? '' ) !== 'c4c73a54-96d7-47b9-9bdc-1a66b9b04505' ) { + return; + } + $mcp_config = array( 'mcpServers' => array( 'wporg-mcp-server' => array( From 9aa130f45e3aeff89020a91068176aef42fd777c Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 11 Feb 2026 14:38:43 -0600 Subject: [PATCH 03/11] WordPress.org: Fix double URL encoding and case-sensitive host comparison. Remove redundant urlencode() calls in add_query_arg() which already handles encoding internally. Normalize hostnames to lowercase before comparing against the allowlist since DNS is case-insensitive. --- .../mu-plugins/pub/authorize-application-login.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php index 4b47cb03c1..7e29eb678d 100644 --- a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php +++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php @@ -105,9 +105,9 @@ function wporg_handle_authorize_application_login(): void { if ( $success_url ) { $redirect = add_query_arg( array( - 'site_url' => urlencode( 'https://wordpress.org' ), - 'user_login' => urlencode( wp_get_current_user()->user_login ), - 'password' => urlencode( $new_password ), + 'site_url' => 'https://wordpress.org', + 'user_login' => wp_get_current_user()->user_login, + 'password' => $new_password, ), $success_url ); @@ -502,8 +502,8 @@ function wporg_validate_authorize_app_request( array $request, WP_User $user ): continue; } - $host = wp_parse_url( $request[ $key ], PHP_URL_HOST ); - if ( ! $host || ! in_array( $host, $allowed_hosts, true ) ) { + $host = strtolower( wp_parse_url( $request[ $key ], PHP_URL_HOST ) ); + if ( ! $host || ! in_array( $host, array_map( 'strtolower', $allowed_hosts ), true ) ) { $error->add( 'unauthorized_redirect', __( 'The callback URL does not point to an allowed domain.' ) ); } } From 80d0c385232615eddb6dddf829ccd47278805868 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 11 Feb 2026 14:41:07 -0600 Subject: [PATCH 04/11] WordPress.org: Gate application password authorization to login.wordpress.org. Only load the mu-plugin on blog ID 350 (login.wordpress.org) so the authorization flow is not available on other sites in the multisite. --- .../mu-plugins/pub/authorize-application-login.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php index 7e29eb678d..1f90d105e8 100644 --- a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php +++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php @@ -6,6 +6,11 @@ declare( strict_types = 1 ); +// Only load on login.wordpress.org. +if ( get_current_blog_id() !== 350 ) { + return; +} + /** * Returns the registered applications, their names, and allowed callback domains. * From 77e98e377c93b58fcc176944c1066a05b37f3ba9 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 11 Feb 2026 15:28:48 -0600 Subject: [PATCH 05/11] WordPress.org: Rename authorize-application-login.php to login-application-passwords.php. --- ...rize-application-login.php => login-application-passwords.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename wordpress.org/public_html/wp-content/mu-plugins/pub/{authorize-application-login.php => login-application-passwords.php} (100%) diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php similarity index 100% rename from wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php rename to wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php From 47bef8099588227664211d3e04eb0cf97aa96fb3 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 12 Feb 2026 08:12:06 -0600 Subject: [PATCH 06/11] Apply suggestions from code review Co-authored-by: Dion Hulse --- .../mu-plugins/pub/login-application-passwords.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php index 1f90d105e8..c0b8a54f8f 100644 --- a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php +++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php @@ -7,7 +7,8 @@ declare( strict_types = 1 ); // Only load on login.wordpress.org. -if ( get_current_blog_id() !== 350 ) { +// Limit to the login site on WordPress.org. Intentionally available to all sites in local environments. +if ( defined( 'WPORG_LOGIN_REGISTER_BLOGID' ) && get_current_blog_id() !== WPORG_LOGIN_REGISTER_BLOGID ) { return; } @@ -65,7 +66,7 @@ function wporg_handle_authorize_application_login(): void { // Re-validate POST values (don't trust hidden form fields). $user = wp_get_current_user(); $allowed_apps = wporg_get_allowed_apps(); - $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : ''; + $app_name = $allowed_apps[ $app_id ]['name'] ?? ''; $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' ); $is_valid = wporg_validate_authorize_app_request( $request, $user ); @@ -142,7 +143,7 @@ function wporg_handle_authorize_application_login(): void { $user = wp_get_current_user(); $allowed_apps = wporg_get_allowed_apps(); - $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : ''; + $app_name = $allowed_apps[ $app_id ]['name'] ?? ''; $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' ); $is_valid = wporg_validate_authorize_app_request( $request, $user ); From 448d94560c647e347368433a88006d24f8dbc399 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 12 Feb 2026 09:17:47 -0600 Subject: [PATCH 07/11] WordPress.org: Address code review feedback on application password authorization. Encode values passed to add_query_arg() to match core's authorize-application.php, use null-coalesce consistently, and remove redundant comment. --- .../mu-plugins/pub/login-application-passwords.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php index c0b8a54f8f..4f41de91ee 100644 --- a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php +++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php @@ -6,7 +6,6 @@ declare( strict_types = 1 ); -// Only load on login.wordpress.org. // Limit to the login site on WordPress.org. Intentionally available to all sites in local environments. if ( defined( 'WPORG_LOGIN_REGISTER_BLOGID' ) && get_current_blog_id() !== WPORG_LOGIN_REGISTER_BLOGID ) { return; @@ -111,9 +110,9 @@ function wporg_handle_authorize_application_login(): void { if ( $success_url ) { $redirect = add_query_arg( array( - 'site_url' => 'https://wordpress.org', - 'user_login' => wp_get_current_user()->user_login, - 'password' => $new_password, + 'site_url' => urlencode( 'https://wordpress.org' ), + 'user_login' => urlencode( wp_get_current_user()->user_login ), + 'password' => urlencode( $new_password ), ), $success_url ); @@ -315,7 +314,7 @@ function wporg_authorize_application_login_message( WP_Error $errors, string $re $allowed_apps = wporg_get_allowed_apps(); $app_id = $query['app_id'] ?? ''; - $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : ''; + $app_name = $allowed_apps[ $app_id ]['name'] ?? ''; if ( $app_name ) { /* translators: 1: Website name, 2: Application name. */ From 7de8da5c7133205c87d60b14a9d75751265ca10d Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 17 Feb 2026 09:50:24 -0600 Subject: [PATCH 08/11] WordPress.org: Convert login-application-passwords from mu-plugin to plugin. --- .../login-application-passwords.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename wordpress.org/public_html/wp-content/{mu-plugins/pub => plugins/login-application-passwords}/login-application-passwords.php (99%) diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php similarity index 99% rename from wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php rename to wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php index 4f41de91ee..ef8cdedf32 100644 --- a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php +++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php @@ -1,7 +1,11 @@ Date: Tue, 17 Feb 2026 10:25:16 -0600 Subject: [PATCH 09/11] WordPress.org: Make allowed apps filterable and remove MCP-specific code. - Replace hardcoded app list with `wporg_login_application_passwords_allowed_apps` filter - Validate filter output with `_doing_it_wrong` for malformed entries - Default `hosts` to empty array via `wp_parse_args` - Add app hosts to `allowed_redirect_hosts` for `wp_safe_redirect` - Remove `wporg_render_mcp_config` and MCP-specific CSS (moving to wporg-abilities) - Improve escaping throughout --- .../login-application-passwords.php | 250 +++++++----------- 1 file changed, 99 insertions(+), 151 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php index ef8cdedf32..6ca8c7af1e 100644 --- a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php +++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php @@ -6,6 +6,8 @@ * Author: WordPress.org * Author URI: https://wordpress.org/ * License: GPLv2 or later + * + * @package login-application-passwords */ declare( strict_types = 1 ); @@ -25,12 +27,64 @@ * @return array Map of app_id UUID => app config. */ function wporg_get_allowed_apps(): array { - return array( - 'c4c73a54-96d7-47b9-9bdc-1a66b9b04505' => array( - 'name' => 'WordPress.org MCP', - 'hosts' => array(), - ), - ); + /** + * Filters the registered applications allowed to use the application password authorization flow. + * + * Each entry must be keyed by a valid UUID (the app_id) and contain an array + * with 'name' (non-empty string) and optionally 'hosts' (array of allowed + * callback domains). If 'hosts' is omitted, it defaults to an empty array, + * meaning the app does not support callback URLs. + * + * Example: + * + * add_filter( 'wporg_login_application_passwords_allowed_apps', function ( $apps ) { + * $apps['c4c73a54-96d7-47b9-9bdc-1a66b9b04505'] = array( + * 'name' => 'My Application', + * 'hosts' => array( 'example.com' ), + * ); + * return $apps; + * } ); + * + * @param array $apps Map of app_id UUID => app config. + */ + $apps = apply_filters( 'wporg_login_application_passwords_allowed_apps', array() ); + + $validated = array(); + + foreach ( $apps as $app_id => $config ) { + $config = wp_parse_args( $config, array( 'hosts' => array() ) ); + + if ( ! wp_is_uuid( $app_id ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( 'App ID must be a valid UUID. Got: %s', esc_html( $app_id ) ), + '1.0.0' + ); + continue; + } + + if ( ! is_array( $config ) || empty( $config['name'] ) || ! is_string( $config['name'] ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( 'App "%s" must have a non-empty "name" string.', esc_html( $app_id ) ), + '1.0.0' + ); + continue; + } + + if ( ! is_array( $config['hosts'] ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( 'App "%s" "hosts" must be an array.', esc_html( $app_id ) ), + '1.0.0' + ); + continue; + } + + $validated[ $app_id ] = $config; + } + + return $validated; } /** @@ -56,14 +110,15 @@ function wporg_handle_authorize_application_login(): void { $new_password = ''; // Handle form submission. - if ( 'POST' === $_SERVER['REQUEST_METHOD'] + if ( isset( $_SERVER['REQUEST_METHOD'] ) + && 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['wp_action'] ) && 'authorize_application_password' === $_POST['wp_action'] ) { check_admin_referer( 'authorize_application_password' ); - $app_id = sanitize_text_field( wp_unslash( $_POST['app_id'] ) ); - $success_url = sanitize_url( wp_unslash( $_POST['success_url'] ) ); - $reject_url = sanitize_url( wp_unslash( $_POST['reject_url'] ) ); + $app_id = sanitize_text_field( wp_unslash( $_POST['app_id'] ?? '' ) ); + $success_url = sanitize_url( wp_unslash( $_POST['success_url'] ?? '' ) ); + $reject_url = sanitize_url( wp_unslash( $_POST['reject_url'] ?? '' ) ); $redirect = ''; // Re-validate POST values (don't trust hidden form fields). @@ -75,13 +130,13 @@ function wporg_handle_authorize_application_login(): void { $is_valid = wporg_validate_authorize_app_request( $request, $user ); if ( is_wp_error( $is_valid ) ) { wp_die( - implode( ' ', $is_valid->get_error_messages() ), - __( 'Cannot Authorize Application' ) + wp_kses_post( implode( ' ', $is_valid->get_error_messages() ) ), + esc_html__( 'Cannot Authorize Application' ) ); } if ( isset( $_POST['reject'] ) ) { - $redirect = $reject_url ?: admin_url(); + $redirect = $reject_url ?: admin_url(); // phpcs:ignore Universal.Operators.DisallowShortTernary } elseif ( isset( $_POST['approve'] ) ) { // Revoke any existing password for this app_id. if ( $app_id ) { @@ -114,9 +169,9 @@ function wporg_handle_authorize_application_login(): void { if ( $success_url ) { $redirect = add_query_arg( array( - 'site_url' => urlencode( 'https://wordpress.org' ), - 'user_login' => urlencode( wp_get_current_user()->user_login ), - 'password' => urlencode( $new_password ), + 'site_url' => rawurlencode( 'https://wordpress.org' ), + 'user_login' => rawurlencode( wp_get_current_user()->user_login ), + 'password' => rawurlencode( $new_password ), ), $success_url ); @@ -126,15 +181,22 @@ function wporg_handle_authorize_application_login(): void { } if ( $redirect ) { - // Explicitly not using wp_safe_redirect b/c sends to arbitrary domain. - wp_redirect( $redirect ); + $allowed_hosts = $allowed_apps[ $app_id ]['hosts'] ?? array(); + add_filter( + 'allowed_redirect_hosts', + function ( $hosts ) use ( $allowed_hosts ) { + return array_merge( $hosts, $allowed_hosts ); + } + ); + + wp_safe_redirect( $redirect ); exit; } } // Extract and validate request parameters. $app_id = sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) ); - $success_url = sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ) ?: null; + $success_url = sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ) ?: null; // phpcs:ignore Universal.Operators.DisallowShortTernary if ( ! empty( $_REQUEST['reject_url'] ) ) { $reject_url = sanitize_url( wp_unslash( $_REQUEST['reject_url'] ) ); @@ -152,8 +214,8 @@ function wporg_handle_authorize_application_login(): void { $is_valid = wporg_validate_authorize_app_request( $request, $user ); if ( is_wp_error( $is_valid ) ) { wp_die( - implode( ' ', $is_valid->get_error_messages() ), - __( 'Cannot Authorize Application' ) + wp_kses_post( implode( ' ', $is_valid->get_error_messages() ) ), + esc_html__( 'Cannot Authorize Application' ) ); } @@ -165,12 +227,12 @@ function wporg_handle_authorize_application_login(): void { } wp_die( - $message, - __( 'Cannot Authorize Application' ), + esc_html( $message ), + esc_html__( 'Cannot Authorize Application' ), array( 'response' => 501, - 'link_text' => __( 'Go Back' ), - 'link_url' => $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url(), + 'link_text' => esc_html__( 'Go Back' ), + 'link_url' => esc_url( $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url() ), ) ); } @@ -209,27 +271,27 @@ function wporg_handle_authorize_application_login(): void {

-

+

-

+

' . esc_html( $app_name ) . '' ); ?>

-

+

' . esc_html( $host ) . '' ); } else { - _e( 'You will be given a password to manually enter into the application in question.' ); + esc_html_e( 'You will be given a password to manually enter into the application in question.' ); } ?>

@@ -321,16 +383,16 @@ function wporg_authorize_application_login_message( WP_Error $errors, string $re $app_name = $allowed_apps[ $app_id ]['name'] ?? ''; if ( $app_name ) { - /* translators: 1: Website name, 2: Application name. */ $message = sprintf( - __( 'Please log in to %1$s to authorize %2$s to connect to your account.' ), + /* translators: 1: Website name, 2: Application name. */ + esc_html__( 'Please log in to %1$s to authorize %2$s to connect to your account.' ), get_bloginfo( 'name', 'display' ), '' . esc_html( $app_name ) . '' ); } else { - /* translators: %s: Website name. */ $message = sprintf( - __( 'Please log in to %s to proceed with authorization.' ), + /* translators: %s: Website name. */ + esc_html__( 'Please log in to %s to proceed with authorization.' ), get_bloginfo( 'name', 'display' ) ); } @@ -358,10 +420,6 @@ function wporg_authorize_application_login_styles(): void { max-width: 90vw; } - .login-action-authorize_application #login:has(.mcp-config-display) { - width: 540px; - } - .login-action-authorize_application h2 { margin-bottom: 1em; } @@ -410,47 +468,6 @@ function wporg_authorize_application_login_styles(): void { width: 100%; } - form:has(.mcp-config-display) .authorize-application-password-display { - display: none; - } - - .login .mcp-config-display textarea { - font-family: Consolas, Monaco, monospace; - font-size: 12px; - line-height: 1.5; - width: 100%; - background: #f6f7f7; - border: 1px solid #dcdcde; - border-radius: 4px; - resize: none; - padding: 12px; - margin-top: 1em; - box-sizing: border-box; - } - - .login .mcp-config-display #copy-mcp-config { - float: none; - width: 100%; - text-align: center; - margin-top: 12px; - padding: 6px 0; - } - - .login .mcp-config-display .client-notes { - margin: 1.5em 0 0; - padding: 0; - list-style: none; - font-size: 12px; - color: #50575e; - line-height: 1.8; - } - - .login .mcp-config-display .client-notes code { - font-size: 11px; - background: #f6f7f7; - padding: 2px 5px; - border-radius: 2px; - } array( - 'wporg-mcp-server' => array( - 'command' => 'npx', - 'args' => array( '-y', '@automattic/mcp-wordpress-remote@latest' ), - 'env' => array( - 'WP_API_URL' => rest_url( 'mcp/wporg' ), - 'WP_API_USERNAME' => $user->user_login, - 'WP_API_PASSWORD' => WP_Application_Passwords::chunk_password( $new_password ), - ), - ), - ), - ); - $json = wp_json_encode( $mcp_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); - $json = str_replace( ' ', ' ', $json ); // Use 2-space indentation instead of PHP's default 4-space. - - ?> -
-

- - - -
    -
  • Claude Desktop
  • -
  • Claude Code.mcp.json' ); - ?>
  • -
  • Cursor
  • -
  • VS Code.vscode/mcp.json', - 'servers', - 'mcpServers' - ); - ?>
  • -
-
- - Date: Tue, 17 Feb 2026 13:41:52 -0600 Subject: [PATCH 10/11] Login Application Passwords: Add MCP client and extract assets into files. Introduces `MCP_Authorization` in `clients/mcp/` to register the WordPress.org MCP server via the new allowed-apps filter and render the client configuration after password creation. Moves inline CSS and JS into enqueued files: - `css/authorize-application.css` for the base authorization form. - `clients/mcp/style.css` for MCP config display styles. - `clients/mcp/copy-config.js` for the copy-to-clipboard button. --- .../clients/mcp/class-mcp-authorization.php | 138 ++++++++++++++++++ .../clients/mcp/copy-config.js | 11 ++ .../clients/mcp/style.css | 45 ++++++ .../css/authorize-application.css | 52 +++++++ .../login-application-passwords.php | 71 ++------- 5 files changed, 255 insertions(+), 62 deletions(-) create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/copy-config.js create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/style.css create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/css/authorize-application.css diff --git a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php new file mode 100644 index 0000000000..835ccc3b55 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php @@ -0,0 +1,138 @@ + 'WordPress.org MCP', + 'hosts' => array(), + ); + + return $apps; + } + + /** + * Renders the MCP client configuration after application password creation. + * + * @param string $new_password The newly created application password. + * @param array $request The request data. + * @param WP_User $user The user who authorized the application. + */ + public static function render_config( string $new_password, array $request, WP_User $user ): void { + if ( ( $request['app_id'] ?? '' ) !== self::APP_ID ) { + return; + } + + $mcp_config = array( + 'mcpServers' => array( + 'wporg-mcp-server' => array( + 'command' => 'npx', + 'args' => array( '-y', '@automattic/mcp-wordpress-remote@latest' ), + 'env' => array( + 'WP_API_URL' => rest_url( 'mcp/wporg' ), + 'WP_API_USERNAME' => $user->user_login, + 'WP_API_PASSWORD' => WP_Application_Passwords::chunk_password( $new_password ), + ), + ), + ), + ); + + $json = wp_json_encode( $mcp_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + $json = str_replace( ' ', ' ', $json ); + + ?> +
+

+ + + +
    +
  • Claude Desktop
  • +
  • Claude Code — + .mcp.json' ); + ?> +
  • +
  • Cursor
  • +
  • VS Code — + .vscode/mcp.json', + 'servers', + 'mcpServers' + ); + ?> +
  • +
+
+ - - Date: Tue, 17 Feb 2026 13:45:23 -0600 Subject: [PATCH 11/11] Login Application Passwords: Add authorization link to MCP client file header. --- .../clients/mcp/class-mcp-authorization.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php index 835ccc3b55..f16eee550a 100644 --- a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php +++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php @@ -5,6 +5,9 @@ * Registers the WordPress.org MCP server as an allowed application and * renders the client configuration after password creation. * + * Authorization link: + * https://login.wordpress.org/wp-login.php?action=authorize_application&app_id=c4c73a54-96d7-47b9-9bdc-1a66b9b04505 + * * @package login-application-passwords */