Make WordPress Core

Changeset 49109


Ignore:
Timestamp:
10/08/2020 10:12:02 PM (6 years ago)
Author:
TimothyBlynJacobs
Message:

REST API: Introduce Application Passwords for API authentication.

In WordPress 4.4 the REST API was first introduced. A few releases later in WordPress 4.7, the Content API endpoints were added, paving the way for Gutenberg and countless in-site experiences. In the intervening years, numerous plugins have built on top of the REST API. Many developers shared a common frustration, the lack of external authentication to the REST API.

This commit introduces Application Passwords to allow users to connect to external applications to their WordPress website. Users can generate individual passwords for each application, allowing for easy revocation and activity monitoring. An authorization flow is introduced to make the connection flow simple for users and application developers.

Application Passwords uses Basic Authentication, and by default is only available over an SSL connection.

Props georgestephanis, kasparsd, timothyblynjacobs, afercia, akkspro, andraganescu, arippberger, aristath, austyfrosty, ayesh, batmoo, bradyvercher, brianhenryie, helen, ipstenu, jeffmatson, jeffpaul, joostdevalk, joshlevinson, kadamwhite, kjbenk, koke, michael-arestad, Otto42, pekz0r, salzano, spacedmonkey, valendesigns.
Fixes #42790.

Location:
trunk
Files:
8 added
17 edited

Legend:

