WordPress.org

Make WordPress Core

Changeset 49109


Ignore:
Timestamp:
10/08/2020 10:12:02 PM (3 weeks 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.