Make WordPress Core


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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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}
Note: See TracChangeset for help on using the changeset viewer.