├── LICENSE ├── Module.php ├── README.md ├── components ├── AuthMethods │ ├── AuthMethod.php │ ├── HttpBearerAuth.php │ └── HttpMacAuth.php ├── AuthorizationValidators │ ├── BearerTokenValidator.php │ └── MacTokenValidator.php ├── Events │ └── AuthorizationEvent.php ├── Exception │ └── OAuthHttpException.php ├── Grant │ └── RevokeGrant.php ├── Mac.php ├── Psr7 │ ├── ServerRequest.php │ └── ServerResponse.php ├── Repositories │ ├── AccessTokenRepository.php │ ├── BearerTokenRepository.php │ ├── ClientRepository.php │ ├── MacTokenRepository.php │ ├── RefreshTokenRepository.php │ ├── RepositoryCacheInterface.php │ ├── RepositoryCacheTrait.php │ └── ScopeRepository.php ├── ResponseTypes │ ├── BearerTokenResponse.php │ ├── MacTokenResponse.php │ └── RevokeResponse.php └── Server │ ├── AuthorizationServer.php │ └── ResourceServer.php ├── composer.json ├── controllers ├── AuthorizeController.php ├── RevokeController.php └── TokenController.php ├── migrations └── m160920_072449_auth.php ├── models ├── AccessToken.php ├── AccessTokenQuery.php ├── Client.php ├── ClientQuery.php ├── EntityQueryTrait.php ├── EntityTrait.php ├── RefreshToken.php ├── RefreshTokenQuery.php ├── Scope.php ├── ScopeQuery.php └── TokenQueryTrait.php └── rbac ├── GrantedRule.php ├── OwnedInterface.php ├── OwnedQueryInterface.php ├── OwnedQueryTrait.php ├── OwnedRule.php ├── Permission.php └── ScopePermission.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrey Chervinka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Module.php: -------------------------------------------------------------------------------- 1 | [ 58 | 'class' => AuthorizeController::class, 59 | 'as corsFilter' => Cors::class, 60 | ], 61 | 'revoke' => [ 62 | 'class' => RevokeController::class, 63 | 'as corsFilter' => Cors::class, 64 | ], 65 | 'token' => [ 66 | 'class' => TokenController::class, 67 | 'as corsFilter' => Cors::class, 68 | ], 69 | ]; 70 | 71 | /** 72 | * @var array 73 | */ 74 | public $urlManagerRules = []; 75 | 76 | /** 77 | * @var CryptKey|string 78 | */ 79 | public $privateKey; 80 | 81 | /** 82 | * @var CryptKey|string 83 | */ 84 | public $publicKey; 85 | 86 | /** 87 | * @var callable 88 | */ 89 | public $enableGrantTypes; 90 | 91 | /** 92 | * @var array 93 | */ 94 | public $cache; 95 | 96 | /** 97 | * @var AuthorizationServer 98 | */ 99 | private $_authorizationServer; 100 | /** 101 | * @var string 102 | */ 103 | private $_encryptionKey; 104 | 105 | /** 106 | * @var ServerRequest 107 | */ 108 | private $_serverRequest; 109 | /** 110 | * @var ServerResponse 111 | */ 112 | private $_serverResponse; 113 | /** 114 | * @var AccessTokenRepositoryInterface 115 | */ 116 | private $_accessTokenRepository; 117 | 118 | /** 119 | * @var ClientEntityInterface|Client 120 | */ 121 | private $_clientEntity; 122 | /** 123 | * @var ResponseTypeInterface 124 | */ 125 | private $_responseType; 126 | 127 | 128 | /** 129 | * Sets module's URL manager rules on application's bootstrap. 130 | * @param Application $app 131 | */ 132 | public function bootstrap($app) 133 | { 134 | $app->getUrlManager() 135 | ->addRules((new GroupUrlRule([ 136 | 'ruleConfig' => [ 137 | 'class' => UrlRule::class, 138 | 'pluralize' => false, 139 | 'only' => ['create', 'options'] 140 | ], 141 | 'rules' => ArrayHelper::merge([ 142 | ['controller' => $this->uniqueId . '/authorize'], 143 | ['controller' => $this->uniqueId . '/revoke'], 144 | ['controller' => $this->uniqueId . '/token'], 145 | ], $this->urlManagerRules) 146 | ]))->rules, false); 147 | } 148 | 149 | public function __construct($id, $parent = null, $config = []) 150 | { 151 | parent::__construct($id, $parent, ArrayHelper::merge([ 152 | 'components' => [ 153 | 'userRepository' => [ 154 | 'class' => Yii::$app->user->identityClass, 155 | ], 156 | 'clientRepository' => [ 157 | 'class' => ClientRepository::class, 158 | ], 159 | 'scopeRepository' => [ 160 | 'class' => ScopeRepository::class, 161 | ], 162 | 'refreshTokenRepository' => [ 163 | 'class' => RefreshTokenRepository::class, 164 | ], 165 | ], 166 | ], $config)); 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function init() 173 | { 174 | parent::init(); 175 | 176 | if (!$this->privateKey instanceof CryptKey) { 177 | $this->privateKey = new CryptKey($this->privateKey); 178 | } 179 | if (!$this->publicKey instanceof CryptKey) { 180 | $this->publicKey = new CryptKey($this->publicKey); 181 | } 182 | } 183 | 184 | /** 185 | * @return AuthorizationServer 186 | * @throws OAuthServerException 187 | */ 188 | public function getAuthorizationServer() 189 | { 190 | if (!$this->_authorizationServer instanceof AuthorizationServer) { 191 | $this->prepareAuthorizationServer(); 192 | } 193 | 194 | return $this->_authorizationServer; 195 | } 196 | 197 | /** 198 | * @throws OAuthServerException 199 | */ 200 | protected function prepareAuthorizationServer() 201 | { 202 | $this->_responseType = ArrayHelper::getValue($this, 'clientEntity.responseType'); 203 | 204 | $this->_authorizationServer = new AuthorizationServer( 205 | $this->clientRepository, 206 | $this->accessTokenRepository, 207 | $this->scopeRepository, 208 | $this->privateKey, 209 | $this->_encryptionKey, 210 | $this->_responseType 211 | ); 212 | 213 | if (is_callable($this->enableGrantTypes) !== true) { 214 | $this->enableGrantTypes = function (Module &$module) { 215 | throw OAuthServerException::unsupportedGrantType(); 216 | }; 217 | } 218 | 219 | call_user_func_array($this->enableGrantTypes, [&$this]); 220 | } 221 | 222 | /** 223 | * @return BearerTokenRepository|MacTokenRepository|AccessTokenRepositoryInterface 224 | * @throws InvalidConfigException 225 | */ 226 | public function getAccessTokenRepository() 227 | { 228 | if (!$this->_accessTokenRepository instanceof AccessTokenRepositoryInterface) { 229 | $this->_accessTokenRepository = $this->prepareAccessTokenRepository(); 230 | } 231 | 232 | if ($this->_accessTokenRepository instanceof RepositoryCacheInterface) { 233 | $this->_accessTokenRepository->setCache( 234 | ArrayHelper::getValue($this->cache, AccessTokenRepositoryInterface::class) 235 | ); 236 | } 237 | 238 | return $this->_accessTokenRepository; 239 | } 240 | 241 | /** 242 | * @return BearerTokenRepository|MacTokenRepository 243 | * @throws InvalidConfigException 244 | */ 245 | protected function prepareAccessTokenRepository() 246 | { 247 | if ($this->_responseType instanceof MacTokenResponse) { 248 | return new MacTokenRepository($this->_encryptionKey); 249 | } 250 | 251 | return new BearerTokenRepository(); 252 | } 253 | 254 | /** 255 | * @return Client 256 | * @throws OAuthServerException 257 | */ 258 | protected function getClientEntity() 259 | { 260 | if (!$this->_clientEntity instanceof ClientEntityInterface) { 261 | $request = Yii::$app->request; 262 | $this->_clientEntity = $this->clientRepository 263 | ->getClientEntity( 264 | $request->getAuthUser(), 265 | null, // fixme: need to provide grant type 266 | $request->getAuthPassword() 267 | ); 268 | } 269 | 270 | if ($this->_clientEntity instanceof ClientEntityInterface) { 271 | return $this->_clientEntity; 272 | } 273 | 274 | throw OAuthServerException::invalidClient(); 275 | } 276 | 277 | /** 278 | * @param ClientEntityInterface $clientEntity 279 | */ 280 | public function setClientEntity(ClientEntityInterface $clientEntity) 281 | { 282 | $this->_clientEntity = $clientEntity; 283 | } 284 | 285 | /** 286 | * @return ServerRequest 287 | */ 288 | public function getServerRequest() 289 | { 290 | if (!$this->_serverRequest instanceof ServerRequest) { 291 | $request = Yii::$app->request; 292 | $this->_serverRequest = (new ServerRequest($request)) 293 | ->withParsedBody($request->bodyParams); 294 | } 295 | 296 | return $this->_serverRequest; 297 | } 298 | 299 | /** 300 | * @return ServerResponse 301 | */ 302 | public function getServerResponse() 303 | { 304 | if (!$this->_serverResponse instanceof ServerResponse) { 305 | $this->_serverResponse = new ServerResponse(); 306 | } 307 | 308 | return $this->_serverResponse; 309 | } 310 | 311 | /** 312 | * @param string $encryptionKey 313 | */ 314 | public function setEncryptionKey($encryptionKey) 315 | { 316 | $this->_encryptionKey = $encryptionKey; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii 2.0 OAuth 2.0 Server 2 | 3 | `chervand/yii2-oauth2-server` is a `Yii 2.0 PHP Framework` integration of [`thephpleague/oauth2-server`](https://github.com/thephpleague/oauth2-server) library which implements a standards compliant [`OAuth 2.0 Server`](https://tools.ietf.org/html/rfc6749) for PHP. It supports all of the grants defined in the specification with usage of `JWT` `Bearer` tokens. 4 | 5 | `chervand/yii2-oauth2-server` additionally provides [`MAC`](https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-05) tokens support, which is not supported by the original library, because `MAC` tokens specification is currently in draft and it was not updated since 2014, so it's a pretty experimental feature. 6 | 7 | It also includes tokens revocation implementation based on [RFC7009](https://tools.ietf.org/html/rfc7009). 8 | 9 | ## Installation 10 | 11 | ### Applying DB migrations 12 | 13 | ./yii migrate --migrationPath="@vendor/chervand/yii2-oauth2-server/migrations" 14 | 15 | ### Generating public and private keys 16 | 17 | See [OAuth 2.0 Server installation](https://oauth2.thephpleague.com/installation/) page. 18 | 19 | ### Integrating with your users 20 | 21 | To integrate OAuth 2.0 server with your users DB, you should implement `League\OAuth2\Server\Repositories\UserRepositoryInterface` for a `user` component's `identityClass` which should be extended from `chervand\yii2\oauth2\server\models\AccessToken`. `League\OAuth2\Server\Repositories\UserRepositoryInterface::getUserEntityByUserCredentials()` should return your user model instance implementing `League\OAuth2\Server\Entities\UserEntityInterface` or `null`. You may additionally add a foreign key for the `auth__access_token.user_id` column referencing your users table. You mau also override `getRateLimit()` to provider ` yii\filters\RateLimitInterface` with required values. 22 | 23 | ```php 24 | [ 31 | // ... 32 | 'user' => [ 33 | 'identityClass' => 'app\components\Identity', 34 | // ... 35 | ], 36 | // ... 37 | ], 38 | // ... 39 | ]; 40 | ``` 41 | 42 | ### Configuring the authorization server 43 | 44 | Module configuration: 45 | 46 | ```php 47 | [ 54 | 'oauth2', 55 | // ... 56 | ], 57 | 'modules' => [ 58 | 'oauth2' => [ 59 | 'class' => \chervand\yii2\oauth2\server\Module::class, 60 | 'privateKey' => __DIR__ . '/../private.key', 61 | 'publicKey' => __DIR__ . '/../public.key', 62 | 'cache' => [ 63 | \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface::class => [ 64 | 'cacheDuration' => 3600, 65 | 'cacheDependency' => new \yii\caching\FileDependency(['fileName' => 'example.txt']), 66 | ], 67 | \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface::class => [ 68 | 'cacheDuration' => 3600, 69 | 'cacheDependency' => new \yii\caching\FileDependency(['fileName' => 'example.txt']), 70 | ], 71 | ], 72 | 'enableGrantTypes' => function (\chervand\yii2\oauth2\server\Module &$module) { 73 | $server = $module->authorizationServer; 74 | $server->enableGrantType(new \League\OAuth2\Server\Grant\ImplicitGrant( 75 | new \DateInterval('PT1H') 76 | )); 77 | $server->enableGrantType(new \League\OAuth2\Server\Grant\PasswordGrant( 78 | $module->userRepository, 79 | $module->refreshTokenRepository 80 | )); 81 | $server->enableGrantType(new \League\OAuth2\Server\Grant\ClientCredentialsGrant()); 82 | $server->enableGrantType(new \League\OAuth2\Server\Grant\RefreshTokenGrant( 83 | $module->refreshTokenRepository 84 | )); 85 | $server->enableGrantType(new \chervand\yii2\oauth2\server\components\Grant\RevokeGrant( 86 | $module->refreshTokenRepository, 87 | $module->publicKey 88 | )); 89 | }, 90 | ], 91 | // ... 92 | ], 93 | // ... 94 | ]; 95 | ``` 96 | 97 | ### Configuring the resource server 98 | 99 | Controller's behaviors configuration: 100 | 101 | ```php 102 | getModule('oauth2'); 115 | 116 | $behaviors['authenticator'] = [ 117 | 'class' => \yii\filters\auth\CompositeAuth::class, 118 | 'authMethods' => [ 119 | [ 120 | 'class' => \chervand\yii2\oauth2\server\components\AuthMethods\HttpMacAuth::class, 121 | 'publicKey' => $auth->publicKey, 122 | 'cache' => $auth->cache, 123 | ], 124 | [ 125 | 'class' => \chervand\yii2\oauth2\server\components\AuthMethods\HttpBearerAuth::class, 126 | 'publicKey' => $auth->publicKey, 127 | 'cache' => $auth->cache, 128 | ], 129 | ] 130 | ]; 131 | 132 | $behaviors['rateLimiter'] = [ 133 | 'class' => \yii\filters\RateLimiter::class, 134 | ]; 135 | 136 | return $behaviors; 137 | } 138 | 139 | } 140 | ``` 141 | 142 | ### RBAC 143 | 144 | TBA 145 | -------------------------------------------------------------------------------- /components/AuthMethods/AuthMethod.php: -------------------------------------------------------------------------------- 1 | tokenTypeExists($request)) { 44 | return null; 45 | } 46 | 47 | $accessTokenRepository = $this->getAccessTokenRepository(); 48 | 49 | if ($accessTokenRepository instanceof RepositoryCacheInterface) { 50 | $accessTokenRepository->setCache( 51 | ArrayHelper::getValue($this->cache, AccessTokenRepositoryInterface::class) 52 | ); 53 | } 54 | 55 | return $this->validate( 56 | new ResourceServer( 57 | $accessTokenRepository, 58 | $this->publicKey, 59 | $this->getAuthorizationValidator() 60 | ), 61 | new ServerRequest( 62 | $this->request ?: \Yii::$app->getRequest() 63 | ), 64 | $this->response ?: \Yii::$app->getResponse(), 65 | $this->user ?: \Yii::$app->getUser() 66 | ); 67 | } 68 | 69 | protected function tokenTypeExists(Request &$request) 70 | { 71 | $authHeader = $request->getHeaders()->get('Authorization'); 72 | 73 | if ( 74 | $authHeader !== null && $this->getTokenType() !== null 75 | && preg_match('/^' . $this->getTokenType() . '\s+(.*?)$/', $authHeader, $matches) 76 | ) { 77 | return true; 78 | } 79 | 80 | return false; 81 | } 82 | 83 | /** 84 | * @return string 85 | */ 86 | protected abstract function getTokenType(); 87 | 88 | /** 89 | * @param ResourceServer $resourceServer 90 | * @param ServerRequest $serverRequest 91 | * @param Response $response 92 | * @param User $user 93 | * @return null|\yii\web\IdentityInterface 94 | * @throws HttpException 95 | * @throws OAuthHttpException 96 | */ 97 | protected function validate( 98 | ResourceServer $resourceServer, 99 | ServerRequest $serverRequest, 100 | Response $response, 101 | User $user 102 | ) 103 | { 104 | try { 105 | 106 | $serverRequest = $resourceServer 107 | ->validateAuthenticatedRequest($serverRequest); 108 | 109 | $identity = $user->loginByAccessToken( 110 | $serverRequest->getAttribute('oauth_access_token_id'), 111 | get_called_class() 112 | ); 113 | 114 | if ( 115 | $identity === null 116 | || $serverRequest->getAttribute('oauth_user_id') != $identity->getId() 117 | ) { 118 | $this->handleFailure($response); 119 | } 120 | 121 | /** @var BaseManager $authManager */ 122 | $authManager = \Yii::$app->authManager; 123 | if ($authManager instanceof BaseManager && $this->setAuthManagerDefaultRoles === true) { 124 | $authManager->defaultRoles = $serverRequest->getAttribute('oauth_scopes', []); 125 | } 126 | 127 | return $identity; 128 | 129 | } catch (OAuthServerException $e) { 130 | throw new OAuthHttpException($e); 131 | } catch (\Exception $e) { 132 | throw new HttpException(500, 'Unable to validate the request.', 0, YII_DEBUG ? $e : null); 133 | } 134 | } 135 | 136 | public function handleFailure($response) 137 | { 138 | throw OAuthServerException::accessDenied(); 139 | } 140 | 141 | /** 142 | * @return AccessTokenRepositoryInterface 143 | */ 144 | protected abstract function getAccessTokenRepository(); 145 | 146 | /** 147 | * @return \League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface 148 | */ 149 | protected abstract function getAuthorizationValidator(); 150 | } 151 | -------------------------------------------------------------------------------- /components/AuthMethods/HttpBearerAuth.php: -------------------------------------------------------------------------------- 1 | getHeaders()->set('WWW-Authenticate', "Bearer realm=\"{$this->realm}\""); 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | protected function getTokenType() 32 | { 33 | return 'Bearer'; 34 | } 35 | 36 | /** 37 | * @return AuthorizationValidatorInterface 38 | * @throws \yii\base\InvalidConfigException 39 | */ 40 | protected function getAuthorizationValidator() 41 | { 42 | if (!$this->_authorizationValidator instanceof AuthorizationValidatorInterface) { 43 | $this->_authorizationValidator = new BearerTokenValidator($this->getAccessTokenRepository()); 44 | } 45 | 46 | return $this->_authorizationValidator; 47 | } 48 | 49 | /** 50 | * @return AccessTokenRepositoryInterface 51 | * @throws \yii\base\InvalidConfigException 52 | */ 53 | protected function getAccessTokenRepository() 54 | { 55 | if (!$this->_accessTokenRepository instanceof AccessTokenRepositoryInterface) { 56 | $this->_accessTokenRepository = new BearerTokenRepository(); 57 | } 58 | 59 | return $this->_accessTokenRepository; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /components/AuthMethods/HttpMacAuth.php: -------------------------------------------------------------------------------- 1 | getHeaders()->set('WWW-Authenticate', 'MAC error="Invalid credentials"'); 20 | } 21 | 22 | protected function getTokenType() 23 | { 24 | return 'MAC'; 25 | } 26 | 27 | /** 28 | * @return AuthorizationValidatorInterface 29 | * @throws \yii\base\InvalidConfigException 30 | */ 31 | protected function getAuthorizationValidator() 32 | { 33 | if (!$this->_authorizationValidator instanceof AuthorizationValidatorInterface) { 34 | $this->_authorizationValidator = new MacTokenValidator($this->getAccessTokenRepository()); 35 | } 36 | 37 | return $this->_authorizationValidator; 38 | } 39 | 40 | /** 41 | * @return AccessTokenRepositoryInterface 42 | * @throws \yii\base\InvalidConfigException 43 | */ 44 | protected function getAccessTokenRepository() 45 | { 46 | if (!$this->_accessTokenRepository instanceof AccessTokenRepositoryInterface) { 47 | $this->_accessTokenRepository = new MacTokenRepository(); 48 | } 49 | 50 | return $this->_accessTokenRepository; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /components/AuthorizationValidators/BearerTokenValidator.php: -------------------------------------------------------------------------------- 1 | accessTokenRepository = $accessTokenRepository; 36 | } 37 | 38 | /** 39 | * Set the private key 40 | * 41 | * @param \League\OAuth2\Server\CryptKey $key 42 | */ 43 | public function setPublicKey(CryptKey $key) 44 | { 45 | $this->publicKey = $key; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function validateAuthorization(ServerRequestInterface $request) 52 | { 53 | if ($request->hasHeader('authorization') === false) { 54 | throw OAuthServerException::accessDenied('Missing "Authorization" header'); 55 | } 56 | 57 | try { 58 | // Attempt to parse and validate the JWT 59 | $token = (new Mac($request)) 60 | ->validate() 61 | ->getJwt(); 62 | 63 | if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) { 64 | throw OAuthServerException::accessDenied('Access token could not be verified'); 65 | } 66 | 67 | // Ensure access token hasn't expired 68 | $data = new ValidationData(); 69 | $data->setCurrentTime(time()); 70 | 71 | if ($token->validate($data) === false) { 72 | throw OAuthServerException::accessDenied('Access token is invalid'); 73 | } 74 | 75 | // Check if token has been revoked 76 | if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) { 77 | throw OAuthServerException::accessDenied('Access token has been revoked'); 78 | } 79 | 80 | // Return the request with additional attributes 81 | return $request 82 | ->withAttribute('oauth_access_token_id', $token->getClaim('jti')) 83 | ->withAttribute('oauth_client_id', $token->getClaim('aud')) 84 | ->withAttribute('oauth_user_id', $token->getClaim('sub')) 85 | ->withAttribute('oauth_scopes', $token->getClaim('scopes')); 86 | } catch (\InvalidArgumentException $exception) { 87 | // JWT couldn't be parsed so return the request as is 88 | throw OAuthServerException::accessDenied($exception->getMessage()); 89 | } catch (\RuntimeException $exception) { 90 | //JWR couldn't be parsed so return the request as is 91 | throw OAuthServerException::accessDenied('Error while decoding to JSON'); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /components/Events/AuthorizationEvent.php: -------------------------------------------------------------------------------- 1 | request = $request; 36 | $this->response = $response; 37 | } 38 | 39 | /** 40 | * @return Token 41 | */ 42 | public function getToken() 43 | { 44 | if ( 45 | !$this->_token instanceof Token 46 | && $this->response instanceof ResponseInterface 47 | ) { 48 | $response = Json::decode($this->response->getBody()->__toString()); 49 | if (array_key_exists('access_token', $response)) { 50 | $this->_token = (new Parser())->parse($response['access_token']); 51 | } 52 | } 53 | 54 | return $this->_token; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /components/Exception/OAuthHttpException.php: -------------------------------------------------------------------------------- 1 | getHint(); 21 | 22 | parent::__construct( 23 | $previous->getHttpStatusCode(), 24 | $hint ? $previous->getMessage() . ' ' . $hint . '.' : $previous->getMessage(), 25 | $previous->getCode(), 26 | YII_DEBUG === true ? $previous : null 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/Grant/RevokeGrant.php: -------------------------------------------------------------------------------- 1 | setRefreshTokenRepository($refreshTokenRepository); 34 | $this->setPublicKey($publicKey); 35 | } 36 | 37 | public function setPublicKey(CryptKey $key) 38 | { 39 | $this->publicKey = $key; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getIdentifier() 46 | { 47 | return 'revoke'; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function respondToAccessTokenRequest( 54 | ServerRequestInterface $request, 55 | ResponseTypeInterface $responseType, 56 | \DateInterval $accessTokenTTL 57 | ) 58 | { 59 | throw new \LogicException('This grant does not used this method'); 60 | } 61 | 62 | /** 63 | * "Note: invalid tokens do not cause an error response since the client 64 | * cannot handle such an error in a reasonable way. Moreover, the 65 | * purpose of the revocation request, invalidating the particular token, 66 | * is already achieved." 67 | * 68 | * @see https://tools.ietf.org/html/rfc7009#section-2.2 69 | * 70 | * @param ServerRequestInterface $request 71 | * @param ResponseTypeInterface $response 72 | * @return mixed 73 | * @throws OAuthServerException 74 | */ 75 | public function respondToRevokeTokenRequest( 76 | ServerRequestInterface $request, 77 | ResponseTypeInterface $response 78 | ) 79 | { 80 | $client = $this->validateClient($request); 81 | $this->invalidateToken($request, $client->getIdentifier()); 82 | 83 | return $response; 84 | } 85 | 86 | /** 87 | * "If the server is unable to locate the token using 88 | * the given hint, it MUST extend its search across all of its 89 | * supported token types." 90 | * 91 | * @see https://tools.ietf.org/html/rfc7009#section-2.1 92 | * 93 | * @param ServerRequestInterface $request 94 | * @param $clientId 95 | */ 96 | protected function invalidateToken(ServerRequestInterface $request, $clientId) 97 | { 98 | $tokenTypeHint = $this->getRequestParameter('token_type_hint', $request); 99 | 100 | $callStack = $tokenTypeHint == 'refresh_token' 101 | ? ['invalidateRefreshToken', 'invalidateAccessToken'] 102 | : ['invalidateAccessToken', 'invalidateRefreshToken']; 103 | 104 | foreach ($callStack as $function) { 105 | if (call_user_func([$this, $function], $request, $clientId) === true) { 106 | break; 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * @param ServerRequestInterface $request 113 | * @param $clientId 114 | * @return bool 115 | */ 116 | protected function invalidateAccessToken(ServerRequestInterface $request, $clientId) 117 | { 118 | $accessToken = $this->getRequestParameter('token', $request); 119 | if (is_null($accessToken)) { 120 | throw OAuthServerException::invalidRequest('token'); 121 | } 122 | 123 | try { 124 | $token = (new Parser())->parse($accessToken); 125 | } catch (\Exception $exception) { 126 | return false; 127 | } 128 | 129 | if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) { 130 | throw OAuthServerException::accessDenied('Access token could not be verified'); 131 | } 132 | 133 | if ($token->getClaim('aud') !== $clientId) { 134 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); 135 | throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); 136 | } 137 | 138 | $this->accessTokenRepository->revokeAccessToken($token->getClaim('jti')); 139 | 140 | return true; 141 | } 142 | 143 | /** 144 | * @param ServerRequestInterface $request 145 | * @param $clientId 146 | * @return bool 147 | */ 148 | protected function invalidateRefreshToken(ServerRequestInterface $request, $clientId) 149 | { 150 | $encryptedRefreshToken = $this->getRequestParameter('token', $request); 151 | if (is_null($encryptedRefreshToken)) { 152 | throw OAuthServerException::invalidRequest('token'); 153 | } 154 | 155 | try { 156 | $refreshToken = $this->decrypt($encryptedRefreshToken); 157 | } catch (\Exception $exception) { 158 | return false; 159 | } 160 | 161 | $refreshTokenData = json_decode($refreshToken, true); 162 | if ($refreshTokenData['client_id'] !== $clientId) { 163 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); 164 | throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); 165 | } 166 | 167 | $this->accessTokenRepository->revokeAccessToken($refreshTokenData['access_token_id']); 168 | $this->refreshTokenRepository->revokeRefreshToken($refreshTokenData['refresh_token_id']); 169 | 170 | return true; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /components/Mac.php: -------------------------------------------------------------------------------- 1 | getHeader('authorization'); 39 | $params = empty($header) ? [] : $header[0]; 40 | } 41 | 42 | if (is_string($params)) { 43 | $params = $this->prepare($params); 44 | } 45 | 46 | if (!is_array($params)) { 47 | throw OAuthServerException::serverError('MAC construction failed'); 48 | } 49 | 50 | $this->_request = $request; 51 | $this->_params = $params; 52 | } 53 | 54 | /** 55 | * Prepares MAC params array from the `Authorization` header value. 56 | * 57 | * @internal 58 | * @param string $header 59 | * @param array $required required params 60 | * @param array $optional optional params, name => default 61 | * @return array parsed MAC params 62 | * @throws OAuthServerException 63 | */ 64 | protected function prepare( 65 | $header, 66 | $required = ['kid', 'ts', 'access_token', 'mac'], 67 | $optional = ['h' => ['host'], 'seq-nr' => null, 'cb' => null] 68 | ) 69 | { 70 | $mac = []; 71 | $params = explode(',', preg_replace('/^(?:\s+)?MAC\s/', '', $header)); 72 | array_walk($params, function (&$param) use (&$mac) { 73 | $param = array_map('trim', explode('=', $param, 2)); 74 | if (count($param) != 2) { 75 | throw OAuthServerException::accessDenied('Error while parsing MAC params'); 76 | } 77 | if ($param[0] == 'h') { 78 | $mac[$param[0]] = explode(':', trim($param[1], '"')); 79 | } else { 80 | $mac[$param[0]] = trim($param[1], '"'); 81 | } 82 | }); 83 | 84 | foreach ($required as $param) { 85 | if (!array_key_exists($param, $mac)) { 86 | throw OAuthServerException::accessDenied("Required MAC param `$param` missing"); 87 | } 88 | } 89 | 90 | return array_merge($optional, $mac); 91 | } 92 | 93 | public function __get($name) 94 | { 95 | if (isset($this->_params[$name])) { 96 | return $this->_params[$name]; 97 | } else { 98 | $name = str_replace('_', '-', $name); 99 | if (isset($this->_params[$name])) { 100 | return $this->_params[$name]; 101 | } 102 | } 103 | 104 | return null; 105 | } 106 | 107 | public function validate() 108 | { 109 | $values = array_merge( 110 | [$this->getStartLine()], 111 | $this->getHeaders(), 112 | [$this->ts, $this->seq_nr] 113 | ); 114 | 115 | $mac = hash_hmac( 116 | $this->getAlgorithm(), 117 | implode('\\n', array_filter($values)) . '\\n', 118 | $this->getJwt()->getClaim('mac_key') 119 | ); 120 | 121 | if (base64_encode($mac) === $this->mac) { 122 | return $this; 123 | } 124 | 125 | throw OAuthServerException::accessDenied('MAC validation failed'); 126 | } 127 | 128 | protected function getStartLine() 129 | { 130 | return implode(' ', [ 131 | $this->_request->getMethod(), 132 | $this->_request->getUri(), 133 | 'HTTP/' . $this->_request->getProtocolVersion() 134 | ]); 135 | } 136 | 137 | protected function getHeaders() 138 | { 139 | return array_map(function ($name) { 140 | $h = $this->_request->getHeader($name); 141 | return empty($h) ? null : $h[0]; 142 | }, $this->h); 143 | } 144 | 145 | protected function getAlgorithm() 146 | { 147 | return 'sha256'; 148 | } 149 | 150 | public function getJwt() 151 | { 152 | if (!isset($this->_jwt)) { 153 | $this->_jwt = (new Parser())->parse($this->access_token); 154 | } 155 | 156 | return $this->_jwt instanceof Token ? $this->_jwt : new Token(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /components/Psr7/ServerRequest.php: -------------------------------------------------------------------------------- 1 | method, 22 | $request->url, 23 | $request->headers->toArray(), 24 | $request->rawBody 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/Psr7/ServerResponse.php: -------------------------------------------------------------------------------- 1 | _tokenEntityClass = ArrayHelper::getValue( 54 | \Yii::$app, 'user.identityClass', 55 | AccessToken::class 56 | ); 57 | 58 | if ( 59 | class_exists($this->_tokenEntityClass) !== true 60 | || in_array( 61 | AccessTokenEntityInterface::class, 62 | class_implements($this->_tokenEntityClass) 63 | ) !== true 64 | ) { 65 | $this->_tokenEntityClass = AccessToken::class; 66 | } 67 | 68 | $this->_tokenTypeId = $tokenTypeId; 69 | $this->setEncryptionKey($encryptionKey); 70 | } 71 | 72 | /** 73 | * Create a new access token instance. 74 | * 75 | * @param ClientEntityInterface|Client $clientEntity 76 | * @param ScopeEntityInterface[] $scopes 77 | * @param mixed $userIdentifier 78 | * @return AccessTokenEntityInterface 79 | * @throws OAuthServerException 80 | */ 81 | public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null) 82 | { 83 | $token = new $this->_tokenEntityClass(); 84 | 85 | if ($token instanceof AccessToken) { 86 | $token->client_id = $clientEntity->id; 87 | $token->type = $clientEntity->token_type; 88 | } 89 | 90 | if ($token->validate() === true) { 91 | return $token; 92 | } 93 | 94 | throw OAuthServerException::serverError('Token creation failed'); 95 | } 96 | 97 | /** 98 | * Persists a new access token to permanent storage. 99 | * 100 | * @param AccessTokenEntityInterface $accessTokenEntity 101 | * @return AccessTokenEntityInterface 102 | */ 103 | public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity) 104 | { 105 | if ($accessTokenEntity instanceof AccessToken) { 106 | if ($this->_tokenTypeId === AccessToken::TYPE_MAC) { 107 | $accessTokenEntity->type = AccessToken::TYPE_MAC; 108 | $accessTokenEntity->mac_key = $this->encrypt($accessTokenEntity->getIdentifier()); 109 | } 110 | $accessTokenEntity->expired_at = $accessTokenEntity->getExpiryDateTime()->getTimestamp(); 111 | 112 | 113 | // TODO[d6, 14/10/16]: transaction 114 | if ($accessTokenEntity->save()) { 115 | foreach ($accessTokenEntity->getScopes() as $scope) { 116 | if ($scope instanceof Scope) { 117 | $accessTokenEntity->link('grantedScopes', $scope); 118 | } 119 | } 120 | } 121 | } 122 | 123 | return $accessTokenEntity; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | * @throws \Throwable 129 | */ 130 | public function isAccessTokenRevoked($tokenId) 131 | { 132 | $token = $this->getCachedToken( 133 | $tokenId, 134 | $this->getCacheDuration(), 135 | $this->getCacheDependency() 136 | ); 137 | 138 | if ( 139 | $token instanceof AccessToken 140 | && $token->type !== $this->_tokenTypeId 141 | ) { 142 | $this->revokeAccessToken($tokenId); 143 | return true; 144 | } 145 | 146 | return $token instanceof AccessToken === false; 147 | } 148 | 149 | /** 150 | * {@inheritdoc} 151 | */ 152 | public function revokeAccessToken($tokenId) 153 | { 154 | $token = $this->getCachedToken( 155 | $tokenId, 156 | $this->getCacheDuration(), 157 | $this->getCacheDependency() 158 | ); 159 | 160 | if ($token instanceof AccessToken) { 161 | 162 | $token->updateAttributes([ 163 | 'status' => AccessToken::STATUS_REVOKED, 164 | 'updated_at' => time(), 165 | ]); 166 | 167 | TagDependency::invalidate( 168 | \Yii::$app->cache, 169 | static::class 170 | ); 171 | 172 | } 173 | } 174 | 175 | /** 176 | * @param $tokenId 177 | * @param null|int $duration 178 | * @param null|Dependency $dependency 179 | * @return AccessToken|null 180 | */ 181 | protected function getCachedToken($tokenId, $duration = null, $dependency = null) 182 | { 183 | try { 184 | $token = AccessToken::getDb() 185 | ->cache( 186 | function () use ($tokenId) { 187 | return AccessToken::find() 188 | ->identifier($tokenId) 189 | ->active()->one(); 190 | }, 191 | $duration, 192 | $dependency instanceof Dependency 193 | ? $dependency 194 | : new TagDependency(['tags' => static::class]) 195 | ); 196 | } catch (\Throwable $exception) { 197 | $token = null; 198 | \Yii::error($exception->getMessage()); 199 | } 200 | 201 | return $token; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /components/Repositories/BearerTokenRepository.php: -------------------------------------------------------------------------------- 1 | setAttribute( 43 | 'expired_at', 44 | $refreshTokenEntity->getExpiryDateTime()->getTimestamp() 45 | ); 46 | if ($refreshTokenEntity->save()) { 47 | return $refreshTokenEntity; 48 | } 49 | } 50 | 51 | throw OAuthServerException::serverError('Refresh token failure'); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | * @throws \Throwable 57 | */ 58 | public function isRefreshTokenRevoked($tokenId) 59 | { 60 | $token = $this->getCachedToken( 61 | $tokenId, 62 | $this->getCacheDuration(), 63 | $this->getCacheDependency() 64 | ); 65 | 66 | return $token instanceof RefreshToken === false; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function revokeRefreshToken($tokenId) 73 | { 74 | $token = $this->getCachedToken( 75 | $tokenId, 76 | $this->getCacheDuration(), 77 | $this->getCacheDependency() 78 | ); 79 | 80 | if ($token instanceof RefreshToken) { 81 | 82 | $token->updateAttributes([ 83 | 'status' => RefreshToken::STATUS_REVOKED, 84 | 'updated_at' => time(), 85 | ]); 86 | 87 | TagDependency::invalidate( 88 | \Yii::$app->cache, 89 | static::class 90 | ); 91 | 92 | } 93 | } 94 | 95 | 96 | /** 97 | * @param $tokenId 98 | * @param null|int $duration 99 | * @param null|Dependency $dependency 100 | * @return RefreshToken|null 101 | */ 102 | protected function getCachedToken($tokenId, $duration = null, $dependency = null) 103 | { 104 | try { 105 | $token = RefreshToken::getDb() 106 | ->cache( 107 | function () use ($tokenId) { 108 | return RefreshToken::find() 109 | ->identifier($tokenId) 110 | ->active()->one(); 111 | }, 112 | $duration, 113 | $dependency instanceof Dependency 114 | ? $dependency 115 | : new TagDependency(['tags' => static::class]) 116 | ); 117 | } catch (\Throwable $exception) { 118 | $token = null; 119 | } 120 | 121 | return $token; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /components/Repositories/RepositoryCacheInterface.php: -------------------------------------------------------------------------------- 1 | _cacheDuration; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getCacheDependency() 23 | { 24 | return $this->_cacheDependency; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function setCacheDuration($cacheDuration) 31 | { 32 | $this->_cacheDuration = $cacheDuration; 33 | return $this; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function setCacheDependency($cacheDependency) 40 | { 41 | $this->_cacheDependency = $cacheDependency; 42 | return $this; 43 | } 44 | 45 | /** 46 | * @param array|null $config 47 | * @return RepositoryCacheTrait 48 | */ 49 | public function setCache($config) 50 | { 51 | if (isset($config['cacheDuration'])) { 52 | $this->setCacheDuration($config['cacheDuration']); 53 | } 54 | 55 | if (isset($config['cacheDependency'])) { 56 | $this->setCacheDependency($config['cacheDependency']); 57 | } 58 | 59 | return $this; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /components/Repositories/ScopeRepository.php: -------------------------------------------------------------------------------- 1 | cache( 33 | function () use ($identifier) { 34 | return Scope::find() 35 | ->identifier($identifier) 36 | ->one(); 37 | }, 38 | $this->getCacheDuration(), 39 | $this->getCacheDependency() 40 | ); 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | * @throws Throwable 46 | */ 47 | public function finalizeScopes( 48 | array $scopes, 49 | $grantType, 50 | ClientEntityInterface $clientEntity, 51 | $userIdentifier = null 52 | ) { 53 | 54 | /** @var Client $clientEntity */ 55 | return $clientEntity::getDb() 56 | ->cache( 57 | function () use ($scopes, $grantType, $clientEntity, $userIdentifier) { 58 | 59 | $permittedScopes = $clientEntity->getRelatedScopes( 60 | 61 | function (ActiveQuery $query) use ($scopes, $grantType, $userIdentifier) { 62 | 63 | if (empty($scopes) === true) { 64 | $query->andWhere(['is_default' => true]); 65 | } 66 | 67 | // common and assigned to user 68 | $query->andWhere(['or', ['user_id' => null], ['user_id' => $userIdentifier]]); 69 | 70 | // common and grant-specific 71 | $query->andWhere([ 72 | 'or', 73 | ['grant_type' => null], 74 | ['grant_type' => Client::getGrantTypeId($grantType)] 75 | ]); 76 | 77 | } 78 | ); 79 | 80 | if (empty($scopes) === false) { 81 | $permittedScopes->andWhere(['in', 'identifier', $scopes]); 82 | } 83 | 84 | return $permittedScopes->all(); 85 | }, 86 | $this->getCacheDuration(), 87 | $this->getCacheDependency() 88 | ); 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /components/ResponseTypes/BearerTokenResponse.php: -------------------------------------------------------------------------------- 1 | accessToken->getExpiryDateTime()->getTimestamp(); 25 | 26 | $jwtAccessToken = $this->accessToken->convertToJWT($this->privateKey); 27 | 28 | $responseParams = [ 29 | 'token_type' => 'mac', 30 | 'expires_in' => $expireDateTime - (new \DateTime())->getTimestamp(), 31 | 'access_token' => (string)$jwtAccessToken, 32 | 'kid' => $this->accessToken->identifier, 33 | 'mac_key' => $this->accessToken->mac_key, 34 | 'mac_algorithm' => $this->accessToken->getMacAlgorithm() 35 | ]; 36 | 37 | if ($this->refreshToken instanceof RefreshTokenEntityInterface) { 38 | $refreshToken = $this->encrypt( 39 | json_encode( 40 | [ 41 | 'client_id' => $this->accessToken->getClient()->getIdentifier(), 42 | 'refresh_token_id' => $this->refreshToken->getIdentifier(), 43 | 'access_token_id' => $this->accessToken->getIdentifier(), 44 | 'scopes' => $this->accessToken->getScopes(), 45 | 'user_id' => $this->accessToken->getUserIdentifier(), 46 | 'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(), 47 | ] 48 | ) 49 | ); 50 | 51 | $responseParams['refresh_token'] = $refreshToken; 52 | } 53 | 54 | $response = $response 55 | ->withStatus(200) 56 | ->withHeader('pragma', 'no-cache') 57 | ->withHeader('cache-control', 'no-store') 58 | ->withHeader('content-type', 'application/json; charset=UTF-8'); 59 | 60 | $response->getBody()->write(json_encode($responseParams)); 61 | 62 | return $response; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /components/ResponseTypes/RevokeResponse.php: -------------------------------------------------------------------------------- 1 | withStatus(200) 22 | ->withHeader('pragma', 'no-cache') 23 | ->withHeader('cache-control', 'no-store'); 24 | 25 | return $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/Server/AuthorizationServer.php: -------------------------------------------------------------------------------- 1 | getEmitter()->emit( 27 | new AuthorizationEvent( 28 | AuthorizationEvent::USER_AUTHENTICATION_SUCCEED, 29 | $request, 30 | $response 31 | ) 32 | ); 33 | } 34 | 35 | return $response; 36 | } 37 | 38 | /** 39 | * @param ServerRequestInterface $request 40 | * @param ResponseInterface $response 41 | * @return ResponseInterface 42 | * @throws OAuthServerException 43 | */ 44 | public function respondToRevokeTokenRequest( 45 | ServerRequestInterface $request, 46 | ResponseInterface $response 47 | ) 48 | { 49 | if ( 50 | array_key_exists('revoke', $this->enabledGrantTypes) === true 51 | && $this->enabledGrantTypes['revoke'] instanceof RevokeGrant 52 | ) { 53 | 54 | $this->responseType = new RevokeResponse(); 55 | 56 | /** @var RevokeGrant $revokeGrant */ 57 | $revokeGrant = $this->enabledGrantTypes['revoke']; 58 | $revokeResponse = $revokeGrant->respondToRevokeTokenRequest($request, $this->getResponseType()); 59 | 60 | if ($revokeResponse instanceof ResponseTypeInterface) { 61 | return $revokeResponse->generateHttpResponse($response); 62 | } 63 | 64 | } 65 | 66 | throw OAuthServerException::unsupportedGrantType(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /components/Server/ResourceServer.php: -------------------------------------------------------------------------------- 1 | =5.6.0", 33 | "yiisoft/yii2": "^2.0", 34 | "league/oauth2-server": "^6.1", 35 | "guzzlehttp/guzzle": "^6.3", 36 | "lcobucci/jwt": "3.3.2" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "chervand\\yii2\\oauth2\\server\\": "/" 41 | } 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "1.0.x-dev" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /controllers/AuthorizeController.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'class' => OptionsAction::class, 18 | ], 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /controllers/RevokeController.php: -------------------------------------------------------------------------------- 1 | [ 29 | 'class' => OptionsAction::class, 30 | ], 31 | ]; 32 | } 33 | 34 | /** 35 | * @return mixed 36 | * @throws HttpException 37 | * @throws OAuthHttpException 38 | */ 39 | public function actionCreate() 40 | { 41 | /** @var Module $module */ 42 | $module = $this->module; 43 | 44 | try { 45 | 46 | $response = $module->getAuthorizationServer() 47 | ->respondToRevokeTokenRequest( 48 | $module->getServerRequest(), 49 | $module->getServerResponse() 50 | ); 51 | 52 | return Json::decode($response->getBody()->__toString()); 53 | 54 | } catch (OAuthServerException $exception) { 55 | 56 | throw new OAuthHttpException($exception); 57 | 58 | } catch (\Exception $exception) { 59 | 60 | throw new HttpException( 61 | 500, 'Unable to respond to revoke token request.', 0, 62 | YII_DEBUG ? $exception : null 63 | ); 64 | 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /controllers/TokenController.php: -------------------------------------------------------------------------------- 1 | [ 30 | 'class' => OptionsAction::class, 31 | ], 32 | ]; 33 | } 34 | 35 | /** 36 | * @return mixed 37 | * @throws HttpException 38 | * @throws BadRequestHttpException 39 | * @throws OAuthHttpException 40 | */ 41 | public function actionCreate() 42 | { 43 | /** @var Module $module */ 44 | $module = $this->module; 45 | 46 | try { 47 | 48 | $response = $module->getAuthorizationServer() 49 | ->respondToAccessTokenRequest( 50 | $module->getServerRequest(), 51 | $module->getServerResponse() 52 | ); 53 | 54 | return Json::decode($response->getBody()->__toString()); 55 | 56 | } catch (OAuthServerException $exception) { 57 | 58 | throw new OAuthHttpException($exception); 59 | 60 | } catch (BadRequestHttpException $exception) { 61 | 62 | throw $exception; 63 | 64 | } catch (\Exception $exception) { 65 | 66 | throw new HttpException( 67 | 500, 'Unable to respond to access token request.', 0, 68 | YII_DEBUG ? $exception : null 69 | ); 70 | 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /migrations/m160920_072449_auth.php: -------------------------------------------------------------------------------- 1 | [ 24 | 'id' => Schema::TYPE_PK, 25 | 'identifier' => Schema::TYPE_STRING . ' NOT NULL', 26 | 'secret' => Schema::TYPE_STRING, // not confidential if null 27 | 'name' => Schema::TYPE_STRING . ' NOT NULL', 28 | 'redirect_uri' => Schema::TYPE_STRING, 29 | 'token_type' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 1', // Bearer 30 | 'grant_type' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 1', // Authorization Code 31 | 'created_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 32 | 'updated_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 33 | 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 1', // Active, 34 | 'KEY (token_type)', 35 | 'KEY (grant_type)', 36 | 'KEY (status)', 37 | ], 38 | '{{%auth__access_token}}' => [ 39 | 'id' => Schema::TYPE_UBIGPK, 40 | 'client_id' => Schema::TYPE_INTEGER . ' NOT NULL', 41 | 'user_id' => Schema::TYPE_INTEGER, 42 | 'identifier' => Schema::TYPE_STRING . ' NOT NULL', 43 | 'type' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 1', // Bearer 44 | 'mac_key' => 'varchar(500)', 45 | 'mac_algorithm' => Schema::TYPE_SMALLINT, 46 | 'allowance' => Schema::TYPE_SMALLINT, 47 | 'allowance_updated_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 48 | 'created_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 49 | 'updated_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 50 | 'expired_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 51 | 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 1', // Active, 52 | 'FOREIGN KEY (client_id) REFERENCES {{%auth__client}} (id) ON DELETE CASCADE ON UPDATE CASCADE', 53 | 'KEY (type)', 54 | 'KEY (mac_algorithm)', 55 | 'KEY (status)', 56 | ], 57 | '{{%auth__scope}}' => [ 58 | 'id' => Schema::TYPE_PK, 59 | 'identifier' => Schema::TYPE_STRING . ' NOT NULL', 60 | 'name' => Schema::TYPE_STRING, 61 | ], 62 | '{{%auth__client_scope}}' => [ 63 | 'id' => Schema::TYPE_PK, 64 | 'client_id' => Schema::TYPE_INTEGER . ' NOT NULL', 65 | 'scope_id' => Schema::TYPE_INTEGER . ' NOT NULL', 66 | 'user_id' => Schema::TYPE_INTEGER . ' DEFAULT NULL', // common if null 67 | 'grant_type' => Schema::TYPE_SMALLINT . ' DEFAULT NULL', // all grants if null 68 | 'is_default' => Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 0', 69 | 'UNIQUE KEY (client_id, scope_id, user_id, grant_type)', 70 | 'FOREIGN KEY (client_id) REFERENCES {{%auth__client}} (id) ON DELETE CASCADE ON UPDATE CASCADE', 71 | 'FOREIGN KEY (scope_id) REFERENCES {{%auth__scope}} (id) ON DELETE CASCADE ON UPDATE CASCADE', 72 | 'KEY (grant_type)', 73 | 'KEY (is_default)', 74 | ], 75 | '{{%auth__access_token_scope}}' => [ 76 | 'access_token_id' => Schema::TYPE_BIGINT . ' UNSIGNED NOT NULL', 77 | 'scope_id' => Schema::TYPE_INTEGER . ' NOT NULL', 78 | 'PRIMARY KEY (access_token_id, scope_id)', 79 | 'FOREIGN KEY (access_token_id) REFERENCES {{%auth__access_token}} (id) ON DELETE CASCADE ON UPDATE CASCADE', 80 | 'FOREIGN KEY (scope_id) REFERENCES {{%auth__scope}} (id) ON DELETE CASCADE ON UPDATE CASCADE', 81 | ], 82 | '{{%auth__refresh_token}}' => [ 83 | 'id' => Schema::TYPE_UBIGPK, 84 | 'access_token_id' => Schema::TYPE_BIGINT . ' UNSIGNED NOT NULL', 85 | 'identifier' => Schema::TYPE_STRING . ' NOT NULL', 86 | 'created_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 87 | 'updated_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 88 | 'expired_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 89 | 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 1', // Active, 90 | 'FOREIGN KEY (access_token_id) REFERENCES {{%auth__access_token}} (id) ON DELETE CASCADE ON UPDATE CASCADE', 91 | 'KEY (status)', 92 | ], 93 | '{{%auth__auth_code}}' => [ 94 | 'id' => Schema::TYPE_PK, 95 | ], 96 | ]; 97 | } 98 | 99 | public function safeUp() 100 | { 101 | foreach (static::_tables() as $name => $attributes) { 102 | try { 103 | $this->createTable($name, $attributes, $this->_tableOptions); 104 | } catch (\Exception $e) { 105 | echo $e->getMessage(), "\n"; 106 | return false; 107 | } 108 | } 109 | 110 | return true; 111 | } 112 | 113 | public function safeDown() 114 | { 115 | foreach (array_reverse(static::_tables()) as $name => $attributes) { 116 | try { 117 | $this->dropTable($name); 118 | } catch (\Exception $e) { 119 | echo "m160920_072449_oauth cannot be reverted.\n"; 120 | echo $e->getMessage(), "\n"; 121 | return false; 122 | } 123 | } 124 | 125 | return true; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /models/AccessToken.php: -------------------------------------------------------------------------------- 1 | hasOne(Client::class, ['id' => 'client_id'])/* todo: ->inverseOf('accessTokens') */; 75 | } 76 | 77 | public function getMacAlgorithm() 78 | { 79 | return ArrayHelper::getValue( 80 | static::algorithms(), 81 | $this->mac_algorithm, 82 | 'hmac-sha-256' 83 | ); 84 | } 85 | 86 | public static function algorithms() 87 | { 88 | return [ 89 | static::MAC_ALGORITHM_HMAC_SHA1 => 'hmac-sha-1', 90 | static::MAC_ALGORITHM_HMAC_SHA256 => 'hmac-sha-256', 91 | ]; 92 | } 93 | 94 | public function rules() 95 | { 96 | return [ 97 | [['client_id'], 'required'], // identifier 98 | [['user_id'], 'default'], 99 | ['type', 'default', 'value' => static::TYPE_BEARER], 100 | ['type', 'in', 'range' => [static::TYPE_BEARER, static::TYPE_MAC]], 101 | ['mac_algorithm', 'default', 'value' => static::MAC_ALGORITHM_HMAC_SHA256], 102 | ['mac_algorithm', 'in', 'range' => array_keys(static::algorithms())], 103 | [['!allowance'], 'default'], 104 | [['!allowance_updated_at', '!created_at', '!updated_at'], 'default', 'value' => time()], 105 | ['status', 'default', 'value' => static::STATUS_ACTIVE], 106 | ['status', 'in', 'range' => [static::STATUS_REVOKED, static::STATUS_ACTIVE]], 107 | ]; 108 | } 109 | 110 | public function getGrantedScopes() 111 | { 112 | return $this->hasMany(Scope::class, ['id' => 'scope_id']) 113 | ->viaTable('{{auth__access_token_scope}}', ['access_token_id' => 'id']); 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | */ 119 | public function convertToJWT(CryptKey $privateKey) 120 | { 121 | $builder = (new Builder()) 122 | ->setAudience($this->getClient()->getIdentifier()) 123 | ->setId($this->getIdentifier(), true) 124 | ->setIssuedAt(time()) 125 | ->setNotBefore(time()) 126 | ->setExpiration($this->getExpiryDateTime()->getTimestamp()) 127 | ->setSubject($this->getUserIdentifier()) 128 | ->set('scopes', $this->getScopes()); 129 | 130 | if ($this->type == static::TYPE_MAC) { 131 | $builder 132 | ->setHeader('kid', $this->identifier) 133 | ->set('kid', $this->identifier) 134 | ->set('mac_key', $this->mac_key); 135 | } 136 | 137 | $builder = $this->finalizeJWTBuilder($builder); 138 | 139 | return $builder 140 | ->sign(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase())) 141 | ->getToken(); 142 | } 143 | 144 | /** 145 | * Override it in order to set additional public or private claims. 146 | * 147 | * @param Builder $builder 148 | * @return Builder 149 | * @see https://tools.ietf.org/html/rfc7519#section-4 150 | */ 151 | protected function finalizeJWTBuilder(Builder $builder) 152 | { 153 | return $builder; 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | public function getClient() 160 | { 161 | return $this->relatedClient; 162 | } 163 | 164 | public function getScopes() 165 | { 166 | if (empty($this->scopes)) { 167 | $this->scopes = $this->grantedScopes; 168 | } 169 | 170 | return array_values($this->scopes); 171 | } 172 | 173 | /** 174 | * {@inheritdoc} 175 | */ 176 | public function setUserIdentifier($identifier) 177 | { 178 | $this->user_id = $identifier; 179 | } 180 | 181 | /** 182 | * {@inheritdoc} 183 | */ 184 | public function getUserIdentifier() 185 | { 186 | return $this->user_id; 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function getRateLimit($request, $action) 193 | { 194 | return [1000, 600]; 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function loadAllowance($request, $action) 201 | { 202 | return [ 203 | $this->allowance === null ? 1000 : $this->allowance, 204 | $this->allowance_updated_at 205 | ]; 206 | } 207 | 208 | /** 209 | * {@inheritdoc} 210 | */ 211 | public function saveAllowance($request, $action, $allowance, $timestamp) 212 | { 213 | $this->updateAttributes([ 214 | 'allowance' => $allowance, 215 | 'allowance_updated_at' => $timestamp, 216 | 'updated_at' => $timestamp, 217 | ]); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /models/AccessTokenQuery.php: -------------------------------------------------------------------------------- 1 | andWhere(['type' => $type]); 17 | } 18 | 19 | /** 20 | * @return ClientQuery|ActiveQuery 21 | */ 22 | public function expired() 23 | { 24 | return $this; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/Client.php: -------------------------------------------------------------------------------- 1 | cache( 89 | function () use ($clientIdentifier, $grantType) { 90 | return static::find() 91 | ->active() 92 | ->identifier($clientIdentifier) 93 | ->grant($grantType) 94 | ->one(); 95 | } 96 | ); 97 | 98 | if ( 99 | $clientEntity instanceof static 100 | && ( 101 | $clientEntity->getIsConfidential() !== true 102 | || $mustValidateSecret !== true 103 | || static::secretVerify($clientSecret, $clientEntity->secret) === true 104 | ) 105 | ) { 106 | 107 | return $clientEntity; 108 | } 109 | 110 | } catch (\Throwable $exception) { 111 | 112 | } 113 | 114 | return null; 115 | } 116 | 117 | public static function grants() 118 | { 119 | return [ 120 | static::GRANT_TYPE_AUTHORIZATION_CODE => 'authorization_code', 121 | static::GRANT_TYPE_IMPLICIT => 'implicit', 122 | static::GRANT_TYPE_PASSWORD => 'password', 123 | static::GRANT_TYPE_CLIENT_CREDENTIALS => 'client_credentials', 124 | static::GRANT_TYPE_REFRESH_TOKEN => 'refresh_token', 125 | static::GRANT_TYPE_REVOKE => 'revoke', 126 | ]; 127 | } 128 | 129 | public static function getGrantTypeId($grantType, $default = null) 130 | { 131 | return ArrayHelper::getValue(array_flip(static::grants()), $grantType, $default); 132 | } 133 | 134 | public static function secretHash($secret) 135 | { 136 | return password_hash($secret, PASSWORD_DEFAULT); 137 | } 138 | 139 | public static function secretVerify($secret, $hash) 140 | { 141 | return password_verify($secret, $hash); 142 | } 143 | 144 | public function getIdentifier() 145 | { 146 | return $this->identifier; 147 | } 148 | 149 | public function getName() 150 | { 151 | return $this->name; 152 | } 153 | 154 | public function getRedirectUri() 155 | { 156 | return $this->redirect_uri; 157 | } 158 | 159 | public function getResponseType() 160 | { 161 | if (!$this->_responseType instanceof ResponseTypeInterface) { 162 | 163 | if ( 164 | isset($this->token_type) 165 | && $this->token_type === static::TOKEN_TYPE_MAC 166 | ) { 167 | $this->_responseType = new MacTokenResponse(); 168 | } else { 169 | $this->_responseType = new BearerTokenResponse(); 170 | } 171 | 172 | } 173 | 174 | return $this->_responseType; 175 | } 176 | 177 | /** 178 | * @param callable|null $callable 179 | * @return ClientQuery|ActiveQuery 180 | */ 181 | public function getRelatedScopes(callable $callable = null) 182 | { 183 | return $this->hasMany(Scope::class, ['id' => 'scope_id']) 184 | ->viaTable('{{auth__client_scope}}', ['client_id' => 'id'], $callable); 185 | } 186 | 187 | public function getIsConfidential() 188 | { 189 | return $this->secret !== null; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /models/ClientQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([ 25 | 'not', [Client::tableName() . '.`secret`' => null] 26 | ]); 27 | } 28 | 29 | /** 30 | * @param int|string $grantType 31 | * @return ClientQuery|ActiveQuery 32 | */ 33 | public function grant($grantType) 34 | { 35 | if ($grantType === null) { 36 | return $this; 37 | } 38 | 39 | if (!is_numeric($grantType)) { 40 | $grantType = Client::getGrantTypeId($grantType, -999); 41 | } 42 | 43 | return $this->andWhere([ 44 | Client::tableName() . '.`grant_type`' => $grantType 45 | ]); 46 | } 47 | 48 | /** 49 | * @return ClientQuery|ActiveQuery 50 | */ 51 | public function active() 52 | { 53 | return $this 54 | ->joinWith(['relatedScopes']) 55 | ->andWhere([ 56 | Client::tableName() . '.`status`' => Client::STATUS_ACTIVE 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /models/EntityQueryTrait.php: -------------------------------------------------------------------------------- 1 | modelClass; 18 | $tableName = $modelClass::tableName(); 19 | } 20 | 21 | /** @var \yii\db\ActiveQuery $this */ 22 | return $this->andWhere([ 23 | $tableName . '.`identifier`' => $identifier 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/EntityTrait.php: -------------------------------------------------------------------------------- 1 | getAttribute('identifier'); 16 | } 17 | 18 | /** 19 | * @param mixed $identifier 20 | */ 21 | public function setIdentifier($identifier) 22 | { 23 | /** @var ActiveRecordInterface $this */ 24 | $this->setAttribute('identifier', $identifier); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/RefreshToken.php: -------------------------------------------------------------------------------- 1 | AccessToken::class, 'targetAttribute' => 'id'], 60 | ['identifier', 'unique'], 61 | [['created_at', 'updated_at'], 'default', 'value' => time()], 62 | ['status', 'default', 'value' => static::STATUS_ACTIVE], 63 | ['status', 'in', 'range' => [static::STATUS_REVOKED, static::STATUS_ACTIVE]], 64 | ]; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function getAccessToken() 71 | { 72 | return $this->hasOne(AccessToken::class, ['id' => 'access_token_id'])/* todo: ->inverseOf('refreshTokens')*/; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | * 78 | * @param AccessTokenEntityInterface|ActiveRecord $accessToken 79 | */ 80 | public function setAccessToken(AccessTokenEntityInterface $accessToken) 81 | { 82 | if ( 83 | !$this->isRelationPopulated('accessToken') 84 | && $accessToken instanceof AccessToken 85 | ) { 86 | $this->setAttributes(['access_token_id' => $accessToken->getPrimaryKey()]); 87 | $this->populateRelation('accessToken', $accessToken); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /models/RefreshTokenQuery.php: -------------------------------------------------------------------------------- 1 | identifier; 41 | } 42 | 43 | /** 44 | * Specify data which should be serialized to JSON 45 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php 46 | * @return mixed data which can be serialized by json_encode, 47 | * which is a value of any type other than a resource. 48 | * @since 5.4.0 49 | */ 50 | function jsonSerialize() 51 | { 52 | return $this->getIdentifier(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /models/ScopeQuery.php: -------------------------------------------------------------------------------- 1 | modelClass; 17 | return $this->andWhere(['status' => $modelClass::STATUS_ACTIVE]); 18 | } 19 | 20 | /** 21 | * @return ClientQuery|ActiveQuery 22 | */ 23 | public function revoked() 24 | { 25 | /** @var ActiveQuery $this */ 26 | /** @var AccessToken|RefreshToken $modelClass */ 27 | $modelClass = $this->modelClass; 28 | return $this->andWhere(['<>', 'status', $modelClass::STATUS_ACTIVE]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rbac/GrantedRule.php: -------------------------------------------------------------------------------- 1 | createdAt = $this->updatedAt = time(); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function execute($user, $item, $params) 35 | { 36 | $identity = \Yii::$app->user->identity; 37 | 38 | if ( 39 | !$identity instanceof AccessTokenEntityInterface 40 | || !$item instanceof Item 41 | ) { 42 | return false; 43 | } 44 | 45 | return array_search($item->name, array_map( 46 | function (ScopeEntityInterface $scopeEntity) { 47 | return $scopeEntity->getIdentifier(); 48 | }, $identity->getScopes() 49 | )) !== false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rbac/OwnedInterface.php: -------------------------------------------------------------------------------- 1 | user; 18 | 19 | if ($value === null) { 20 | $value = $user->getId(); 21 | } 22 | 23 | /** @var ActiveQuery $this */ 24 | if ($strict === true) { 25 | return $this->andWhere([$attribute => $value]); 26 | } else { 27 | return $this->andFilterWhere([$attribute => $value]); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rbac/OwnedRule.php: -------------------------------------------------------------------------------- 1 | createdAt = $this->updatedAt = time(); 25 | } 26 | 27 | public function execute($user, $item, $params) 28 | { 29 | if (!isset($params['model']) || !isset($user)) { 30 | return false; 31 | } 32 | 33 | return 34 | $params['model'] instanceof OwnedInterface 35 | && $params['model']->getOwnerId() === $user; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rbac/Permission.php: -------------------------------------------------------------------------------- 1 | authManager; 30 | $authManager->add($this); 31 | 32 | $this->prepareChildren($authManager); 33 | } 34 | 35 | /** 36 | * @internal 37 | * @param ManagerInterface $authManager 38 | * @return $this 39 | */ 40 | protected function prepareChildren(ManagerInterface &$authManager) 41 | { 42 | foreach ($this->getChildren() as $permissionName => $permission) { 43 | 44 | if (is_string($permission)) { 45 | $permission = class_exists($permission) 46 | ? ['class' => $permission] 47 | : ['name' => $permission]; 48 | } 49 | 50 | if (is_array($permission)) { 51 | $permission = \Yii::createObject( 52 | ArrayHelper::merge([ 53 | 'class' => self::class, 54 | 'name' => $permissionName, 55 | ], $permission) 56 | ); 57 | } 58 | 59 | $authManager->add($permission); 60 | 61 | if ($authManager->canAddChild($this, $permission)) { 62 | $authManager->removeChild($this, $permission); 63 | $authManager->addChild($this, $permission); 64 | } 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function getChildren() 74 | { 75 | return $this->_children; 76 | } 77 | 78 | /** 79 | * @param array $children 80 | */ 81 | public function setChildren($children) 82 | { 83 | $this->_children = $children; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rbac/ScopePermission.php: -------------------------------------------------------------------------------- 1 | authManager; 21 | 22 | $grantedRule = new GrantedRule(); 23 | $authManager->add($grantedRule); 24 | $this->ruleName = $grantedRule->name; 25 | 26 | parent::init(); 27 | } 28 | } 29 | --------------------------------------------------------------------------------