Browse Source

Fixed BasicHttpAuth to respect HTTP_AUTHORIZATION header

Closes #13564
tags/2.0.13
SilverFire - Dmitry Naumenko 7 years ago
parent
commit
0b2e79eac5
No known key found for this signature in database
GPG Key ID: 39DD917A92B270A
  1. 5
      framework/CHANGELOG.md
  2. 38
      framework/filters/auth/HttpBasicAuth.php
  3. 84
      tests/framework/filters/auth/AuthTest.php
  4. 113
      tests/framework/filters/auth/BasicAuthTest.php

5
framework/CHANGELOG.md

@ -51,6 +51,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)
- 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)
@ -1387,7 +1388,7 @@ Yii Framework 2 Change Log
- Bug #4105: `yii\helpers\Html::dropDownlist()` options encodeSpaces was not applied to subgroups (MDMunir)
- Bug #4123: Trace level in logger had no effect in Targets, traces where not logged (cebe)
- Bug #4127: `yii\captcha\CaptchaValidator` clientside error message wasn't formed properly (samdark)
- Bug #4162: Fixed bug where schema name was not used in ’SHOW CREATE TABLE’ query in `yii\db\mysql\Schema` (stevekr)
- Bug #4162: Fixed bug where schema name was not used in ???SHOW CREATE TABLE??? query in `yii\db\mysql\Schema` (stevekr)
- Bug #4241: `yii\widgets\Pjax` was incorrectly setting container id (mitalcoi)
- Bug #4254: `SqlDataProvider` does not work with Oracle and SQL Server (qiangxue, miramir)
- Bug #4276: Added check for UPLOAD_ERR_NO_FILE in `yii\web\UploadedFile` and return null if no file was uploaded (OmgDef)
@ -1467,7 +1468,7 @@ Yii Framework 2 Change Log
- Enh #3410: `yii.activeForm.js` now supports adding/removing fields dynamically (qiangxue)
- Enh #3459: Added logging of errors, which may occur at `yii\caching\FileCache::gc()` (klimov-paul)
- Enh #3472: Added configurable option to encode spaces in dropDownLists and listBoxes (kartik-v)
- Enh #3518: `yii\helpers\Html::encode()` now replaces invalid code sequences with "<EFBFBD>" (DaSourcerer)
- Enh #3518: `yii\helpers\Html::encode()` now replaces invalid code sequences with "???" (DaSourcerer)
- Enh #3520: Added `unlinkAll()` method to active record to remove all records of a model relation (NmDimas, samdark, cebe)
- Enh #3521: Added `yii\filters\HttpCache::sessionCacheLimiter` (qiangxue)
- Enh #3542: Removed requirement to specify `extensions` in application config (samdark)

38
framework/filters/auth/HttpBasicAuth.php

