diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index eae4101308..9a9bc9ca81 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -52,7 +52,7 @@ Yii Framework 2 Change Log - Bug #14773: Fixed `yii\widgets\ActiveField::$options` does not support 'class' option in array format (klimov-paul) - Bug #14921: Fixed bug with replacing numeric keys in `yii\helpers\Url::current()` (rob006) - Bug #13258: Fixed `yii\mutex\FileMutex::$autoRelease` having no effect due to missing base class initialization (kidol) -- Bug #13564: Fixed `yii\filters\HttpBasicAuth` to respect `HTTP_AUTHORIZATION` request header (silverfire) +- Bug #13564: Fixed `yii\web\Request::getAuthUser()`, `getAuthPassword()` to respect `HTTP_AUTHORIZATION` request header (silverfire) - Enh #4495: Added closure support in `yii\i18n\Formatter` (developeruz) - Enh #5786: Allowed to use custom constructors in ActiveRecord-based classes (ElisDN, klimov-paul) - Enh #6644: Added `yii\helpers\ArrayHelper::setValue()` (LAV45) diff --git a/framework/filters/auth/HttpBasicAuth.php b/framework/filters/auth/HttpBasicAuth.php index c6e06d3492..6949d96099 100644 --- a/framework/filters/auth/HttpBasicAuth.php +++ b/framework/filters/auth/HttpBasicAuth.php @@ -49,6 +49,13 @@ use yii\web\Request; * } * ``` * + * > Tip: In case authentication does not work like expected, make sure your web server passes + * username and password to `$_SERVER['PHP_AUTH_USER']` and `$_SERVER['PHP_AUTH_PW']` variables. + * If you are using Apache with PHP-CGI, you might need to add this line to your `.htaccess` file: + * ``` + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] + * ``` + * * @author Qiang Xue * @since 2.0 */ @@ -86,7 +93,7 @@ class HttpBasicAuth extends AuthMethod */ public function authenticate($user, $request, $response) { - list($username, $password) = $this->getCredentialsFromRequest($request); + list($username, $password) = $request->getAuthCredentials(); if ($this->auth) { if ($username !== null || $password !== null) { @@ -111,39 +118,6 @@ class HttpBasicAuth extends AuthMethod return null; } - /** - * Extract username and password from $request. - * - * @param Request $request - * @since 2.0.13 - * @return array - */ - protected function getCredentialsFromRequest($request) - { - $username = $request->getAuthUser(); - $password = $request->getAuthPassword(); - - if ($username !== null || $password !== null) { - return [$username, $password]; - } - - $headers = $request->getHeaders(); - $auth_token = $headers->get('HTTP_AUTHORIZATION') ?: $headers->get('REDIRECT_HTTP_AUTHORIZATION'); - if ($auth_token != null && strpos(strtolower($auth_token), 'basic') === 0) { - $parts = array_map(function ($value) { - return strlen($value) === 0 ? null : $value; - }, explode(':', base64_decode(mb_substr($auth_token, 6)), 2)); - - if (count($parts) < 2) { - return [$parts[0], null]; - } - - return $parts; - } - - return [null, null]; - } - /** * @inheritdoc */ diff --git a/framework/web/Request.php b/framework/web/Request.php index f0df15b403..b300fae982 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -1134,19 +1134,59 @@ class Request extends \yii\base\Request } /** - * @return string|null the username sent via HTTP authentication, null if the username is not given + * @return string|null the username sent via HTTP authentication, `null` if the username is not given + * @see getAuthCredentials() to get both username and password in one call */ public function getAuthUser() { - return isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; + return $this->getAuthCredentials()[0]; } /** - * @return string|null the password sent via HTTP authentication, null if the password is not given + * @return string|null the password sent via HTTP authentication, `null` if the password is not given + * @see getAuthCredentials() to get both username and password in one call */ public function getAuthPassword() { - return isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; + return $this->getAuthCredentials()[1]; + } + + /** + * @return array that contains exactly two elements: + * - 0: the username sent via HTTP authentication, `null` if the username is not given + * - 1: the password sent via HTTP authentication, `null` if the password is not given + * @see getAuthUser() to get only username + * @see getAuthPassword() to get only password + * @since 2.0.13 + */ + public function getAuthCredentials() + { + $username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; + $password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; + if ($username !== null || $password !== null) { + return [$username, $password]; + } + + /* + * Apache with php-cgi does not pass HTTP Basic authentication to PHP by default. + * To make it work, add the following line to to your .htaccess file: + * + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + */ + $auth_token = $this->getHeaders()->get('HTTP_AUTHORIZATION') ?: $this->getHeaders()->get('REDIRECT_HTTP_AUTHORIZATION'); + if ($auth_token != null && strpos(strtolower($auth_token), 'basic') === 0) { + $parts = array_map(function ($value) { + return strlen($value) === 0 ? null : $value; + }, explode(':', base64_decode(mb_substr($auth_token, 6)), 2)); + + if (count($parts) < 2) { + return [$parts[0], null]; + } + + return $parts; + } + + return [null, null]; } private $_port; diff --git a/tests/framework/filters/auth/BasicAuthTest.php b/tests/framework/filters/auth/BasicAuthTest.php index 81d2c6cd95..f380969faf 100644 --- a/tests/framework/filters/auth/BasicAuthTest.php +++ b/tests/framework/filters/auth/BasicAuthTest.php @@ -55,33 +55,6 @@ class BasicAuthTest extends AuthTest $this->ensureFilterApplies($token, $login, $filter); } - public function authHeadersProvider() - { - return [ - ['not a base64 at all', [base64_decode('not a base64 at all'), null]], - [base64_encode('user:'), ['user', null]], - [base64_encode('user'), ['user', null]], - [base64_encode('user:pw'), ['user', 'pw']], - [base64_encode('user:pw'), ['user', 'pw']], - [base64_encode('user:a:b'), ['user', 'a:b']], - [base64_encode(':a:b'), [null, 'a:b']], - [base64_encode(':'), [null, null]], - ]; - } - - /** - * @dataProvider authHeadersProvider - * @param string $header - * @param array $expected - */ - public function testHttpBasicAuthWithBrokenHttpAuthorizationHeader($header, $expected) - { - Yii::$app->request->getHeaders()->set('HTTP_AUTHORIZATION', 'Basic ' . $header); - $filter = new HttpBasicAuth(); - $result = $this->invokeMethod($filter, 'getCredentialsFromRequest', [Yii::$app->request]); - $this->assertSame($expected, $result); - } - /** * @dataProvider tokenProvider * @param string|null $token diff --git a/tests/framework/web/RequestTest.php b/tests/framework/web/RequestTest.php index fdba5d027f..201e1aab4d 100644 --- a/tests/framework/web/RequestTest.php +++ b/tests/framework/web/RequestTest.php @@ -541,4 +541,56 @@ class RequestTest extends TestCase $request = new Request(); $this->assertEquals(null, $request->getOrigin()); } + + public function httpAuthorizationHeadersProvider() + { + return [ + ['not a base64 at all', [base64_decode('not a base64 at all'), null]], + [base64_encode('user:'), ['user', null]], + [base64_encode('user'), ['user', null]], + [base64_encode('user:pw'), ['user', 'pw']], + [base64_encode('user:pw'), ['user', 'pw']], + [base64_encode('user:a:b'), ['user', 'a:b']], + [base64_encode(':a:b'), [null, 'a:b']], + [base64_encode(':'), [null, null]], + ]; + } + + /** + * @dataProvider httpAuthorizationHeadersProvider + * @param string $secret + * @param array $expected + */ + public function testHttpAuthCredentialsFromHttpAuthorizationHeader($secret, $expected) + { + $request = new Request(); + + $request->getHeaders()->set('HTTP_AUTHORIZATION', 'Basic ' . $secret); + $this->assertSame($request->getAuthCredentials(), $expected); + $this->assertSame($request->getAuthUser(), $expected[0]); + $this->assertSame($request->getAuthPassword(), $expected[1]); + $request->getHeaders()->offsetUnset('HTTP_AUTHORIZATION'); + + $request->getHeaders()->set('REDIRECT_HTTP_AUTHORIZATION', 'Basic ' . $secret); + $this->assertSame($request->getAuthCredentials(), $expected); + $this->assertSame($request->getAuthUser(), $expected[0]); + $this->assertSame($request->getAuthPassword(), $expected[1]); + } + + public function testHttpAuthCredentialsFromServerSuperglobal() + { + $original = $_SERVER; + list($user, $pw) = ['foo', 'bar']; + $_SERVER['PHP_AUTH_USER'] = $user; + $_SERVER['PHP_AUTH_PW'] = $pw; + + $request = new Request(); + $request->getHeaders()->set('HTTP_AUTHORIZATION', 'Basic ' . base64_encode('less-priority:than-PHP_AUTH_*')); + + $this->assertSame($request->getAuthCredentials(), [$user, $pw]); + $this->assertSame($request->getAuthUser(), $user); + $this->assertSame($request->getAuthPassword(), $pw); + + $_SERVER = $original; + } }