Unmodified
Added
Removed
  • trunk/Gruntfile.js

    r49101 r49109  
    239239                                files: {
    240240                                        [ WORKING_DIR + 'wp-admin/js/accordion.js' ]: [ './src/js/_enqueues/lib/accordion.js' ],
     241                                        [ WORKING_DIR + 'wp-admin/js/application-passwords.js' ]: [ './src/js/_enqueues/admin/application-passwords.js' ],
     242                                        [ WORKING_DIR + 'wp-admin/js/auth-app.js' ]: [ './src/js/_enqueues/admin/auth-app.js' ],
    241243                                        [ WORKING_DIR + 'wp-admin/js/code-editor.js' ]: [ './src/js/_enqueues/wp/code-editor.js' ],
    242244                                        [ WORKING_DIR + 'wp-admin/js/color-picker.js' ]: [ './src/js/_enqueues/lib/color-picker.js' ],
    … …  
    797799                                file_mappings: {
    798800                                        'src/wp-admin/js/accordion.js': 'src/js/_enqueues/lib/accordion.js',
     801                                        'src/wp-admin/js/application-passwords.js': 'src/js/_enqueues/admin/application-passwords.js',
     802                                        'src/wp-admin/js/auth-app.js': 'src/js/_enqueues/admin/auth-app.js',
    799803                                        'src/wp-admin/js/code-editor.js': 'src/js/_enqueues/wp/code-editor.js',
    800804                                        'src/wp-admin/js/color-picker.js': 'src/js/_enqueues/lib/color-picker.js',
  • trunk/src/wp-admin/css/forms.css

    r48419 r49109  
    757757}
    758758
    759 .form-table td fieldset p label { 
     759.form-table td fieldset p label {
    760760        margin-top: 0 !important;
    761761}
    … …  
    840840.color-option {
    841841        cursor: pointer;
     842}
     843
     844.new-application-password-notice.notice {
     845        margin-top: 20px;
     846        margin-bottom: 0;
    842847}
    843848
  • trunk/src/wp-admin/includes/list-table.php

    r48574 r49109  
    3434                'WP_Theme_Install_List_Table'                 => array( 'themes', 'theme-install' ),
    3535                'WP_Plugins_List_Table'                       => 'plugins',
     36                'WP_Application_Passwords_List_Table'         => 'application-passwords',
    3637
    3738                // Network Admin.
  • trunk/src/wp-admin/includes/user.php

    r48313 r49109  
    595595        );
    596596}
     597
     598/**
     599 * Checks if the Authorize Application Password request is valid.
     600 *
     601 * @since 5.6.0
     602 *
     603 * @param array   $request {
     604 *     The array of request data. All arguments are optional and may be empty.
     605 *
     606 *     @type string $app_name    The suggested name of the application.
     607 *     @type string $success_url The url the user will be redirected to after approving the application.
     608 *     @type string $reject_url  The url the user will be redirected to after rejecting the application.
     609 * }
     610 * @param WP_User $user The user authorizing the application.
     611 * @return true|WP_Error True if the request is valid, a WP_Error object contains errors if not.
     612 */
     613function wp_is_authorize_application_password_request_valid( $request, $user ) {
     614        $error = new WP_Error();
     615
     616        if ( ! empty( $request['success_url'] ) ) {
     617                $scheme = wp_parse_url( $request['success_url'], PHP_URL_SCHEME );
     618
     619                if ( 'http' === $scheme ) {
     620                        $error->add(
     621                                'invalid_redirect_scheme',
     622                                __( 'The success url must be served over a secure connection.' )
     623                        );
     624                }
     625        }
     626
     627        if ( ! empty( $request['reject_url'] ) ) {
     628                $scheme = wp_parse_url( $request['reject_url'], PHP_URL_SCHEME );
     629
     630                if ( 'http' === $scheme ) {
     631                        $error->add(
     632                                'invalid_redirect_scheme',
     633                                __( 'The rejection url must be served over a secure connection.' )
     634                        );
     635                }
     636        }
     637
     638        /**
     639         * Fires before application password errors are returned.
     640         *
     641         * @since 5.6.0
     642         *
     643         * @param WP_Error $error   The error object.
     644         * @param array    $request The array of request data.
     645         * @param WP_User  $user    The user authorizing the application.
     646         */
     647        do_action( 'wp_authorize_application_password_request_errors', $error, $request, $user );
     648
     649        if ( $error->has_errors() ) {
     650                return $error;
     651        }
     652
     653        return true;
     654}
  • trunk/src/wp-admin/user-edit.php

    r47808 r49109  
    2727
    2828wp_enqueue_script( 'user-profile' );
     29
     30if ( wp_is_application_passwords_available_for_user( $user_id ) ) {
     31        wp_enqueue_script( 'application-passwords' );
     32}
    2933
    3034if ( IS_PROFILE_PAGE ) {
    … …  
    703707        </table>
    704708
     709
     710                <?php if ( wp_is_application_passwords_available_for_user( $user_id ) ) : ?>
     711        <div class="application-passwords hide-if-no-js" id="application-passwords-section">
     712                <h2><?php _e( 'Application Passwords' ); ?></h2>
     713                <p><?php _e( 'Application passwords allow authentication via non-interactive systems, such as XML-RPC or the REST API, without providing your actual password. Application passwords can be easily revoked. They cannot be used for traditional logins to your website.' ); ?></p>
     714                <div class="create-application-password">
     715                        <label for="new_application_password_name" class="screen-reader-text"><?php _e( 'New Application Password Name' ); ?></label>
     716                        <input type="text" size="30" id="new_application_password_name" name="new_application_password_name" placeholder="<?php esc_attr_e( 'New Application Password Name' ); ?>" class="input" />
     717
     718                        <?php
     719                        /**
     720                         * Fires in the create Application Passwords form.
     721                         *
     722                         * @since 5.6.0
     723                         *
     724                         * @param WP_User $profileuser The current WP_User object.
     725                         */
     726                        do_action( 'wp_create_application_password_form', $profileuser );
     727                        ?>
     728
     729                        <?php submit_button( __( 'Add New' ), 'secondary', 'do_new_application_password', false ); ?>
     730                </div>
     731
     732                <div class="application-passwords-list-table-wrapper">
     733                        <?php
     734                        $application_passwords_list_table = _get_list_table( 'WP_Application_Passwords_List_Table', array( 'screen' => 'application-passwords-user' ) );
     735                        $application_passwords_list_table->prepare_items();
     736                        $application_passwords_list_table->display();
     737                        ?>
     738                </div>
     739        </div>
     740<?php endif; ?>
     741
    705742                <?php
    706743                if ( IS_PROFILE_PAGE ) {
    … …  
    788825        }
    789826</script>
     827
     828<?php if ( isset( $application_passwords_list_table ) ) : ?>
     829        <script type="text/html" id="tmpl-new-application-password">
     830                <div class="notice notice-success is-dismissible new-application-password-notice" role="alert" tabindex="0">
     831                        <p>
     832                                <?php
     833                                printf(
     834                                        /* translators: 1: Application name, 2: Generated password. */
     835                                        esc_html__( 'Your new password for %1$s is: %2$s' ),
     836                                        '<strong>{{ data.name }}</strong>',
     837                                        '<kbd>{{ data.password }}</kbd>'
     838                                );
     839                                ?>
     840                        </p>
     841                        <p><?php esc_attr_e( 'Be sure to save this in a safe location. You will not be able to retrieve it.' ); ?></p>
     842                        <button type="button" class="notice-dismiss">
     843                                <span class="screen-reader-text"><?php __( 'Dismiss this notice.' ); ?></span>
     844                        </button>
     845                </div>
     846        </script>
     847
     848        <script type="text/html" id="tmpl-application-password-row">
     849                <?php $application_passwords_list_table->print_js_template_row(); ?>
     850        </script>
     851<?php endif; ?>
    790852<?php
    791853require_once ABSPATH . 'wp-admin/admin-footer.php';
  • trunk/src/wp-includes/class-wp-rewrite.php

    r48585 r49109  
    15101510                $rules  = "<IfModule mod_rewrite.c>\n";
    15111511                $rules .= "RewriteEngine On\n";
     1512                $rules .= 'RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]';
    15121513                $rules .= "RewriteBase $home_root\n";
    15131514
  • trunk/src/wp-includes/default-filters.php

    r48966 r49109  
    277277add_action( 'auth_cookie_bad_hash', 'rest_cookie_collect_status' );
    278278add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' );
     279add_action( 'application_password_failed_authentication', 'rest_application_password_collect_status' );
     280add_action( 'application_password_did_authenticate', 'rest_application_password_collect_status' );
     281add_filter( 'rest_authentication_errors', 'rest_application_password_check_errors', 90 );
    279282add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 );
    280283
    … …  
    428431add_filter( 'authenticate', 'wp_authenticate_username_password', 20, 3 );
    429432add_filter( 'authenticate', 'wp_authenticate_email_password', 20, 3 );
     433add_filter( 'authenticate', 'wp_authenticate_application_password', 20, 3 );
    430434add_filter( 'authenticate', 'wp_authenticate_spam_check', 99 );
    431435add_filter( 'determine_current_user', 'wp_validate_auth_cookie' );
    432436add_filter( 'determine_current_user', 'wp_validate_logged_in_cookie', 20 );
     437add_filter( 'determine_current_user', 'wp_validate_application_password', 20 );
    433438
    434439// Split term updates.
  • trunk/src/wp-includes/load.php

    r49023 r49109  
    8787                $PHP_SELF            = $_SERVER['PHP_SELF'];
    8888        }
     89
     90        wp_populate_basic_auth_from_authorization_header();
     91}
     92
     93/**
     94 * Populates the Basic Auth server details from the Authorization header.
     95 *
     96 * Some servers running in CGI or FastCGI mode don't pass the Authorization
     97 * header on to WordPress.  If it's been rewritten to the `HTTP_AUTHORIZATION` header,
     98 * fill in the proper $_SERVER variables instead.
     99 *
     100 * @since 5.6.0
     101 */
     102function wp_populate_basic_auth_from_authorization_header() {
     103        // If we don't have anything to pull from, return early.
     104        if ( ! isset( $_SERVER['HTTP_AUTHORIZATION'] ) && ! isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
     105                return;
     106        }
     107
     108        // If either PHP_AUTH key is already set, do nothing.
     109        if ( isset( $_SERVER['PHP_AUTH_USER'] ) || isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     110                return;
     111        }
     112
     113        // From our prior conditional, one of these must be set.
     114        $header = isset( $_SERVER['HTTP_AUTHORIZATION'] ) ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
     115
     116        // Test to make sure the pattern matches expected.
     117        if ( ! preg_match( '%^Basic [a-z\d/+]*={0,2}$%i', $header ) ) {
     118                return;
     119        }
     120
     121        // Removing `Basic ` the token would start six characters in.
     122        $token    = substr( $header, 6 );
     123        $userpass = base64_decode( $token );
     124
     125        list( $user, $pass ) = explode( ':', $userpass );
     126
     127        // Now shove them in the proper keys where we're expecting later on.
     128        $_SERVER['PHP_AUTH_USER'] = $user;
     129        $_SERVER['PHP_AUTH_PW']   = $pass;
    89130}
    90131
  • trunk/src/wp-includes/rest-api.php

    r49108 r49109  
    210210
    211211        add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
     212        add_filter( 'rest_index', 'rest_add_application_passwords_to_index' );
    212213}
    213214
    … …  
    263264        // Users.
    264265        $controller = new WP_REST_Users_Controller;
     266        $controller->register_routes();
     267
     268        // Application Passwords
     269        $controller = new WP_REST_Application_Passwords_Controller();
    265270        $controller->register_routes();
    266271
    … …  
    311316        $controller = new WP_REST_Block_Directory_Controller();
    312317        $controller->register_routes();
    313 
    314318}
    315319
    … …  
    10331037
    10341038        $wp_rest_auth_cookie = true;
     1039}
     1040
     1041/**
     1042 * Collects the status of authenticating with an application password.
     1043 *
     1044 * @since 5.6.0
     1045 *
     1046 * @global WP_User|WP_Error|null $wp_rest_application_password_status
     1047 *
     1048 * @param WP_Error $user_or_error The authenticated user or error instance.
     1049 */
     1050function rest_application_password_collect_status( $user_or_error ) {
     1051        global $wp_rest_application_password_status;
     1052
     1053        $wp_rest_application_password_status = $user_or_error;
     1054}
     1055
     1056/**
     1057 * Checks for errors when using application password-based authentication.
     1058 *
     1059 * @since 5.6.0
     1060 *
     1061 * @global WP_User|WP_Error|null $wp_rest_application_password_status
     1062 *
     1063 * @param WP_Error|null|true $result Error from another authentication handler,
     1064 *                                   null if we should handle it, or another value if not.
     1065 * @return WP_Error|null|true WP_Error if the application password is invalid, the $result, otherwise true.
     1066 */
     1067function rest_application_password_check_errors( $result ) {
     1068        global $wp_rest_application_password_status;
     1069
     1070        if ( ! empty( $result ) ) {
     1071                return $result;
     1072        }
     1073
     1074        if ( is_wp_error( $wp_rest_application_password_status ) ) {
     1075                $data = $wp_rest_application_password_status->get_error_data();
     1076
     1077                if ( ! isset( $data['status'] ) ) {
     1078                        $data['status'] = 401;
     1079                }
     1080
     1081                $wp_rest_application_password_status->add_data( $data );
     1082
     1083                return $wp_rest_application_password_status;
     1084        }
     1085
     1086        if ( $wp_rest_application_password_status instanceof WP_User ) {
     1087                return true;
     1088        }
     1089
     1090        return $result;
     1091}
     1092
     1093/**
     1094 * Adds Application Passwords info to the REST API index.
     1095 *
     1096 * @since 5.6.0
     1097 *
     1098 * @param WP_REST_Response $response The index response object.
     1099 * @return WP_REST_Response
     1100 */
     1101function rest_add_application_passwords_to_index( $response ) {
     1102        if ( ! wp_is_application_passwords_available() ) {
     1103                return $response;
     1104        }
     1105
     1106        $response->data['authentication']['application-passwords'] = array(
     1107                'endpoints' => array(
     1108                        'authorization' => admin_url( 'authorize-application.php' ),
     1109                ),
     1110        );
     1111
     1112        return $response;
    10351113}
    10361114
  • trunk/src/wp-includes/rest-api/class-wp-rest-server.php

    r49075 r49109  
    224224         * @see WP_REST_Server::dispatch()
    225225         *
     226         * @global WP_User $current_user The currently authenticated user.
     227         *
    226228         * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
    227229         *                     Default null.
    … …  
    229231         */
    230232        public function serve_request( $path = null ) {
     233                /* @var WP_User|null $current_user */
     234                global $current_user;
     235
     236                if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
     237                        /*
     238                         * If there is no current user authenticated via other means, clear
     239                         * the cached lack of user, so that an authenticate check can set it
     240                         * properly.
     241                         *
     242                         * This is done because for authentications such as Application
     243                         * Passwords, we don't want it to be accepted unless the current HTTP
     244                         * request is an API request, which can't always be identified early
     245                         * enough in evaluation.
     246                         */
     247                        $current_user = null;
     248                }
     249
    231250                $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
    232251                $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
  • trunk/src/wp-includes/script-loader.php

    r49104 r49109  
    10681068        $scripts->set_translations( 'password-strength-meter' );
    10691069
     1070        $scripts->add( 'application-passwords', "/wp-admin/js/application-passwords$suffix.js", array( 'jquery', 'wp-util', 'wp-api-request', 'wp-date', 'wp-i18n', 'wp-hooks', 'wp-a11y' ), false, 1 );
     1071        $scripts->set_translations( 'application-passwords' );
     1072
     1073        $scripts->add( 'auth-app', "/wp-admin/js/auth-app$suffix.js", array( 'jquery', 'wp-api-request', 'wp-i18n', 'wp-hooks' ), false, 1 );
     1074        $scripts->set_translations( 'auth-app' );
     1075
    10701076        $scripts->add( 'user-profile', "/wp-admin/js/user-profile$suffix.js", array( 'jquery', 'password-strength-meter', 'wp-util' ), false, 1 );
    10711077        $scripts->set_translations( 'user-profile' );
  • trunk/src/wp-includes/user.php

    r49090 r49109  
    296296
    297297        return $user;
     298}
     299
     300/**
     301 * Authenticates the user using an application password.
     302 *
     303 * @since 5.6.0
     304 *
     305 * @param WP_User|WP_Error|null $input_user WP_User or WP_Error object if a previous
     306 *                                          callback failed authentication.
     307 * @param string                $username   Username for authentication.
     308 * @param string                $password   Password for authentication.
     309 * @return WP_User|WP_Error WP_User on success, WP_Error on failure.
     310 */
     311function wp_authenticate_application_password( $input_user, $username, $password ) {
     312        if ( $input_user instanceof WP_User ) {
     313                return $input_user;
     314        }
     315
     316        $is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );
     317
     318        /**
     319         * Filters whether this is an API request that Application Passwords can be used on.
     320         *
     321         * By default, Application Passwords is available for the REST API and XML-RPC.
     322         *
     323         * @since 5.6.0
     324         *
     325         * @param bool $is_api_request If this is an acceptable API request.
     326         */
     327        $is_api_request = apply_filters( 'application_password_is_api_request', $is_api_request );
     328
     329        if ( ! $is_api_request ) {
     330                return $input_user;
     331        }
     332
     333        $error = null;
     334        $user  = get_user_by( 'login', $username );
     335
     336        if ( ! $user && is_email( $username ) ) {
     337                $user = get_user_by( 'email', $username );
     338        }
     339
     340        // If the login name is invalid, short circuit.
     341        if ( ! $user ) {
     342                if ( is_email( $username ) ) {
     343                        $error = new WP_Error(
     344                                'invalid_email',
     345                                __( 'Unknown email address. Check again or try your username.' )
     346                        );
     347                } else {
     348                        $error = new WP_Error(
     349                                'invalid_username',
     350                                __( 'Unknown username. Check again or try your email address.' )
     351                        );
     352                }
     353        } elseif ( ! wp_is_application_passwords_available_for_user( $user ) ) {
     354                $error = new WP_Error(
     355                        'application_passwords_disabled',
     356                        __( 'Application passwords are disabled for the requested user.' )
     357                );
     358        }
     359
     360        if ( $error ) {
     361                /**
     362                 * Fires when an application password failed to authenticate the user.
     363                 *
     364                 * @since 5.6.0
     365                 *
     366                 * @param WP_Error $error The authentication error.
     367                 */
     368                do_action( 'application_password_failed_authentication', $error );
     369
     370                return $error;
     371        }
     372
     373        /*
     374         * Strip out anything non-alphanumeric. This is so passwords can be used with
     375         * or without spaces to indicate the groupings for readability.
     376         *
     377         * Generated application passwords are exclusively alphanumeric.
     378         */
     379        $password = preg_replace( '/[^a-z\d]/i', '', $password );
     380
     381        $hashed_passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
     382
     383        foreach ( $hashed_passwords as $key => $item ) {
     384                if ( ! wp_check_password( $password, $item['password'], $user->ID ) ) {
     385                        continue;
     386                }
     387
     388                $error = new WP_Error();
     389
     390                /**
     391                 * Fires when an application password has been successfully checked as valid.
     392                 *
     393                 * This allows for plugins to add additional constraints to prevent an application password from being used.
     394                 *
     395                 * @since 5.6.0
     396                 *
     397                 * @param WP_Error $error    The error object.
     398                 * @param WP_User  $user     The user authenticating.
     399                 * @param array    $item     The details about the application password.
     400                 * @param string   $password The raw supplied password.
     401                 */
     402                do_action( 'wp_authenticate_application_password_errors', $error, $user, $item, $password );
     403
     404                if ( is_wp_error( $error ) && $error->has_errors() ) {
     405                        /** This action is documented in wp-includes/user.php */
     406                        do_action( 'application_password_failed_authentication', $error );
     407
     408                        return $error;
     409                }
     410
     411                WP_Application_Passwords::record_application_password_usage( $user->ID, $item['uuid'] );
     412
     413                /**
     414                 * Fires after an application password was used for authentication.
     415                 *
     416                 * @since 5.6.0
     417                 *
     418                 * @param WP_User $user The user who was authenticated.
     419                 * @param array   $item The application password used.
     420                 */
     421                do_action( 'application_password_did_authenticate', $user, $item );
     422
     423                return $user;
     424        }
     425
     426        $error = new WP_Error(
     427                'incorrect_password',
     428                __( 'The provided password is an invalid application password.' )
     429        );
     430
     431        /** This action is documented in wp-includes/user.php */
     432        do_action( 'application_password_failed_authentication', $error );
     433
     434        return $error;
     435}
     436
     437/**
     438 * Validates the application password credentials passed via Basic Authentication.
     439 *
     440 * @since 5.6.0
     441 *
     442 * @param int|bool $input_user User ID if one has been determined, false otherwise.
     443 * @return int|bool The authenticated user ID if successful, false otherwise.
     444 */
     445function wp_validate_application_password( $input_user ) {
     446        // Don't authenticate twice.
     447        if ( ! empty( $input_user ) ) {
     448                return $input_user;
     449        }
     450
     451        if ( ! wp_is_application_passwords_available() ) {
     452                return $input_user;
     453        }
     454
     455        // Check that we're trying to authenticate
     456        if ( ! isset( $_SERVER['PHP_AUTH_USER'] ) ) {
     457                return $input_user;
     458        }
     459
     460        $authenticated = wp_authenticate_application_password( null, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] );
     461
     462        if ( $authenticated instanceof WP_User ) {
     463                return $authenticated->ID;
     464        }
     465
     466        // If it wasn't a user what got returned, just pass on what we had received originally.
     467        return $input_user;
    298468}
    299469
    … …  
    39244094        return new WP_User_Request( $post );
    39254095}
     4096
     4097/**
     4098 * Checks if Application Passwords is globally available.
     4099 *
     4100 * By default, Application Passwords is available to all sites using SSL, but this function is
     4101 * filterable to adjust its availability.
     4102 *
     4103 * @since 5.6.0
     4104 *
     4105 * @return bool
     4106 */
     4107function wp_is_application_passwords_available() {
     4108        /**
     4109         * Filters whether Application Passwords is available.
     4110         *
     4111         * @since 5.6.0
     4112         *
     4113         * @param bool $available True if available, false otherwise.
     4114         */
     4115        return apply_filters( 'wp_is_application_passwords_available', is_ssl() );
     4116}
     4117
     4118/**
     4119 * Checks if Application Passwords is enabled for a specific user.
     4120 *
     4121 * By default all users can use Application Passwords, but this function is filterable to restrict
     4122 * availability to certain users.
     4123 *
     4124 * @since 5.6.0
     4125 *
     4126 * @param int|WP_User $user The user to check.
     4127 * @return bool
     4128 */
     4129function wp_is_application_passwords_available_for_user( $user ) {
     4130        if ( ! wp_is_application_passwords_available() ) {
     4131                return false;
     4132        }
     4133
     4134        if ( ! is_object( $user ) ) {
     4135                $user = get_userdata( $user );
     4136        }
     4137
     4138        if ( ! $user || ! $user->exists() ) {
     4139                return false;
     4140        }
     4141
     4142        /**
     4143         * Filters whether Application Passwords is available for a specific user.
     4144         *
     4145         * @since 5.6.0
     4146         *
     4147         * @param bool    $available True if available, false otherwise.
     4148         * @param WP_User $user      The user to check.
     4149         */
     4150        return apply_filters( 'wp_is_application_passwords_available_for_user', true, $user );
     4151}
  • trunk/src/wp-login.php

    r49078 r49109  
    13721372                        } elseif ( WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED === $action ) {
    13731373                                $errors->add( 'enter_recovery_mode', __( 'Recovery Mode Initialized. Please log in to continue.' ), 'message' );
     1374                        } elseif ( isset( $_GET['redirect_to'] ) && false !== strpos( $_GET['redirect_to'], 'wp-admin/authorize-application.php' ) ) {
     1375                                $query_component = wp_parse_url( $_GET['redirect_to'], PHP_URL_QUERY );
     1376                                parse_str( $query_component, $query );
     1377
     1378                                if ( ! empty( $query['app_name'] ) ) {
     1379                                        /* translators: 1: Website name, 2: Application name. */
     1380                                        $message = sprintf( 'Please log in to %1$s to authorize %2$s to connect to your account.', get_bloginfo( 'name', 'display' ), '<strong>' . esc_html( $query['app_name'] ) . '</strong>' );
     1381                                } else {
     1382                                        /* translators: Website name. */
     1383                                        $message = sprintf( 'Please log in to %s to proceed with authorization.', get_bloginfo( 'name', 'display' ) );
     1384                                }
     1385
     1386                                $errors->add( 'authorize_application', $message, 'message' );
    13741387                        }
    13751388                }
  • trunk/src/wp-settings.php

    r49103 r49109  
    237237require ABSPATH . WPINC . '/nav-menu-template.php';
    238238require ABSPATH . WPINC . '/admin-bar.php';
     239require ABSPATH . WPINC . '/class-wp-application-passwords.php';
    239240require ABSPATH . WPINC . '/rest-api.php';
    240241require ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php';
    … …  
    260261require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-plugins-controller.php';
    261262require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-directory-controller.php';
     263require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-application-passwords-controller.php';
    262264require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php';
    263265require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php';
  • trunk/tests/phpunit/tests/auth.php

    r48937 r49109  
    77class Tests_Auth extends WP_UnitTestCase {
    88        protected $user;
     9
     10        /**
     11         * @var WP_User
     12         */
    913        protected static $_user;
    1014        protected static $user_id;
    … …  
    3438                $this->user = clone self::$_user;
    3539                wp_set_current_user( self::$user_id );
     40        }
     41
     42        public function tearDown() {
     43                parent::tearDown();
     44
     45                // Cleanup all the global state.
     46                unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'] );
    3647        }
    3748
    … …  
    415426        }
    416427
     428        /**
     429         * HTTP Auth headers are used to determine the current user.
     430         *
     431         * @ticket 42790
     432         *
     433         * @covers ::wp_validate_application_password
     434         */
     435        public function test_application_password_authentication() {
     436                $user_id = $this->factory()->user->create(
     437                        array(
     438                                'user_login' => 'http_auth_login',
     439                                'user_pass'  => 'http_auth_pass', // Shouldn't be allowed for API login.
     440                        )
     441                );
     442
     443                // Create a new app-only password.
     444                list( $user_app_password ) = WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => 'phpunit' ) );
     445
     446                // Fake a REST API request.
     447                add_filter( 'application_password_is_api_request', '__return_true' );
     448                add_filter( 'wp_is_application_passwords_available', '__return_true' );
     449
     450                // Fake an HTTP Auth request with the regular account password first.
     451                $_SERVER['PHP_AUTH_USER'] = 'http_auth_login';
     452                $_SERVER['PHP_AUTH_PW']   = 'http_auth_pass';
     453
     454                $this->assertEquals(
     455                        0,
     456                        wp_validate_application_password( null ),
     457                        'Regular user account password should not be allowed for API authentication'
     458                );
     459
     460                // Not try with an App password instead.
     461                $_SERVER['PHP_AUTH_PW'] = $user_app_password;
     462
     463                $this->assertEquals(
     464                        $user_id,
     465                        wp_validate_application_password( null ),
     466                        'Application passwords should be allowed for API authentication'
     467                );
     468        }
     469
     470        /**
     471         * @ticket 42790
     472         */
     473        public function test_authenticate_application_password_respects_existing_user() {
     474                $this->assertSame( self::$_user, wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' ) );
     475        }
     476
     477        /**
     478         * @ticket 42790
     479         */
     480        public function test_authenticate_application_password_is_rejected_if_not_api_request() {
     481                add_filter( 'application_password_is_api_request', '__return_false' );
     482
     483                $this->assertNull( wp_authenticate_application_password( null, self::$_user->user_login, 'password' ) );
     484        }
     485
     486        /**
     487         * @ticket 42790
     488         */
     489        public function test_authenticate_application_password_invalid_username() {
     490                add_filter( 'application_password_is_api_request', '__return_true' );
     491
     492                $error = wp_authenticate_application_password( null, 'idonotexist', 'password' );
     493                $this->assertWPError( $error );
     494                $this->assertEquals( 'invalid_username', $error->get_error_code() );
     495        }
     496
     497        /**
     498         * @ticket 42790
     499         */
     500        public function test_authenticate_application_password_invalid_email() {
     501                add_filter( 'application_password_is_api_request', '__return_true' );
     502
     503                $error = wp_authenticate_application_password( null, 'idonotexist@example.org', 'password' );
     504                $this->assertWPError( $error );
     505                $this->assertEquals( 'invalid_email', $error->get_error_code() );
     506        }
     507
     508        /**
     509         * @ticket 42790
     510         */
     511        public function test_authenticate_application_password_not_allowed() {
     512                add_filter( 'application_password_is_api_request', '__return_true' );
     513                add_filter( 'wp_is_application_passwords_available', '__return_false' );
     514
     515                $error = wp_authenticate_application_password( null, self::$_user->user_login, 'password' );
     516                $this->assertWPError( $error );
     517                $this->assertEquals( 'application_passwords_disabled', $error->get_error_code() );
     518        }
     519
     520        /**
     521         * @ticket 42790
     522         */
     523        public function test_authenticate_application_password_not_allowed_for_user() {
     524                add_filter( 'application_password_is_api_request', '__return_true' );
     525                add_filter( 'wp_is_application_passwords_available', '__return_true' );
     526                add_filter( 'wp_is_application_passwords_available_for_user', '__return_false' );
     527
     528                $error = wp_authenticate_application_password( null, self::$_user->user_login, 'password' );
     529                $this->assertWPError( $error );
     530                $this->assertEquals( 'application_passwords_disabled', $error->get_error_code() );
     531        }
     532
     533        /**
     534         * @ticket 42790
     535         */
     536        public function test_authenticate_application_password_incorrect_password() {
     537                add_filter( 'application_password_is_api_request', '__return_true' );
     538                add_filter( 'wp_is_application_passwords_available', '__return_true' );
     539
     540                $error = wp_authenticate_application_password( null, self::$_user->user_login, 'password' );
     541                $this->assertWPError( $error );
     542                $this->assertEquals( 'incorrect_password', $error->get_error_code() );
     543        }
     544
     545        /**
     546         * @ticket 42790
     547         */
     548        public function test_authenticate_application_password_custom_errors() {
     549                add_filter( 'application_password_is_api_request', '__return_true' );
     550                add_filter( 'wp_is_application_passwords_available', '__return_true' );
     551
     552                add_action(
     553                        'wp_authenticate_application_password_errors',
     554                        static function ( WP_Error $error ) {
     555                                $error->add( 'my_code', 'My Error' );
     556                        }
     557                );
     558
     559                list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
     560
     561                $error = wp_authenticate_application_password( null, self::$_user->user_login, $password );
     562                $this->assertWPError( $error );
     563                $this->assertEquals( 'my_code', $error->get_error_code() );
     564        }
     565
     566        /**
     567         * @ticket 42790
     568         */
     569        public function test_authenticate_application_password_by_username() {
     570                add_filter( 'application_password_is_api_request', '__return_true' );
     571                add_filter( 'wp_is_application_passwords_available', '__return_true' );
     572
     573                list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
     574
     575                $user = wp_authenticate_application_password( null, self::$_user->user_login, $password );
     576                $this->assertInstanceOf( WP_User::class, $user );
     577                $this->assertEquals( self::$user_id, $user->ID );
     578        }
     579
     580        /**
     581         * @ticket 42790
     582         */
     583        public function test_authenticate_application_password_by_email() {
     584                add_filter( 'application_password_is_api_request', '__return_true' );
     585                add_filter( 'wp_is_application_passwords_available', '__return_true' );
     586
     587                list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
     588
     589                $user = wp_authenticate_application_password( null, self::$_user->user_email, $password );
     590                $this->assertInstanceOf( WP_User::class, $user );
     591                $this->assertEquals( self::$user_id, $user->ID );
     592        }
     593
     594        /**
     595         * @ticket 42790
     596         */
     597        public function test_authenticate_application_password_chunked() {
     598                add_filter( 'application_password_is_api_request', '__return_true' );
     599                add_filter( 'wp_is_application_passwords_available', '__return_true' );
     600
     601                list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
     602
     603                $user = wp_authenticate_application_password( null, self::$_user->user_email, WP_Application_Passwords::chunk_password( $password ) );
     604                $this->assertInstanceOf( WP_User::class, $user );
     605                $this->assertEquals( self::$user_id, $user->ID );
     606        }
    417607}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php

    r48937 r49109  
    119119                        '/wp/v2/users/(?P<id>[\\d]+)',
    120120                        '/wp/v2/users/me',
     121                        '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords',
     122                        '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)',
    121123                        '/wp/v2/comments',
    122124                        '/wp/v2/comments/(?P<id>[\\d]+)',
    … …  
    133135                );
    134136
    135                 $this->assertSame( $expected_routes, $routes );
     137                $this->assertSameSets( $expected_routes, $routes );
    136138        }
    137139
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r49103 r49109  
    39853985                "self": "http://example.org/index.php?rest_route=/wp/v2/users/me"
    39863986            }
     3987        },
     3988        "/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords": {
     3989            "namespace": "wp/v2",
     3990            "methods": [
     3991                "GET",
     3992                "POST",
     3993                "DELETE"
     3994            ],
     3995            "endpoints": [
     3996                {
     3997                    "methods": [
     3998                        "GET"
     3999                    ],
     4000                    "args": {
     4001                        "context": {
     4002                            "required": false,
     4003                            "default": "view",
     4004                            "enum": [
     4005                                "view",
     4006                                "embed",
     4007                                "edit"
     4008                            ],
     4009                            "description": "Scope under which the request is made; determines fields present in response.",
     4010                            "type": "string"
     4011                        }
     4012                    }
     4013                },
     4014                {
     4015                    "methods": [
     4016                        "POST"
     4017                    ],
     4018                    "args": {
     4019                        "name": {
     4020                            "required": true,
     4021                            "description": "The name of the application password.",
     4022                            "type": "string"
     4023                        }
     4024                    }
     4025                },
     4026                {
     4027                    "methods": [
     4028                        "DELETE"
     4029                    ],
     4030                    "args": []
     4031                }
     4032            ]
     4033        },
     4034        "/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)": {
     4035            "namespace": "wp/v2",
     4036            "methods": [
     4037                "GET",
     4038                "POST",
     4039                "PUT",
     4040                "PATCH",
     4041                "DELETE"
     4042            ],
     4043            "endpoints": [
     4044                {
     4045                    "methods": [
     4046                        "GET"
     4047                    ],
     4048                    "args": {
     4049                        "context": {
     4050                            "required": false,
     4051                            "default": "view",
     4052                            "enum": [
     4053                                "view",
     4054                                "embed",
     4055                                "edit"
     4056                            ],
     4057                            "description": "Scope under which the request is made; determines fields present in response.",
     4058                            "type": "string"
     4059                        }
     4060                    }
     4061                },
     4062                {
     4063                    "methods": [
     4064                        "POST",
     4065                        "PUT",
     4066                        "PATCH"
     4067                    ],
     4068                    "args": {
     4069                        "name": {
     4070                            "required": false,
     4071                            "description": "The name of the application password.",
     4072                            "type": "string"
     4073                        }
     4074                    }
     4075                },
     4076                {
     4077                    "methods": [
     4078                        "DELETE"
     4079                    ],
     4080                    "args": []
     4081                }
     4082            ]
    39874083        },
    39884084        "/wp/v2/comments": {
Note: See TracChangeset for help on using the changeset viewer.