@ -7,6 +7,8 @@
namespace yii\filters\auth;
use yii\web\Request;
/**
* HttpBasicAuth is an action filter that supports the HTTP Basic authentication method.
*
@ -84,8 +86,7 @@ class HttpBasicAuth extends AuthMethod
*/
public function authenticate($user, $request, $response)
{
$username = $request->getAuthUser();
$password = $request->getAuthPassword();
list($username, $password) = $this->getCredentialsFromRequest($request);
if ($this->auth) {
if ($username !== null || $password !== null) {
@ -111,6 +112,39 @@ class HttpBasicAuth extends AuthMethod
}
/**
* 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
*/
public function challenge($response)

84
tests/framework/filters/auth/AuthTest.php

@ -10,7 +10,6 @@ namespace yiiunit\framework\filters\auth;
use Yii;
use yii\base\Action;
use yii\filters\auth\AuthMethod;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;
use yii\helpers\ArrayHelper;
@ -57,66 +56,44 @@ class AuthTest extends \yiiunit\TestCase
];
}
public function authOnly($token, $login, $filter, $action)
public function authOnly($token, $login, $filter)
{
/** @var TestAuthController $controller */
$controller = Yii::$app->createController('test-auth')[0];
$controller->authenticatorConfig = ArrayHelper::merge($filter, ['only' => [$action]]);
$controller->authenticatorConfig = ArrayHelper::merge($filter, ['only' => ['filtered']]);
try {
$this->assertEquals($login, $controller->run($action));
$this->assertEquals($login, $controller->run('filtered'));
} catch (UnauthorizedHttpException $e) {
}
}
public function authOptional($token, $login, $filter, $action)
public function authOptional($token, $login, $filter)
{
/** @var TestAuthController $controller */
$controller = Yii::$app->createController('test-auth')[0];
$controller->authenticatorConfig = ArrayHelper::merge($filter, ['optional' => [$action]]);
$controller->authenticatorConfig = ArrayHelper::merge($filter, ['optional' => ['filtered']]);
try {
$this->assertEquals($login, $controller->run($action));
$this->assertEquals($login, $controller->run('filtered'));
} catch (UnauthorizedHttpException $e) {
}
}
public function authExcept($token, $login, $filter, $action)
public function authExcept($token, $login, $filter)
{
/** @var TestAuthController $controller */
$controller = Yii::$app->createController('test-auth')[0];
$controller->authenticatorConfig = ArrayHelper::merge($filter, ['except' => ['other']]);
try {
$this->assertEquals($login, $controller->run($action));
$this->assertEquals($login, $controller->run('filtered'));
} catch (UnauthorizedHttpException $e) {
}
}
/**
* @dataProvider tokenProvider
* @param string|null $token
* @param string|null $login
*/
public function testQueryParamAuth($token, $login)
{
$_GET['access-token'] = $token;
$filter = ['class' => QueryParamAuth::className()];
$this->authOnly($token, $login, $filter, 'query-param-auth');
$this->authOptional($token, $login, $filter, 'query-param-auth');
$this->authExcept($token, $login, $filter, 'query-param-auth');
}
/**
* @dataProvider tokenProvider
* @param string|null $token
* @param string|null $login
*/
public function testHttpBasicAuth($token, $login)
public function ensureFilterApplies($token, $login, $filter)
{
$_SERVER['PHP_AUTH_USER'] = $token;
$_SERVER['PHP_AUTH_PW'] = 'whatever, we are testers';
$filter = ['class' => HttpBasicAuth::className()];
$this->authOnly($token, $login, $filter, 'basic-auth');
$this->authOptional($token, $login, $filter, 'basic-auth');
$this->authExcept($token, $login, $filter, 'basic-auth');
$this->authOnly($token, $login, $filter);
$this->authOptional($token, $login, $filter);
$this->authExcept($token, $login, $filter);
}
/**
@ -124,23 +101,11 @@ class AuthTest extends \yiiunit\TestCase
* @param string|null $token
* @param string|null $login
*/
public function testHttpBasicAuthCustom($token, $login)
public function testQueryParamAuth($token, $login)
{
$_SERVER['PHP_AUTH_USER'] = $login;
$_SERVER['PHP_AUTH_PW'] = 'whatever, we are testers';
$filter = [
'class' => HttpBasicAuth::className(),
'auth' => function ($username, $password) {
if (preg_match('/\d$/', $username)) {
return UserIdentity::findIdentity($username);
}
return null;
},
];
$this->authOnly($token, $login, $filter, 'basic-auth');
$this->authOptional($token, $login, $filter, 'basic-auth');
$this->authExcept($token, $login, $filter, 'basic-auth');
$_GET['access-token'] = $token;
$filter = ['class' => QueryParamAuth::className()];
$this->ensureFilterApplies($token, $login, $filter);
}
/**
@ -152,16 +117,13 @@ class AuthTest extends \yiiunit\TestCase
{
Yii::$app->request->headers->set('Authorization', "Bearer $token");
$filter = ['class' => HttpBearerAuth::className()];
$this->authOnly($token, $login, $filter, 'bearer-auth');
$this->authOptional($token, $login, $filter, 'bearer-auth');
$this->authExcept($token, $login, $filter, 'bearer-auth');
$this->ensureFilterApplies($token, $login, $filter);
}
public function authMethodProvider()
{
return [
['yii\filters\auth\CompositeAuth'],
['yii\filters\auth\HttpBasicAuth'],
['yii\filters\auth\HttpBearerAuth'],
['yii\filters\auth\QueryParamAuth'],
];
@ -232,17 +194,7 @@ class TestAuthController extends Controller
return ['authenticator' => $this->authenticatorConfig];
}
public function actionBasicAuth()
{
return Yii::$app->user->id;
}
public function actionBearerAuth()
{
return Yii::$app->user->id;
}
public function actionQueryParamAuth()
public function actionFiltered()
{
return Yii::$app->user->id;
}

113
tests/framework/filters/auth/BasicAuthTest.php

@ -0,0 +1,113 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\filters\auth;
use Yii;
use yii\filters\auth\HttpBasicAuth;
use yiiunit\framework\filters\stubs\UserIdentity;
/**
* @group filters
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.7
*/
class BasicAuthTest extends AuthTest
{
/**
* @dataProvider tokenProvider
* @param string|null $token
* @param string|null $login
*/
public function testHttpBasicAuth($token, $login)
{
$_SERVER['PHP_AUTH_USER'] = $token;
$_SERVER['PHP_AUTH_PW'] = 'whatever, we are testers';
$filter = ['class' => HttpBasicAuth::className()];
$this->ensureFilterApplies($token, $login, $filter);
}
/**
* @dataProvider tokenProvider
* @param string|null $token
* @param string|null $login
*/
public function testHttpBasicAuthWithHttpAuthorizationHeader($token, $login)
{
Yii::$app->request->headers->set('HTTP_AUTHORIZATION', 'Basic ' . base64_encode($token . ':' . 'mypw'));
$filter = ['class' => HttpBasicAuth::className()];
$this->ensureFilterApplies($token, $login, $filter);
}
/**
* @dataProvider tokenProvider
* @param string|null $token
* @param string|null $login
*/
public function testHttpBasicAuthWithRedirectHttpAuthorizationHeader($token, $login)
{
Yii::$app->request->headers->set('REDIRECT_HTTP_AUTHORIZATION', 'Basic ' . base64_encode($token . ':' . 'mypw'));
$filter = ['class' => HttpBasicAuth::className()];
$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
* @param string|null $login
*/
public function testHttpBasicAuthCustom($token, $login)
{
$_SERVER['PHP_AUTH_USER'] = $login;
$_SERVER['PHP_AUTH_PW'] = 'whatever, we are testers';
$filter = [
'class' => HttpBasicAuth::className(),
'auth' => function ($username, $password) {
if (preg_match('/\d$/', $username)) {
return UserIdentity::findIdentity($username);
}
return null;
},
];
$this->ensureFilterApplies($token, $login, $filter);
}
public function authMethodProvider()
{
return [
['yii\filters\auth\HttpBasicAuth'],
];
}
}
Loading…
Cancel
Save