diff --git a/inc/authentication/namespace.php b/inc/authentication/namespace.php index 0078792..46ccd74 100644 --- a/inc/authentication/namespace.php +++ b/inc/authentication/namespace.php @@ -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; } } @@ -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(); diff --git a/tests/test-authentication.php b/tests/test-authentication.php index 8d185e3..cde4135 100644 --- a/tests/test-authentication.php +++ b/tests/test-authentication.php @@ -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; @@ -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 // -------------------------------------------------------------------------