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.


git-svn-id: https://develop.svn.wordpress.org/trunk@49109 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
TimothyBlynJacobs
2020-10-08 22:12:02 +00:00
parent 79703088c4
commit 1856d0fe2a
25 changed files with 3493 additions and 3 deletions

View File

@@ -297,6 +297,176 @@ function wp_authenticate_cookie( $user, $username, $password ) {
return $user;
}
/**
* Authenticates the user using an application password.
*
* @since 5.6.0
*
* @param WP_User|WP_Error|null $input_user WP_User or WP_Error object if a previous
* callback failed authentication.
* @param string $username Username for authentication.
* @param string $password Password for authentication.
* @return WP_User|WP_Error WP_User on success, WP_Error on failure.
*/
function wp_authenticate_application_password( $input_user, $username, $password ) {
if ( $input_user instanceof WP_User ) {
return $input_user;
}
$is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );
/**
* Filters whether this is an API request that Application Passwords can be used on.
*
* By default, Application Passwords is available for the REST API and XML-RPC.
*
* @since 5.6.0
*
* @param bool $is_api_request If this is an acceptable API request.
*/
$is_api_request = apply_filters( 'application_password_is_api_request', $is_api_request );
if ( ! $is_api_request ) {
return $input_user;
}
$error = null;
$user = get_user_by( 'login', $username );
if ( ! $user && is_email( $username ) ) {
$user = get_user_by( 'email', $username );
}
// If the login name is invalid, short circuit.
if ( ! $user ) {
if ( is_email( $username ) ) {
$error = new WP_Error(
'invalid_email',
__( 'Unknown email address. Check again or try your username.' )
);
} else {
$error = new WP_Error(
'invalid_username',
__( 'Unknown username. Check again or try your email address.' )
);
}
} elseif ( ! wp_is_application_passwords_available_for_user( $user ) ) {
$error = new WP_Error(
'application_passwords_disabled',
__( 'Application passwords are disabled for the requested user.' )
);
}
if ( $error ) {
/**
* Fires when an application password failed to authenticate the user.
*
* @since 5.6.0
*
* @param WP_Error $error The authentication error.
*/
do_action( 'application_password_failed_authentication', $error );
return $error;
}
/*
* Strip out anything non-alphanumeric. This is so passwords can be used with
* or without spaces to indicate the groupings for readability.
*
* Generated application passwords are exclusively alphanumeric.
*/
$password = preg_replace( '/[^a-z\d]/i', '', $password );
$hashed_passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
foreach ( $hashed_passwords as $key => $item ) {
if ( ! wp_check_password( $password, $item['password'], $user->ID ) ) {
continue;
}
$error = new WP_Error();
/**
* Fires when an application password has been successfully checked as valid.
*
* This allows for plugins to add additional constraints to prevent an application password from being used.
*
* @since 5.6.0
*
* @param WP_Error $error The error object.
* @param WP_User $user The user authenticating.
* @param array $item The details about the application password.
* @param string $password The raw supplied password.
*/
do_action( 'wp_authenticate_application_password_errors', $error, $user, $item, $password );
if ( is_wp_error( $error ) && $error->has_errors() ) {
/** This action is documented in wp-includes/user.php */
do_action( 'application_password_failed_authentication', $error );
return $error;
}
WP_Application_Passwords::record_application_password_usage( $user->ID, $item['uuid'] );
/**
* Fires after an application password was used for authentication.
*
* @since 5.6.0
*
* @param WP_User $user The user who was authenticated.
* @param array $item The application password used.
*/
do_action( 'application_password_did_authenticate', $user, $item );
return $user;
}
$error = new WP_Error(
'incorrect_password',
__( 'The provided password is an invalid application password.' )
);
/** This action is documented in wp-includes/user.php */
do_action( 'application_password_failed_authentication', $error );
return $error;
}
/**
* Validates the application password credentials passed via Basic Authentication.
*
* @since 5.6.0
*
* @param int|bool $input_user User ID if one has been determined, false otherwise.
* @return int|bool The authenticated user ID if successful, false otherwise.
*/
function wp_validate_application_password( $input_user ) {
// Don't authenticate twice.
if ( ! empty( $input_user ) ) {
return $input_user;
}
if ( ! wp_is_application_passwords_available() ) {
return $input_user;
}
// Check that we're trying to authenticate
if ( ! isset( $_SERVER['PHP_AUTH_USER'] ) ) {
return $input_user;
}
$authenticated = wp_authenticate_application_password( null, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] );
if ( $authenticated instanceof WP_User ) {
return $authenticated->ID;
}
// If it wasn't a user what got returned, just pass on what we had received originally.
return $input_user;
}
/**
* For Multisite blogs, check if the authenticated user has been marked as a
* spammer, or if the user's primary blog has been marked as spam.
@@ -3923,3 +4093,59 @@ function wp_get_user_request( $request_id ) {
return new WP_User_Request( $post );
}
/**
* Checks if Application Passwords is globally available.
*
* By default, Application Passwords is available to all sites using SSL, but this function is
* filterable to adjust its availability.
*
* @since 5.6.0
*
* @return bool
*/
function wp_is_application_passwords_available() {
/**
* Filters whether Application Passwords is available.
*
* @since 5.6.0
*
* @param bool $available True if available, false otherwise.
*/
return apply_filters( 'wp_is_application_passwords_available', is_ssl() );
}
/**
* Checks if Application Passwords is enabled for a specific user.
*
* By default all users can use Application Passwords, but this function is filterable to restrict
* availability to certain users.
*
* @since 5.6.0
*
* @param int|WP_User $user The user to check.
* @return bool
*/
function wp_is_application_passwords_available_for_user( $user ) {
if ( ! wp_is_application_passwords_available() ) {
return false;
}
if ( ! is_object( $user ) ) {
$user = get_userdata( $user );
}
if ( ! $user || ! $user->exists() ) {
return false;
}
/**
* Filters whether Application Passwords is available for a specific user.
*
* @since 5.6.0
*
* @param bool $available True if available, false otherwise.
* @param WP_User $user The user to check.
*/
return apply_filters( 'wp_is_application_passwords_available_for_user', true, $user );
}