diff --git a/docs/guide/rest.md b/docs/guide/rest.md index e143ac8..924d2f1 100644 --- a/docs/guide/rest.md +++ b/docs/guide/rest.md @@ -140,25 +140,6 @@ class User extends ActiveRecord In the following subsections, we will explain in more details about implementing RESTful APIs. -HTTP Status Code Summary ------------------------- - -* `200`: OK. Everything worked as expected. -* `201`: A resource was successfully created in response to a `POST` request. The `Location` header - contains the URL pointing to the newly created resource. -* `204`: The request is handled successfully and the response contains no body content (like a `DELETE` request). -* `304`: Resource was not modified. You can use the cached version. -* `400`: Bad request. This could be caused by various reasons from the user side, such as invalid JSON - data in the request body, invalid action parameters, etc. -* `401`: No valid API access token is provided. -* `403`: The authenticated user is not allowed to access the specified API endpoint. -* `404`: The requested resource does not exist. -* `405`: Method not allowed. Please check the `Allow` header for allowed HTTP methods. -* `415`: Unsupported media type. The requested content type or version number is invalid. -* `422`: Data validation failed (in response to a `POST` request, for example). Please check the response body for detailed error messages. -* `429`: Too many requests. The request is rejected due to rate limiting. -* `500`: Internal server error. This could be caused by internal program errors. - Data Formatting --------------- @@ -191,8 +172,30 @@ Caching Rate Limiting ------------- + +HTTP Status Code Summary +------------------------ + +* `200`: OK. Everything worked as expected. +* `201`: A resource was successfully created in response to a `POST` request. The `Location` header + contains the URL pointing to the newly created resource. +* `204`: The request is handled successfully and the response contains no body content (like a `DELETE` request). +* `304`: Resource was not modified. You can use the cached version. +* `400`: Bad request. This could be caused by various reasons from the user side, such as invalid JSON + data in the request body, invalid action parameters, etc. +* `401`: No valid API access token is provided. +* `403`: The authenticated user is not allowed to access the specified API endpoint. +* `404`: The requested resource does not exist. +* `405`: Method not allowed. Please check the `Allow` header for allowed HTTP methods. +* `415`: Unsupported media type. The requested content type or version number is invalid. +* `422`: Data validation failed (in response to a `POST` request, for example). Please check the response body for detailed error messages. +* `429`: Too many requests. The request is rejected due to rate limiting. +* `500`: Internal server error. This could be caused by internal program errors. + + Documentation ------------- Testing ------- + diff --git a/framework/rest/AuthInterface.php b/framework/rest/AuthInterface.php new file mode 100644 index 0000000..6117607 --- /dev/null +++ b/framework/rest/AuthInterface.php @@ -0,0 +1,41 @@ + + * @since 2.0 + */ +interface AuthInterface +{ + /** + * Authenticates the current user. + * + * @param User $user + * @param Request $request + * @param Response $response + * @return IdentityInterface the authenticated user identity. If authentication information is not provided, null will be returned. + * @throws UnauthorizedHttpException if authentication information is provided but is invalid. + */ + public function authenticate($user, $request, $response); + /** + * Handles authentication failure. + * The implementation should normally throw UnauthorizedHttpException to indicate authentication failure. + * @param Response $response + * @throws UnauthorizedHttpException + */ + public function handleFailure($response); +} diff --git a/framework/rest/Controller.php b/framework/rest/Controller.php index 28509c3..12c1783 100644 --- a/framework/rest/Controller.php +++ b/framework/rest/Controller.php @@ -8,6 +8,7 @@ namespace yii\rest; use Yii; +use yii\base\InvalidConfigException; use yii\web\Response; use yii\web\UnauthorizedHttpException; use yii\web\UnsupportedMediaTypeHttpException; @@ -33,18 +34,6 @@ class Controller extends \yii\web\Controller * The name of the header parameter representing the API version number. */ const HEADER_VERSION = 'version'; - /** - * HTTP Basic authentication. - */ - const AUTH_TYPE_BASIC = 'Basic'; - /** - * HTTP Bearer authentication (the token obtained through OAuth2) - */ - const AUTH_TYPE_BEARER = 'Bearer'; - /** - * Authentication by an access token passed via a query parameter - */ - const AUTH_TYPE_QUERY = 'Query'; /** * @var string|array the configuration for creating the serializer that formats the response data. @@ -55,18 +44,10 @@ class Controller extends \yii\web\Controller */ public $enableCsrfValidation = false; /** - * @var string|array the supported authentication type(s). Valid values include [[AUTH_TYPE_BASIC]], - * [[AUTH_TYPE_BEARER]] and [[AUTH_TYPE_QUERY]]. - */ - public $authType = [self::AUTH_TYPE_BASIC, self::AUTH_TYPE_BEARER, self::AUTH_TYPE_QUERY]; - /** - * @var string the authentication realm to display in case when authentication fails. - */ - public $authRealm = 'api'; - /** - * @var string the name of the query parameter containing the access token when [[AUTH_TYPE_QUERY]] is used. + * @var array the supported authentication methods. This property should take a list of supported + * authentication methods, each represented by an authentication class or configuration. */ - public $authParam = 'access-token'; + public $authMethods = ['yii\rest\HttpBasicAuth', 'yii\rest\HttpBearerAuth', 'yii\rest\QueryParamAuth']; /** * @var string the chosen API version number * @see supportedVersions @@ -182,36 +163,25 @@ class Controller extends \yii\web\Controller */ protected function authenticate() { - $request = Yii::$app->getRequest(); - foreach ((array)$this->authType as $authType) { - switch ($authType) { - case self::AUTH_TYPE_BASIC: - $accessToken = $request->getAuthUser(); - break; - case self::AUTH_TYPE_BEARER: - $authHeader = $request->getHeaders()->get('Authorization'); - if ($authHeader !== null && preg_match("/^{$this->authType}\\s+(.*?)$/", $authHeader, $matches)) { - $accessToken = $matches[1]; - } - break; - case self::AUTH_TYPE_QUERY: - $accessToken = $request->get($this->authParam); - break; - } - if (isset($accessToken)) { - break; - } + if (empty($this->authMethods)) { + return; } - if (!isset($accessToken) || !Yii::$app->getUser()->loginByAccessToken($accessToken)) { - if (!isset($accessToken, $authType)) { - $authType = is_array($this->authType) ? reset($this->authType) : $this->authType; - } - if ($authType !== self::AUTH_TYPE_QUERY) { - Yii::$app->getResponse()->getHeaders()->set('WWW-Authenticate', "{$authType} realm=\"{$this->authRealm}\""); + $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; } - throw new UnauthorizedHttpException(empty($accessToken) ? 'Access token required.' : 'You are requesting with an invalid access token.'); } + + /** @var AuthInterface $auth */ + $auth = reset($this->authMethods); + $auth->handleFailure($response); } /** diff --git a/framework/rest/HttpBasicAuth.php b/framework/rest/HttpBasicAuth.php new file mode 100644 index 0000000..7a69c15 --- /dev/null +++ b/framework/rest/HttpBasicAuth.php @@ -0,0 +1,50 @@ + + * @since 2.0 + */ +class HttpBasicAuth extends Component implements AuthInterface +{ + /** + * @var string the HTTP authentication realm + */ + public $realm = 'api'; + + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + if (($accessToken = $request->getAuthUser()) !== null) { + $identity = $user->loginByAccessToken($accessToken); + if ($identity !== null) { + return $identity; + } + $this->handleFailure($response); + } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + $response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } +} diff --git a/framework/rest/HttpBearerAuth.php b/framework/rest/HttpBearerAuth.php new file mode 100644 index 0000000..81033c9 --- /dev/null +++ b/framework/rest/HttpBearerAuth.php @@ -0,0 +1,52 @@ + + * @since 2.0 + */ +class HttpBearerAuth extends Component implements AuthInterface +{ + /** + * @var string the HTTP authentication realm + */ + public $realm = 'api'; + + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + $authHeader = $request->getHeaders()->get('Authorization'); + if ($authHeader !== null && preg_match("/^Bearer\\s+(.*?)$/", $authHeader, $matches)) { + $identity = $user->loginByAccessToken($matches[1]); + if ($identity !== null) { + return $identity; + } + + $this->handleFailure($response); + } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + $response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } +} diff --git a/framework/rest/QueryParamAuth.php b/framework/rest/QueryParamAuth.php new file mode 100644 index 0000000..f45e4c8 --- /dev/null +++ b/framework/rest/QueryParamAuth.php @@ -0,0 +1,52 @@ + + * @since 2.0 + */ +class QueryParamAuth extends Component implements AuthInterface +{ + /** + * @var string the parameter name for passing the access token + */ + public $tokenParam = 'access-token'; + + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + $accessToken = $request->get($this->tokenParam); + if (is_string($accessToken)) { + $identity = $user->loginByAccessToken($accessToken); + if ($identity !== null) { + return $identity; + } + } + if ($accessToken !== null) { + $this->handleFailure($response); + } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } +}