From e3502d0cd4bf633b3c6be4860206e911ca6addec Mon Sep 17 00:00:00 2001 From: Abhishek Kaushik Date: Wed, 22 Apr 2026 15:08:51 +0200 Subject: [PATCH 1/4] Add custom Auth header --- inc/authentication/namespace.php | 58 +++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/inc/authentication/namespace.php b/inc/authentication/namespace.php index aa6aa8e..63c0617 100644 --- a/inc/authentication/namespace.php +++ b/inc/authentication/namespace.php @@ -46,17 +46,57 @@ function get_authorization_header() { * @return string|null Token on success, null on failure. */ function get_provided_token() { - $header = get_authorization_header(); - if ( $header ) { - return get_token_from_bearer_header( $header ); - } + // Prefer the standard Authorization header. Only if it is missing or + // does not contain a bearer token (e.g. a proxy has injected Basic + // auth), fall back to the non-standard X-Authorization header. + $header = get_authorization_header(); + if ( $header ) { + $token = get_token_from_bearer_header( $header ); + if ( $token ) { + return $token; + } + } + + $alt_header = get_custom_authorization_header(); + if ( $alt_header ) { + $token = get_token_from_bearer_header( $alt_header ); + if ( $token ) { + return $token; + } + } + + $token = get_token_from_request(); + if ( $token ) { + return $token; + } + + return null; +} + +/** + * Get the X-Authorization header. + * + * Used when the standard Authorization header is consumed by a proxy + * layer (e.g. Imperva HTTP Basic Auth). + * + * @return string|null Header value if set, null otherwise. + */ +function get_custom_authorization_header() { + if ( ! empty( $_SERVER['HTTP_X_AUTHORIZATION'] ) ) { + return wp_unslash( $_SERVER['HTTP_X_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } - $token = get_token_from_request(); - if ( $token ) { - return $token; - } + if ( function_exists( 'getallheaders' ) ) { + $headers = getallheaders(); - return null; + foreach ( $headers as $key => $value ) { + if ( strtolower( $key ) === 'x-authorization' ) { + return $value; + } + } + } + + return null; } /** From 08deba69f164f47963cd4d868605d06081bac90a Mon Sep 17 00:00:00 2001 From: Abhishek Kaushik Date: Tue, 12 May 2026 15:03:26 +0530 Subject: [PATCH 2/4] Use filter for alternative Authorization header --- inc/authentication/namespace.php | 41 +++++++++----------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/inc/authentication/namespace.php b/inc/authentication/namespace.php index 63c0617..02e7d63 100644 --- a/inc/authentication/namespace.php +++ b/inc/authentication/namespace.php @@ -46,9 +46,6 @@ function get_authorization_header() { * @return string|null Token on success, null on failure. */ function get_provided_token() { - // Prefer the standard Authorization header. Only if it is missing or - // does not contain a bearer token (e.g. a proxy has injected Basic - // auth), fall back to the non-standard X-Authorization header. $header = get_authorization_header(); if ( $header ) { $token = get_token_from_bearer_header( $header ); @@ -57,7 +54,17 @@ function get_provided_token() { } } - $alt_header = get_custom_authorization_header(); + /** + * Provide an alternative authorization header value. + * + * Use this filter when the standard Authorization header is consumed by a + * proxy or server layer (e.g. Imperva HTTP Basic Auth). Return the raw + * header value (e.g. "Bearer ") to have it parsed as a bearer token. + * Return null to skip the fallback entirely. + * + * @param string|null $header Raw header value, or null to skip. + */ + $alt_header = apply_filters( 'oauth2.authentication.alternative_authorization_header', null ); if ( $alt_header ) { $token = get_token_from_bearer_header( $alt_header ); if ( $token ) { @@ -73,32 +80,6 @@ function get_provided_token() { return null; } -/** - * Get the X-Authorization header. - * - * Used when the standard Authorization header is consumed by a proxy - * layer (e.g. Imperva HTTP Basic Auth). - * - * @return string|null Header value if set, null otherwise. - */ -function get_custom_authorization_header() { - if ( ! empty( $_SERVER['HTTP_X_AUTHORIZATION'] ) ) { - return wp_unslash( $_SERVER['HTTP_X_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - } - - if ( function_exists( 'getallheaders' ) ) { - $headers = getallheaders(); - - foreach ( $headers as $key => $value ) { - if ( strtolower( $key ) === 'x-authorization' ) { - return $value; - } - } - } - - return null; -} - /** * Extracts the token from the given authorization header. * From 04215519654c42f3cfe87f934991ffdd032aa2e4 Mon Sep 17 00:00:00 2001 From: Abhishek Kaushik Date: Tue, 2 Jun 2026 16:14:35 +0530 Subject: [PATCH 3/4] Allow custom authorization header name --- inc/authentication/namespace.php | 42 +++++++++++++------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/inc/authentication/namespace.php b/inc/authentication/namespace.php index 02e7d63..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,27 +47,18 @@ function get_authorization_header() { * @return string|null Token on success, null on failure. */ function get_provided_token() { - $header = get_authorization_header(); - if ( $header ) { - $token = get_token_from_bearer_header( $header ); - if ( $token ) { - return $token; - } - } - /** - * Provide an alternative authorization header value. + * Filter the authorization header name used to extract the bearer token. * - * Use this filter when the standard Authorization header is consumed by a - * proxy or server layer (e.g. Imperva HTTP Basic Auth). Return the raw - * header value (e.g. "Bearer ") to have it parsed as a bearer token. - * Return null to skip the fallback entirely. + * 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|null $header Raw header value, or null to skip. + * @param string $name Header name. Default 'authorization'. */ - $alt_header = apply_filters( 'oauth2.authentication.alternative_authorization_header', null ); - if ( $alt_header ) { - $token = get_token_from_bearer_header( $alt_header ); + $header = get_authorization_header( apply_filters( 'oauth2.authentication.authorization_header', 'authorization' ) ); + if ( $header ) { + $token = get_token_from_bearer_header( $header ); if ( $token ) { return $token; } From fe7d5ff8d71d70313e7b949ee4cce5094ea65599 Mon Sep 17 00:00:00 2001 From: Abhishek Kaushik Date: Tue, 2 Jun 2026 16:36:13 +0530 Subject: [PATCH 4/4] Add tests for authorization header handling --- tests/test-authentication.php | 142 ++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) 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 // -------------------------------------------------------------------------