Commit 83da0cd0 by Qiang Xue

refactored authentication filters.

parent 12cd71d7
......@@ -599,30 +599,48 @@ There are different ways to send an access token:
server and sent to the API server via [HTTP Bearer Tokens](http://tools.ietf.org/html/rfc6750),
according to the OAuth2 protocol.
Yii supports all of the above authentication methods and can be further extended to support other methods.
Yii supports all of the above authentication methods. You can also easily create new authentication methods.
To enable authentication for your APIs, do the following two steps:
1. Configure [[yii\rest\Controller::authMethods]] with the authentication methods you plan to use.
1. Specify which authentication methods you plan to use by configuring the `auth` behavior
in your REST controller classes.
2. Implement [[yii\web\IdentityInterface::findIdentityByAccessToken()]] in your [[yii\web\User::identityClass|user identity class]].
For example, to enable all three authentication methods explained above, you would configure `authMethods`
as follows,
For example, to enable all three authentication methods explained above, you can configure `auth` like following,
```php
class UserController extends ActiveController
public function behaviors()
{
public $authMethods = [
'yii\rest\HttpBasicAuth',
'yii\rest\QueryParamAuth',
'yii\rest\HttpBearerAuth',
];
return array_merge(parent::behaviors(), [
'auth' => [
'authMethods' => [
\yii\filters\auth\HttpBasicAuth::className(),
\yii\filters\auth\QueryParamAuth::className(),
\yii\filters\auth\HttpBearerAuth::className(),
],
],
]);
}
```
Each element in `authMethods` should be an auth class name or a configuration array. An auth class
Each element in `authMethods` should be an auth method class name or a configuration array. An auth class
must implement [[yii\rest\AuthInterface]].
If you only want to a single authentication method, such as HTTP Basic Auth, you may use the following code:
```php
public function behaviors()
{
return array_merge(parent::behaviors(), [
'auth' => [
'class' => \yii\filters\auth\HttpBasicAuth::className(),
],
]);
}
```
Implementation of `findIdentityByAccessToken()` is application specific. For example, in simple scenarios
when each user can only have one access token, you may store the access token in an `access_token` column
in the user table. The method can then be readily implemented in the `User` class as follows,
......@@ -713,7 +731,6 @@ public function behaviors()
{
return array_merge(parent::behaviors(), [
'rateLimiter' => [
'class' => \yii\filters\RateLimiter::className(),
'enableRateLimitHeaders' => false,
],
]);
......
......@@ -290,6 +290,7 @@ Yii Framework 2 Change Log
- New: Added `yii\codeception\DbTestCase` (qiangxue)
- New: Added `yii\web\GroupUrlRule` (qiangxue)
- New: Added `yii\filters\RateLimiter` (qiangxue)
- New: Added various authentication methods, including `HttpBasicAuth`, `HttpBearerAuth`, `QueryParamAuth`, and `CompositeAuth` (qiangxue)
2.0.0-alpha, December 1, 2013
-----------------------------
......
......@@ -185,15 +185,16 @@ return [
'yii\requirements\YiiRequirementChecker' => YII_PATH . '/requirements/YiiRequirementChecker.php',
'yii\rest\Action' => YII_PATH . '/rest/Action.php',
'yii\rest\ActiveController' => YII_PATH . '/rest/ActiveController.php',
'yii\rest\AuthInterface' => YII_PATH . '/rest/AuthInterface.php',
'yii\rest\Controller' => YII_PATH . '/rest/Controller.php',
'yii\rest\CreateAction' => YII_PATH . '/rest/CreateAction.php',
'yii\rest\DeleteAction' => YII_PATH . '/rest/DeleteAction.php',
'yii\rest\HttpBasicAuth' => YII_PATH . '/rest/HttpBasicAuth.php',
'yii\rest\HttpBearerAuth' => YII_PATH . '/rest/HttpBearerAuth.php',
'yii\rest\IndexAction' => YII_PATH . '/rest/IndexAction.php',
'yii\rest\OptionsAction' => YII_PATH . '/rest/OptionsAction.php',
'yii\rest\QueryParamAuth' => YII_PATH . '/rest/QueryParamAuth.php',
'yii\filters\auth\CompositeAuth' => YII_PATH . '/filters/auth/CompositeAuth.php',
'yii\filters\auth\AuthInterface' => YII_PATH . '/filters/auth/AuthInterface.php',
'yii\filters\auth\HttpBasicAuth' => YII_PATH . '/filters/auth/HttpBasicAuth.php',
'yii\filters\auth\HttpBearerAuth' => YII_PATH . '/filters/auth/HttpBearerAuth.php',
'yii\filters\auth\QueryParamAuth' => YII_PATH . '/filters/auth/QueryParamAuth.php',
'yii\filters\RateLimitInterface' => YII_PATH . '/filters/RateLimitInterface.php',
'yii\filters\RateLimiter' => YII_PATH . '/filters/RateLimiter.php',
'yii\rest\Serializer' => YII_PATH . '/rest/Serializer.php',
......
......@@ -40,18 +40,26 @@ use yii\web\TooManyRequestsHttpException;
class RateLimiter extends ActionFilter
{
/**
* @var boolean whether to include rate limit headers in the response
*/
public $enableRateLimitHeaders = true;
/**
* @var string the message to be displayed when rate limit exceeds
*/
public $errorMessage = 'Rate limit exceeded.';
/**
* @var RateLimitInterface the user object that implements the RateLimitInterface.
* If not set, it will take the value of `Yii::$app->user->getIdentity(false)`.
*/
public $user;
/**
* @var boolean whether to include rate limit headers in the response
* @var Request the current request. If not set, the `request` application component will be used.
*/
public $enableRateLimitHeaders = true;
public $request;
/**
* @var string the message to be displayed when rate limit exceeds
* @var Response the response to be sent. If not set, the `response` application component will be used.
*/
public $errorMessage = 'Rate limit exceeded.';
public $response;
/**
......@@ -62,7 +70,12 @@ class RateLimiter extends ActionFilter
$user = $this->user ? : Yii::$app->getUser()->getIdentity(false);
if ($user instanceof RateLimitInterface) {
Yii::trace('Check rate limit', __METHOD__);
$this->checkRateLimit($user, Yii::$app->getRequest(), Yii::$app->getResponse(), $action);
$this->checkRateLimit(
$user,
$this->request ? : Yii::$app->getRequest(),
$this->response ? : Yii::$app->getResponse(),
$action
);
} elseif ($user) {
Yii::info('Rate limit skipped: "user" does not implement RateLimitInterface.');
} else {
......
......@@ -5,7 +5,7 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
namespace yii\filters\auth;
use yii\web\User;
use yii\web\Request;
......@@ -14,7 +14,7 @@ use yii\web\IdentityInterface;
use yii\web\UnauthorizedHttpException;
/**
* AuthInterface is the interface required by classes that support user authentication.
* AuthInterface is the interface that should be implemented by auth method classes.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\filters\auth;
use Yii;
use yii\base\ActionFilter;
use yii\web\UnauthorizedHttpException;
use yii\web\User;
use yii\web\Request;
use yii\web\Response;
/**
* AuthMethod is a base class implementing the [[AuthInterface]] interface.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
abstract class AuthMethod extends ActionFilter implements AuthInterface
{
/**
* @var User the user object representing the user authentication status. If not set, the `user` application component will be used.
*/
public $user;
/**
* @var Request the current request. If not set, the `request` application component will be used.
*/
public $request;
/**
* @var Response the response to be sent. If not set, the `response` application component will be used.
*/
public $response;
/**
* @inheritdoc
*/
public function beforeAction($action)
{
$identity = $this->authenticate(
$this->user ? : Yii::$app->getUser(),
$this->request ? : Yii::$app->getRequest(),
$this->response ? : Yii::$app->getResponse()
);
if ($identity !== null) {
return true;
} else {
$this->handleFailure($this->response ? : Yii::$app->getResponse());
return false;
}
}
/**
* @inheritdoc
*/
public function handleFailure($response)
{
throw new UnauthorizedHttpException('You are requesting with an invalid credential.');
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\filters\auth;
use Yii;
use yii\base\InvalidConfigException;
/**
* CompositeAuth is an action filter that supports multiple authentication methods at the same time.
*
* The authentication methods contained by CompositeAuth are configured via [[authMethods]],
* which is a list of supported authentication class configurations.
*
* The following example shows how to support three authentication methods:
*
* ```php
* public function behaviors()
* {
* return [
* 'compositeAuth' => [
* 'class' => \yii\filters\auth\CompositeAuth::className(),
* 'authMethods' => [
* \yii\filters\auth\HttpBasicAuth::className(),
* \yii\filters\auth\QueryParamAuth::className(),
* ],
* ],
* ];
* }
* ```
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class CompositeAuth extends AuthMethod
{
/**
* @var array the supported authentication methods. This property should take a list of supported
* authentication methods, each represented by an authentication class or configuration.
* If this is not set or empty, no authentication will be performed.
*
* Note that an auth method class must implement the [[\yii\filters\auth\AuthInterface]] interface.
*/
public $authMethods = [];
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
foreach ($this->authMethods as $i => $auth) {
$this->authMethods[$i] = $auth = Yii::createObject($auth);
if (!$auth instanceof AuthInterface) {
throw new InvalidConfigException(get_class($auth) . ' must implement yii\filters\auth\AuthInterface');
}
$identity = $auth->authenticate($user, $request, $response);
if ($identity !== null) {
return $identity;
}
}
if (!empty($this->authMethods)) {
/** @var AuthInterface $auth */
$auth = reset($this->authMethods);
$auth->handleFailure($response);
}
return null;
}
}
......@@ -5,36 +5,83 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
namespace yii\filters\auth;
use Yii;
use yii\base\Component;
use yii\web\UnauthorizedHttpException;
/**
* HttpBasicAuth implements the HTTP Basic authentication method.
* HttpBasicAuth is an action filter that supports the HTTP Basic authentication method.
*
* You may use HttpBasicAuth by attaching it as a behavior to a controller or module, like the following:
*
* ```php
* public function behaviors()
* {
* return [
* 'basicAuth' => [
* 'class' => \yii\filters\auth\HttpBasicAuth::className(),
* ],
* ];
* }
* ```
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HttpBasicAuth extends Component implements AuthInterface
class HttpBasicAuth extends AuthMethod
{
/**
* @var string the HTTP authentication realm
*/
public $realm = 'api';
/**
* @var callable a PHP callable that will authenticate the user with the HTTP basic auth information.
* The callable receives a username and a password as its parameters. It should return an identity object
* that matches the username and password. Null should be returned if there is no such identity.
*
* The following code is a typical implementation of this callable:
*
* ```php
* function ($username, $password) {
* return \app\models\User::find([
* 'username' => $username,
* 'password' => $password,
* ]);
* }
* ```
*
* If this property is not set, the username information will be considered as an access token
* while the password information will be ignored. The [[\yii\web\User::loginByAccessToken()]]
* method will be called to authenticate and login the user.
*/
public $auth;
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
if (($accessToken = $request->getAuthUser()) !== null) {
$identity = $user->loginByAccessToken($accessToken);
if ($identity !== null) {
$username = $request->getAuthUser();
$password = $request->getAuthPassword();
if ($this->auth) {
if ($username !== null || $password !== null) {
$identity = call_user_func($this->auth, $username, $password);
if ($identity !== null) {
$user->setIdentity($identity);
} else {
$this->handleFailure($response);
}
return $identity;
}
$this->handleFailure($response);
} elseif ($username !== null) {
$identity = $user->loginByAccessToken($username);
if ($identity === null) {
$this->handleFailure($response);
}
return $identity;
}
return null;
......
......@@ -5,19 +5,31 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
namespace yii\filters\auth;
use Yii;
use yii\base\Component;
use yii\web\UnauthorizedHttpException;
/**
* HttpBearerAuth implements the authentication method based on HTTP Bearer token.
* HttpBearerAuth is an action filter that supports the authentication method based on HTTP Bearer token.
*
* You may use HttpBasicAuth by attaching it as a behavior to a controller or module, like the following:
*
* ```php
* public function behaviors()
* {
* return [
* 'bearerAuth' => [
* 'class' => \yii\filters\auth\HttpBearerAuth::className(),
* ],
* ];
* }
* ```
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HttpBearerAuth extends Component implements AuthInterface
class HttpBearerAuth extends AuthMethod
{
/**
* @var string the HTTP authentication realm
......@@ -32,11 +44,10 @@ class HttpBearerAuth extends Component implements AuthInterface
$authHeader = $request->getHeaders()->get('Authorization');
if ($authHeader !== null && preg_match("/^Bearer\\s+(.*?)$/", $authHeader, $matches)) {
$identity = $user->loginByAccessToken($matches[1]);
if ($identity !== null) {
return $identity;
if ($identity === null) {
$this->handleFailure($response);
}
$this->handleFailure($response);
return $identity;
}
return null;
......
......@@ -5,19 +5,18 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
namespace yii\filters\auth;
use Yii;
use yii\base\Component;
use yii\web\UnauthorizedHttpException;
/**
* QueryParamAuth implements the authentication method based on the access token passed through a query parameter.
* QueryParamAuth is an action filter that supports the authentication based on the access token passed through a query parameter.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class QueryParamAuth extends Component implements AuthInterface
class QueryParamAuth extends AuthMethod
{
/**
* @var string the parameter name for passing the access token
......
......@@ -8,10 +8,9 @@
namespace yii\rest;
use Yii;
use yii\base\InvalidConfigException;
use yii\filters\auth\CompositeAuth;
use yii\filters\RateLimiter;
use yii\web\Response;
use yii\web\UnauthorizedHttpException;
use yii\web\UnsupportedMediaTypeHttpException;
use yii\filters\VerbFilter;
use yii\web\ForbiddenHttpException;
......@@ -23,8 +22,9 @@ use yii\web\ForbiddenHttpException;
*
* 1. Resolving response format and API version number (see [[supportedFormats]], [[supportedVersions]] and [[version]]);
* 2. Validating request method (see [[verbs()]]).
* 3. Authenticating user (see [[authenticate()]]);
* 4. Formatting response data (see [[serializeData()]]).
* 3. Authenticating user (see [[\yii\filters\auth\AuthInterface]]);
* 4. Rate limiting (see [[\yii\filters\RateLimiter]]);
* 5. Formatting response data (see [[serializeData()]]).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
......@@ -44,12 +44,6 @@ class Controller extends \yii\web\Controller
*/
public $enableCsrfValidation = false;
/**
* @var array the supported authentication methods. This property should take a list of supported
* authentication methods, each represented by an authentication class or configuration.
* If this is not set or empty, it means authentication is disabled.
*/
public $authMethods;
/**
* @var string the chosen API version number, or null if [[supportedVersions]] is empty.
* @see supportedVersions
*/
......@@ -80,6 +74,9 @@ class Controller extends \yii\web\Controller
'class' => VerbFilter::className(),
'actions' => $this->verbs(),
],
'auth' => [
'class' => CompositeAuth::className(),
],
'rateLimiter' => [
'class' => RateLimiter::className(),
],
......@@ -98,15 +95,6 @@ class Controller extends \yii\web\Controller
/**
* @inheritdoc
*/
public function beforeAction($action)
{
$this->authenticate($action);
return parent::beforeAction($action);
}
/**
* @inheritdoc
*/
public function afterAction($action, $result)
{
$result = parent::afterAction($action, $result);
......@@ -157,36 +145,6 @@ class Controller extends \yii\web\Controller
}
/**
* Authenticates the user.
* This method implements the user authentication based on an access token sent through the `Authorization` HTTP header.
* @param \yii\base\Action $action the action to be executed
* @throws UnauthorizedHttpException if the user is not authenticated successfully
* @throws InvalidConfigException if an auth method declared in [[authMethods]] is invalid.
*/
protected function authenticate($action)
{
if (empty($this->authMethods)) {
return;
}
$user = Yii::$app->getUser();
$request = Yii::$app->getRequest();
$response = Yii::$app->getResponse();
foreach ($this->authMethods as $i => $auth) {
$this->authMethods[$i] = $auth = Yii::createObject($auth);
if (!$auth instanceof AuthInterface) {
throw new InvalidConfigException(get_class($auth) . ' must implement yii\rest\AuthInterface');
} elseif ($auth->authenticate($user, $request, $response) !== null) {
return;
}
}
/** @var AuthInterface $auth */
$auth = reset($this->authMethods);
$auth->handleFailure($response);
}
/**
* Serializes the specified data.
* The default implementation will create a serializer based on the configuration given by [[serializer]].
* It then uses the serializer to serialize the given data.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment