WordPress.org

Make WordPress Core


Ignore:
Timestamp:
10/08/2020 10:12:02 PM (12 months 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/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}
Note: See TracChangeset for help on using the changeset viewer.