Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions inc/authentication/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,27 @@
use WP\OAuth2\Tokens;

/**
* Get the authorization header
* Get a request header by name, case-insensitively.
*
* On certain systems and configurations, the Authorization header will be
* stripped out by the server or PHP. Typically this is then used to
* generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use
* `getallheaders` here to try and grab it out instead.
*
* @return string|null Authorization header if set, null otherwise
* @param string $name Header name. Default 'authorization'.
*
* @return string|null Header value if set, null otherwise.
*/
function get_authorization_header() {
if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
function get_authorization_header( $name = 'authorization' ) {
$server_key = 'HTTP_' . strtoupper( str_replace( '-', '_', $name ) );
if ( ! empty( $_SERVER[ $server_key ] ) ) {
return wp_unslash( $_SERVER[ $server_key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}

if ( function_exists( 'getallheaders' ) ) {
$headers = getallheaders();

// Check for the authorization header case-insensitively
foreach ( $headers as $key => $value ) {
if ( strtolower( $key ) === 'authorization' ) {
if ( strtolower( $key ) === strtolower( $name ) ) {
return $value;
}
}
Expand All @@ -46,9 +47,21 @@ function get_authorization_header() {
* @return string|null Token on success, null on failure.
*/
function get_provided_token() {
$header = get_authorization_header();
/**
* Filter the authorization header name used to extract the bearer token.
*
* Override when the standard Authorization header is consumed by a proxy
* (e.g. Imperva HTTP Basic Auth) and the token is forwarded under a
* different name such as X-Authorization.
*
* @param string $name Header name. Default 'authorization'.
*/
$header = get_authorization_header( apply_filters( 'oauth2.authentication.authorization_header', 'authorization' ) );
if ( $header ) {
return get_token_from_bearer_header( $header );
$token = get_token_from_bearer_header( $header );
if ( $token ) {
return $token;
}
}

$token = get_token_from_request();
Expand Down
142 changes: 142 additions & 0 deletions tests/test-authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use WP_User;

use function WP\OAuth2\Authentication\attempt_authentication;
use function WP\OAuth2\Authentication\get_authorization_header;
use function WP\OAuth2\Authentication\get_token_from_bearer_header;
use function WP\OAuth2\Authentication\maybe_report_errors;

Expand Down Expand Up @@ -118,6 +119,147 @@ public function test_attempt_authentication_no_op_when_no_token() {
$this->assertNull( $result );
}

// -------------------------------------------------------------------------
// get_authorization_header
// -------------------------------------------------------------------------

public function test_get_authorization_header_reads_default_authorization_header() {
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer testtoken';

$result = get_authorization_header();

$this->assertEquals( 'Bearer testtoken', $result );
}

public function test_get_authorization_header_reads_custom_header_name() {
$_SERVER['HTTP_X_CUSTOM_AUTH'] = 'Bearer customtoken';

$result = get_authorization_header( 'x-custom-auth' );

unset( $_SERVER['HTTP_X_CUSTOM_AUTH'] );
$this->assertEquals( 'Bearer customtoken', $result );
}

public function test_get_authorization_header_returns_null_when_header_absent() {
unset( $_SERVER['HTTP_AUTHORIZATION'] );

$result = get_authorization_header();

$this->assertNull( $result );
}

public function test_get_authorization_header_returns_null_for_absent_custom_header() {
unset( $_SERVER['HTTP_X_MISSING'] );

$result = get_authorization_header( 'x-missing' );

$this->assertNull( $result );
}

public function test_get_authorization_header_converts_hyphen_to_underscore_in_server_key() {
$_SERVER['HTTP_X_MY_TOKEN'] = 'Bearer hyphentest';

$result = get_authorization_header( 'x-my-token' );

unset( $_SERVER['HTTP_X_MY_TOKEN'] );
$this->assertEquals( 'Bearer hyphentest', $result );
}

// -------------------------------------------------------------------------
// oauth2.authentication.authorization_header filter
// -------------------------------------------------------------------------

public function test_authorization_header_filter_default_reads_authorization_header() {
$token = Access_Token::create( $this->client, $this->user );

$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $token->get_key();
$result = attempt_authentication();

$this->assertEquals( $this->user->ID, $result );
}

public function test_custom_authorization_header_filter_authenticates_token() {
$token = Access_Token::create( $this->client, $this->user );

add_filter( 'oauth2.authentication.authorization_header', static function () {
return 'x-my-auth';
} );
$_SERVER['HTTP_X_MY_AUTH'] = 'Bearer ' . $token->get_key();

$result = attempt_authentication();

remove_all_filters( 'oauth2.authentication.authorization_header' );
unset( $_SERVER['HTTP_X_MY_AUTH'] );

$this->assertEquals( $this->user->ID, $result );
}

public function test_custom_header_filter_with_hyphenated_name() {
$token = Access_Token::create( $this->client, $this->user );

add_filter( 'oauth2.authentication.authorization_header', static function () {
return 'x-forwarded-auth';
} );
$_SERVER['HTTP_X_FORWARDED_AUTH'] = 'Bearer ' . $token->get_key();

$result = attempt_authentication();

remove_all_filters( 'oauth2.authentication.authorization_header' );
unset( $_SERVER['HTTP_X_FORWARDED_AUTH'] );

$this->assertEquals( $this->user->ID, $result );
}

public function test_custom_header_absent_does_not_authenticate() {
add_filter( 'oauth2.authentication.authorization_header', static function () {
return 'x-my-auth';
} );
unset( $_SERVER['HTTP_X_MY_AUTH'] );

$result = attempt_authentication();

remove_all_filters( 'oauth2.authentication.authorization_header' );

$this->assertNull( $result );
}

public function test_custom_header_with_invalid_token_sets_error() {
global $oauth2_error;

add_filter( 'oauth2.authentication.authorization_header', static function () {
return 'x-my-auth';
} );
$_SERVER['HTTP_X_MY_AUTH'] = 'Bearer invalidtoken123';

attempt_authentication();

remove_all_filters( 'oauth2.authentication.authorization_header' );
unset( $_SERVER['HTTP_X_MY_AUTH'] );

$this->assertWPError( $oauth2_error );
$this->assertEquals(
'oauth2.authentication.attempt_authentication.invalid_token',
$oauth2_error->get_error_code()
);
}

public function test_filter_does_not_affect_other_requests_after_removal() {
$token = Access_Token::create( $this->client, $this->user );

// Add and immediately remove the filter.
$cb = static function () {
return 'x-my-auth';
};
add_filter( 'oauth2.authentication.authorization_header', $cb );
remove_filter( 'oauth2.authentication.authorization_header', $cb );

// Standard Authorization header should still work.
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $token->get_key();
$result = attempt_authentication();

$this->assertEquals( $this->user->ID, $result );
}

// -------------------------------------------------------------------------
// maybe_report_errors
// -------------------------------------------------------------------------
Expand Down
Loading