├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codeception.yml ├── composer.json ├── src ├── AuthorizeFilter.php ├── BaseModel.php ├── Exception.php ├── Module.php ├── OAuth2IdentityInterface.php ├── RedirectException.php ├── TokenAction.php ├── TokenAuth.php ├── console │ └── Oauth2Controller.php ├── granttypes │ ├── Authorization.php │ ├── ClientCredentials.php │ ├── JwtBearer.php │ ├── RefreshToken.php │ └── UserCredentials.php ├── message.php ├── messages │ └── en │ │ └── oauth2.php ├── migrations │ └── m150610_162817_oauth.php ├── models │ ├── AccessToken.php │ ├── AuthorizationCode.php │ ├── Client.php │ └── RefreshToken.php ├── request │ └── AccessTokenExtractor.php └── responsetypes │ ├── Authorization.php │ └── Implicit.php └── tests ├── _bootstrap.php ├── _output └── .gitignore ├── _support ├── UnitTester.php └── _generated │ └── .gitignore ├── unit.suite.yml └── unit └── request └── AccessTokenExtractorTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings/ 2 | /.buildpath 3 | /.project 4 | /.idea/ 5 | /composer.lock 6 | /composer.phar 7 | /vendor/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.6' 4 | - '7.0' 5 | - '7.1' 6 | - '7.2' 7 | 8 | install: 9 | - composer install --no-interaction 10 | 11 | script: 12 | - vendor/bin/phpcs --standard=vendor/yiisoft/yii2-coding-standards/Yii2 --extensions=php src 13 | - php vendor/bin/codecept run --coverage --coverage-xml 14 | 15 | after_success: 16 | - bash <(curl -s https://codecov.io/bash) 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.8.0 2 | ----- 3 | - Enh: Internationalization support 4 | 5 | 1.7.0 6 | ----- 7 | - Bug #37: Fixed `conquer\oauth2\TokenAuth` behavior when multiple Authorization headers are present 8 | - Bug #32: `conquer\oauth2\BaseModel` caches request 9 | - Enh #24: Client Credentials grant type support 10 | - Enh: Added `scopes` setting to `conquer\oauth2\TokenAuth` 11 | - Enh: Added migrations namespace 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrey Borodulin 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii2 OAuth 2.0 Server 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.org/borodulin/yii2-oauth2-server.svg?branch=master)](https://travis-ci.org/borodulin/yii2-oauth2-server) 5 | 6 | ## Description 7 | 8 | This extension provides simple implementation of [Oauth 2.0](http://tools.ietf.org/wg/oauth/draft-ietf-oauth-v2/) specification using Yii2 framework. 9 | 10 | ## Installation 11 | 12 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 13 | 14 | To install, either run 15 | 16 | ``` 17 | $ php composer.phar require conquer/oauth2 "*" 18 | ``` 19 | or add 20 | 21 | ``` 22 | "conquer/oauth2": "*" 23 | ``` 24 | 25 | to the ```require``` section of your `composer.json` file. 26 | 27 | Migrations are available from [migrations](./src/migrations) folder. 28 | 29 | To add migrations to your application, edit the console config file to configure 30 | [a namespaced migration](http://www.yiiframework.com/doc-2.0/guide-db-migrations.html#namespaced-migrations): 31 | 32 | ```php 33 | 'controllerMap' => [ 34 | // ... 35 | 'migrate' => [ 36 | 'class' => 'yii\console\controllers\MigrateController', 37 | 'migrationPath' => null, 38 | 'migrationNamespaces' => [ 39 | // ... 40 | 'conquer\oauth2\migrations', 41 | ], 42 | ], 43 | ], 44 | ``` 45 | 46 | Then issue the `migrate/up` command: 47 | 48 | ```sh 49 | yii migrate/up 50 | ``` 51 | 52 | You also need to specify message translation source for this package: 53 | 54 | ``` 55 | 'components' => [ 56 | 'i18n' => [ 57 | 'translations' => [ 58 | 'conquer/oauth2' => [ 59 | 'class' => \yii\i18n\PhpMessageSource::class, 60 | 'basePath' => '@conquer/oauth2/messages', 61 | ], 62 | ], 63 | ] 64 | ], 65 | ``` 66 | 67 | ## Usage 68 | 69 | OAuth 2.0 Authorization usage 70 | ```php 71 | namespace app\controllers; 72 | 73 | use app\models\LoginForm; 74 | 75 | class AuthController extends \yii\web\Controller 76 | { 77 | public function behaviors() 78 | { 79 | return [ 80 | /** 81 | * Checks oauth2 credentions and try to perform OAuth2 authorization on logged user. 82 | * AuthorizeFilter uses session to store incoming oauth2 request, so 83 | * you can do additional steps, such as third party oauth authorization (Facebook, Google ...) 84 | */ 85 | 'oauth2Auth' => [ 86 | 'class' => \conquer\oauth2\AuthorizeFilter::className(), 87 | 'only' => ['index'], 88 | ], 89 | ]; 90 | } 91 | public function actions() 92 | { 93 | return [ 94 | /** 95 | * Returns an access token. 96 | */ 97 | 'token' => [ 98 | 'class' => \conquer\oauth2\TokenAction::classname(), 99 | ], 100 | /** 101 | * OPTIONAL 102 | * Third party oauth providers also can be used. 103 | */ 104 | 'back' => [ 105 | 'class' => \yii\authclient\AuthAction::className(), 106 | 'successCallback' => [$this, 'successCallback'], 107 | ], 108 | ]; 109 | } 110 | /** 111 | * Display login form, signup or something else. 112 | * AuthClients such as Google also may be used 113 | */ 114 | public function actionIndex() 115 | { 116 | $model = new LoginForm(); 117 | if ($model->load(\Yii::$app->request->post()) && $model->login()) { 118 | if ($this->isOauthRequest) { 119 | $this->finishAuthorization(); 120 | } else { 121 | return $this->goBack(); 122 | } 123 | } else { 124 | return $this->render('index', [ 125 | 'model' => $model, 126 | ]); 127 | } 128 | } 129 | /** 130 | * OPTIONAL 131 | * Third party oauth callback sample 132 | * @param OAuth2 $client 133 | */ 134 | public function successCallback($client) 135 | { 136 | switch ($client::className()) { 137 | case GoogleOAuth::className(): 138 | // Do login with automatic signup 139 | break; 140 | ... 141 | default: 142 | break; 143 | } 144 | /** 145 | * If user is logged on, redirects to oauth client with success, 146 | * or redirects error with Access Denied 147 | */ 148 | if ($this->isOauthRequest) { 149 | $this->finishAuthorization(); 150 | } 151 | } 152 | 153 | } 154 | ``` 155 | Api controller sample 156 | ```php 157 | class ApiController extends \yii\rest\Controller 158 | { 159 | public function behaviors() 160 | { 161 | return [ 162 | /** 163 | * Performs authorization by token 164 | */ 165 | 'tokenAuth' => [ 166 | 'class' => \conquer\oauth2\TokenAuth::className(), 167 | ], 168 | ]; 169 | } 170 | /** 171 | * Returns username and email 172 | */ 173 | public function actionIndex() 174 | { 175 | $user = \Yii::$app->user->identity; 176 | return [ 177 | 'username' => $user->username, 178 | 'email' => $user->email, 179 | ]; 180 | } 181 | } 182 | ``` 183 | Sample client config 184 | ```php 185 | return [ 186 | ... 187 | 'components' => [ 188 | 'authClientCollection' => [ 189 | 'class' => 'yii\authclient\Collection', 190 | 'clients' => [ 191 | 'myserver' => [ 192 | 'class' => 'yii\authclient\OAuth2', 193 | 'clientId' => 'unique client_id', 194 | 'clientSecret' => 'client_secret', 195 | 'tokenUrl' => 'http://myserver.local/auth/token', 196 | 'authUrl' => 'http://myserver.local/auth/index', 197 | 'apiBaseUrl' => 'http://myserver.local/api', 198 | ], 199 | ], 200 | ], 201 | ]; 202 | ``` 203 | 204 | If you want to use Resource Owner Password Credentials Grant, 205 | implement `\conquer\oauth2\OAuth2IdentityInterface`. 206 | 207 | ```php 208 | use conquer\oauth2\OAuth2IdentityInterface; 209 | 210 | class User extends ActiveRecord implements IdentityInterface, OAuth2IdentityInterface 211 | { 212 | ... 213 | 214 | /** 215 | * Finds user by username 216 | * 217 | * @param string $username 218 | * @return static|null 219 | */ 220 | public static function findIdentityByUsername($username) 221 | { 222 | return static::findOne(['username' => $username]); 223 | } 224 | 225 | /** 226 | * Validates password 227 | * 228 | * @param string $password password to validate 229 | * @return bool if password provided is valid for current user 230 | */ 231 | public function validatePassword($password) 232 | { 233 | return Yii::$app->security->validatePassword($password, $this->password_hash); 234 | } 235 | 236 | ... 237 | } 238 | ``` 239 | 240 | ### Warning 241 | 242 | As official documentation says: 243 | 244 | > Since this access token request utilizes the resource owner's 245 | password, the authorization server MUST protect the endpoint against 246 | brute force attacks (e.g., using rate-limitation or generating 247 | alerts). 248 | 249 | It's strongly recommended to rate limits on token endpoint. 250 | Fortunately, Yii2 have instruments to do this. 251 | 252 | For further information see [Yii2 Ratelimiter](http://www.yiiframework.com/doc-2.0/yii-filters-ratelimiter.html) 253 | 254 | ## License 255 | 256 | **conquer/oauth2** is released under the MIT License. See the bundled `LICENSE` for details. 257 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | namespace: tests 2 | actor_suffix: Tester 3 | settings: 4 | shuffle: true 5 | lint: true 6 | bootstrap: _bootstrap.php 7 | colors: true 8 | memory_limit: 1024M 9 | paths: 10 | tests: tests 11 | output: tests/_output 12 | support: tests/_support 13 | data: tests/_data 14 | coverage: 15 | enabled: true 16 | include: 17 | - src/* -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conquer/oauth2", 3 | "description": "The Oauth2 Server extension for the Yii2 framework", 4 | "keywords": ["yii2", "oauth2", "oauth2-server"], 5 | "homepage": "https://github.com/borodulin/yii2-oauth2-server", 6 | "type": "yii2-extension", 7 | "license": "MIT", 8 | "support": { 9 | "issues": "https://github.com/borodulin/yii2-oauth2-server/issues", 10 | "source": "https://github.com/borodulin/yii2-oauth2-server" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Andrey Borodulin", 15 | "email": "borodulin@gmail.com" 16 | } 17 | ], 18 | "minimum-stability": "dev", 19 | "prefer-stable": true, 20 | "require": { 21 | "yiisoft/yii2": "^2.0", 22 | "jakeasmith/http_build_url": ">=1.0.1" 23 | }, 24 | "require-dev": { 25 | "codeception/codeception": "^2.5", 26 | "yiisoft/yii2-coding-standards": "^2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "conquer\\oauth2\\": "src" 31 | } 32 | }, 33 | "config": { 34 | "sort-packages": true 35 | }, 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "1.8.x-dev" 39 | } 40 | }, 41 | "repositories": [ 42 | { 43 | "type": "composer", 44 | "url": "https://asset-packagist.org" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/AuthorizeFilter.php: -------------------------------------------------------------------------------- 1 | 'conquer\oauth2\responsetypes\Implicit', 25 | 'code' => 'conquer\oauth2\responsetypes\Authorization', 26 | ]; 27 | 28 | /** 29 | * 30 | * @var boolean 31 | */ 32 | public $allowImplicit = true; 33 | 34 | public $storeKey = 'ear6kme7or19rnfldtmwsxgzxsrmngqw'; 35 | 36 | public function init() 37 | { 38 | if (!$this->allowImplicit) { 39 | unset($this->responseTypes['token']); 40 | } 41 | } 42 | 43 | /** 44 | * Performs OAuth 2.0 request validation and store granttype object in the session, 45 | * so, user can go from our authorization server to the third party OAuth provider. 46 | * You should call finishAuthorization() in the current controller to finish client authorization 47 | * or to stop with Access Denied error message if the user is not logged on. 48 | * @param \yii\base\Action $action 49 | * @return bool 50 | * @throws Exception 51 | */ 52 | public function beforeAction($action) 53 | { 54 | if (!$responseType = BaseModel::getRequestValue('response_type')) { 55 | throw new Exception(Yii::t('conquer/oauth2', 'Invalid or missing response type.')); 56 | } 57 | if (isset($this->responseTypes[$responseType])) { 58 | $this->_responseType = Yii::createObject($this->responseTypes[$responseType]); 59 | } else { 60 | throw new Exception(Yii::t('conquer/oauth2', 'An unsupported response type was requested.'), Exception::UNSUPPORTED_RESPONSE_TYPE); 61 | } 62 | 63 | $this->_responseType->validate(); 64 | 65 | if ($this->storeKey) { 66 | Yii::$app->session->set($this->storeKey, serialize($this->_responseType)); 67 | } 68 | 69 | return true; 70 | } 71 | 72 | /** 73 | * If user is logged on, do oauth login immediatly, 74 | * continue authorization in the another case 75 | * @param \yii\base\Action $action 76 | * @param mixed $result 77 | * @return mixed|null 78 | */ 79 | public function afterAction($action, $result) 80 | { 81 | if (Yii::$app->user->isGuest) { 82 | return $result; 83 | } else { 84 | $this->finishAuthorization(); 85 | } 86 | return null; 87 | } 88 | 89 | /** 90 | * @throws Exception 91 | * @return \conquer\oauth2\BaseModel 92 | */ 93 | protected function getResponseType() 94 | { 95 | if (empty($this->_responseType) && $this->storeKey) { 96 | if (Yii::$app->session->has($this->storeKey)) { 97 | $this->_responseType = unserialize(Yii::$app->session->get($this->storeKey)); 98 | } else { 99 | throw new Exception(Yii::t('conquer/oauth2', 'Invalid server state or the User Session has expired.'), Exception::SERVER_ERROR); 100 | } 101 | } 102 | return $this->_responseType; 103 | } 104 | 105 | /** 106 | * Finish oauth authorization. 107 | * Builds redirect uri and performs redirect. 108 | * If user is not logged on, redirect contains the Access Denied Error 109 | */ 110 | public function finishAuthorization() 111 | { 112 | /** @var Authorization $responseType */ 113 | $responseType = $this->getResponseType(); 114 | if (Yii::$app->user->isGuest) { 115 | $responseType->errorRedirect(Yii::t('conquer/oauth2', 'The User denied access to your application.'), Exception::ACCESS_DENIED); 116 | } 117 | $parts = $responseType->getResponseData(); 118 | 119 | $redirectUri = http_build_url($responseType->redirect_uri, $parts, HTTP_URL_JOIN_QUERY | HTTP_URL_STRIP_FRAGMENT); 120 | 121 | if (isset($parts['fragment'])) { 122 | $redirectUri .= '#' . $parts['fragment']; 123 | } 124 | 125 | Yii::$app->response->redirect($redirectUri); 126 | } 127 | 128 | /** 129 | * @return boolean 130 | */ 131 | public function getIsOauthRequest() 132 | { 133 | return !empty($this->storeKey) && Yii::$app->session->has($this->storeKey); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/BaseModel.php: -------------------------------------------------------------------------------- 1 | 'PHP_AUTH_USER', 56 | 'client_secret' => 'PHP_AUTH_PW', 57 | ]; 58 | 59 | foreach ($this->safeAttributes() as $attribute) { 60 | $this->$attribute = self::getRequestValue($attribute, ArrayHelper::getValue($headers, $attribute)); 61 | } 62 | } 63 | 64 | public function addError($attribute, $error = '', $type = Exception::INVALID_REQUEST) 65 | { 66 | throw new Exception($error, $type); 67 | } 68 | 69 | public function errorServer($error, $type = Exception::INVALID_REQUEST) 70 | { 71 | throw new Exception($error, $type); 72 | } 73 | 74 | public function errorRedirect($error, $type = Exception::INVALID_REQUEST) 75 | { 76 | $redirectUri = isset($this->redirect_uri) ? $this->redirect_uri : $this->getClient()->redirect_uri; 77 | if ($redirectUri) { 78 | throw new RedirectException($redirectUri, $error, $type, isset($this->state) ? $this->state : null); 79 | } else { 80 | throw new Exception($error, $type); 81 | } 82 | } 83 | 84 | abstract public function getResponseData(); 85 | 86 | public static function getRequestValue($param, $header = null, Request $request = null) 87 | { 88 | if (is_null($request)) { 89 | $request = Yii::$app->request; 90 | } 91 | if ($header && ($result = $request->headers->get($header))) { 92 | return $result; 93 | } else { 94 | return $request->post($param, $request->get($param)); 95 | } 96 | } 97 | 98 | /** 99 | * @return Client 100 | */ 101 | public function getClient() 102 | { 103 | if (is_null($this->_client)) { 104 | if (empty($this->client_id)) { 105 | $this->errorServer(Yii::t('conquer/oauth2', 'Unknown client.'), Exception::INVALID_CLIENT); 106 | } 107 | if (!$this->_client = Client::findOne(['client_id' => $this->client_id])) { 108 | $this->errorServer(Yii::t('conquer/oauth2', 'Unknown client.'), Exception::INVALID_CLIENT); 109 | } 110 | } 111 | return $this->_client; 112 | } 113 | 114 | public function validateClientId() 115 | { 116 | $this->getClient(); 117 | } 118 | 119 | public function validateClientSecret($attribute) 120 | { 121 | if (!Yii::$app->security->compareString($this->getClient()->client_secret, $this->$attribute)) { 122 | $this->addError($attribute, Yii::t('conquer/oauth2', 'The client credentials are invalid.'), Exception::UNAUTHORIZED_CLIENT); 123 | } 124 | } 125 | 126 | public function validateRedirectUri($attribute) 127 | { 128 | if (!empty($this->$attribute)) { 129 | $clientRedirectUri = $this->getClient()->redirect_uri; 130 | if (strncasecmp($this->$attribute, $clientRedirectUri, strlen($clientRedirectUri)) !== 0) { 131 | $this->errorServer(Yii::t('conquer/oauth2', 'The redirect URI provided is missing or does not match.'), Exception::REDIRECT_URI_MISMATCH); 132 | } 133 | } 134 | } 135 | 136 | public function validateScope($attribute) 137 | { 138 | if (!$this->checkSets($this->$attribute, $this->client->scope)) { 139 | $this->errorRedirect(Yii::t('conquer/oauth2', 'The requested scope is invalid, unknown, or malformed.'), Exception::INVALID_SCOPE); 140 | } 141 | } 142 | 143 | /** 144 | * Checks if everything in required set is contained in available set. 145 | * 146 | * @param string|array $requiredSet 147 | * @param string|array $availableSet 148 | * @return boolean 149 | */ 150 | protected function checkSets($requiredSet, $availableSet) 151 | { 152 | if (!is_array($requiredSet)) { 153 | $requiredSet = explode(' ', trim($requiredSet)); 154 | } 155 | if (!is_array($availableSet)) { 156 | $availableSet = explode(' ', trim($availableSet)); 157 | } 158 | return (count(array_diff($requiredSet, $availableSet)) == 0); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | error = $error; 41 | parent::__construct(400, $error_description, $code = 0); 42 | } 43 | 44 | /** 45 | * @return string the user-friendly name of this exception 46 | */ 47 | public function getName() 48 | { 49 | return isset($this->error) ? $this->error : self::SERVER_ERROR; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | controllerMap[$this->id] = [ 27 | 'class' => Oauth2Controller::class, 28 | ]; 29 | } 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function behaviors() 36 | { 37 | if (!empty($this->behaviors)) { 38 | return $this->behaviors; 39 | } else { 40 | return parent::behaviors(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/OAuth2IdentityInterface.php: -------------------------------------------------------------------------------- 1 | $error]; 32 | 33 | if ($error_description) { 34 | $query['error_description'] = $error_description; 35 | } 36 | 37 | if ($state) { 38 | $query['state'] = $state; 39 | } 40 | Yii::$app->response->redirect(http_build_url($redirect_uri, [ 41 | 'query' => http_build_query($query) 42 | ], HTTP_URL_JOIN_QUERY)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/TokenAction.php: -------------------------------------------------------------------------------- 1 | 'conquer\oauth2\granttypes\Authorization', 26 | 'refresh_token' => 'conquer\oauth2\granttypes\RefreshToken', 27 | 'client_credentials' => 'conquer\oauth2\granttypes\ClientCredentials', 28 | // 'password' => 'conquer\oauth2\granttypes\UserCredentials', 29 | // 'urn:ietf:params:oauth:grant-type:jwt-bearer' => 'conquer\oauth2\granttypes\JwtBearer', 30 | ]; 31 | 32 | public function init() 33 | { 34 | Yii::$app->response->format = $this->format; 35 | $this->controller->enableCsrfValidation = false; 36 | } 37 | 38 | public function run() 39 | { 40 | if (!$grantType = BaseModel::getRequestValue('grant_type')) { 41 | throw new Exception(Yii::t('conquer/oauth2', 'The grant type was not specified in the request.')); 42 | } 43 | if (isset($this->grantTypes[$grantType])) { 44 | $grantModel = Yii::createObject($this->grantTypes[$grantType]); 45 | } else { 46 | throw new Exception(Yii::t('conquer/oauth2', 'An unsupported grant type was requested.'), Exception::UNSUPPORTED_GRANT_TYPE); 47 | } 48 | 49 | $grantModel->validate(); 50 | 51 | Yii::$app->response->data = $grantModel->getResponseData(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/TokenAuth.php: -------------------------------------------------------------------------------- 1 | [ 29 | * 'class' => \conquer\oauth2\TokenAuth::className(), 30 | * ], 31 | * ]; 32 | * } 33 | * ``` 34 | * 35 | * @author Andrey Borodulin 36 | */ 37 | class TokenAuth extends AuthMethod 38 | { 39 | /** 40 | * @var AccessToken 41 | */ 42 | private $_accessToken; 43 | 44 | /** 45 | * @var string the HTTP authentication realm 46 | */ 47 | public $realm; 48 | 49 | /** 50 | * @var string the class name of the [[identity]] object. 51 | */ 52 | public $identityClass; 53 | 54 | /** 55 | * @var array scopes that need to be on token. 56 | */ 57 | public $scopes = []; 58 | 59 | /** 60 | * @param \yii\web\User $user 61 | * @param \yii\web\Request $request 62 | * @param \yii\web\Response $response 63 | * @return mixed 64 | * @throws Exception 65 | * @throws UnauthorizedHttpException 66 | */ 67 | public function authenticate($user, $request, $response) 68 | { 69 | $accessToken = $this->getAccessToken(); 70 | 71 | if (!$this->checkScopes($this->scopes, $accessToken->scope)) { 72 | throw new UnauthorizedHttpException(Yii::t('conquer/oauth2', 'The access token does not have required scopes.')); 73 | } 74 | 75 | /** @var IdentityInterface $identityClass */ 76 | $identityClass = is_null($this->identityClass) ? $user->identityClass : $this->identityClass; 77 | 78 | $identity = $identityClass::findIdentity($accessToken->user_id); 79 | 80 | if (empty($identity)) { 81 | throw new Exception(Yii::t('conquer/oauth2', 'User is not found.'), Exception::ACCESS_DENIED); 82 | } 83 | 84 | $user->setIdentity($identity); 85 | 86 | return $identity; 87 | } 88 | 89 | /** 90 | * Checks if everything in required set is contained in available set. 91 | * 92 | * @param string|array $requiredSet 93 | * @param string|array $availableSet 94 | * @return boolean 95 | */ 96 | protected function checkScopes($requiredSet, $availableSet) 97 | { 98 | if (!is_array($requiredSet)) { 99 | $requiredSet = explode(' ', trim($requiredSet)); 100 | } 101 | if (!is_array($availableSet)) { 102 | $availableSet = explode(' ', trim($availableSet)); 103 | } 104 | return (count(array_diff($requiredSet, $availableSet)) == 0); 105 | } 106 | 107 | /** 108 | * @param Response $response 109 | */ 110 | public function challenge($response) 111 | { 112 | /** @var Controller $owner */ 113 | $owner = $this->owner; 114 | $realm = empty($this->realm) ? $owner->getUniqueId() : $this->realm; 115 | $response->getHeaders()->set('WWW-Authenticate', "Bearer realm=\"{$realm}\""); 116 | } 117 | 118 | /** 119 | * @inheritdoc 120 | */ 121 | public function handleFailure($response) 122 | { 123 | throw new Exception(Yii::t('conquer/oauth2', 'You are requesting with an invalid credential.')); 124 | } 125 | 126 | /** 127 | * @return AccessToken 128 | * @throws Exception 129 | * @throws UnauthorizedHttpException 130 | */ 131 | protected function getAccessToken() 132 | { 133 | if (is_null($this->_accessToken)) { 134 | $tokenExtractor = Yii::createObject(AccessTokenExtractor::class); 135 | 136 | if (!$accessToken = AccessToken::findOne(['access_token' => $tokenExtractor->extract()])) { 137 | throw new UnauthorizedHttpException(Yii::t('conquer/oauth2', 'The access token provided is invalid.')); 138 | } 139 | if ($accessToken->expires < time()) { 140 | throw new UnauthorizedHttpException(Yii::t('conquer/oauth2', 'The access token provided has expired.')); 141 | } 142 | $this->_accessToken = $accessToken; 143 | } 144 | return $this->_accessToken; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/console/Oauth2Controller.php: -------------------------------------------------------------------------------- 1 | 80], 67 | [['code'], 'string', 'max' => 40], 68 | [['redirect_uri'], 'url'], 69 | [['client_id'], 'validateClientId'], 70 | [['code'], 'validateCode'], 71 | [['redirect_uri'], 'validateRedirectUri'], 72 | ]; 73 | } 74 | 75 | /** 76 | * @param $attribute 77 | * @throws Exception 78 | * @throws \conquer\oauth2\RedirectException 79 | */ 80 | public function validateRedirectUri($attribute) 81 | { 82 | $authCode = $this->getAuthCode(); 83 | 84 | if ($authCode->redirect_uri && (strcasecmp($this->$attribute, $authCode->redirect_uri) !== 0)) { 85 | $this->errorServer(Yii::t('conquer/oauth2', 'The redirect URI provided does not match.'), Exception::REDIRECT_URI_MISMATCH); 86 | } 87 | parent::validateRedirectUri($attribute); 88 | } 89 | 90 | /** 91 | * @return array 92 | * @throws Exception 93 | * @throws \Exception 94 | * @throws \Throwable 95 | * @throws \conquer\oauth2\RedirectException 96 | * @throws \yii\base\Exception 97 | * @throws \yii\db\StaleObjectException 98 | */ 99 | public function getResponseData() 100 | { 101 | $authCode = $this->getAuthCode(); 102 | 103 | $acessToken = AccessToken::createAccessToken([ 104 | 'client_id' => $this->client_id, 105 | 'user_id' => $authCode->user_id, 106 | 'expires' => $this->accessTokenLifetime + time(), 107 | 'scope' => $authCode->scope, 108 | ]); 109 | 110 | $refreshToken = RefreshToken::createRefreshToken([ 111 | 'client_id' => $this->client_id, 112 | 'user_id' => $authCode->user_id, 113 | 'expires' => $this->refreshTokenLifetime + time(), 114 | 'scope' => $authCode->scope, 115 | ]); 116 | /** 117 | * The client MUST NOT use the authorization code more than once. 118 | * @link https://tools.ietf.org/html/rfc6749#section-4.1.2 119 | */ 120 | $authCode->delete(); 121 | 122 | return [ 123 | 'access_token' => $acessToken->access_token, 124 | 'expires_in' => $this->accessTokenLifetime, 125 | 'token_type' => $this->tokenType, 126 | 'scope' => $this->scope, 127 | 'refresh_token' => $refreshToken->refresh_token, 128 | ]; 129 | } 130 | 131 | /** 132 | * @throws Exception 133 | * @throws \conquer\oauth2\RedirectException 134 | */ 135 | public function validateCode() 136 | { 137 | $this->getAuthCode(); 138 | } 139 | 140 | /** 141 | * @return AuthorizationCode 142 | * @throws Exception 143 | * @throws \conquer\oauth2\RedirectException 144 | */ 145 | public function getAuthCode() 146 | { 147 | if (is_null($this->_authCode)) { 148 | if (empty($this->code)) { 149 | $this->errorRedirect(Yii::t('conquer/oauth2', 'Authorization code is missing.'), Exception::INVALID_REQUEST); 150 | } 151 | if (!$this->_authCode = AuthorizationCode::findOne(['authorization_code' => $this->code])) { 152 | $this->errorRedirect(Yii::t('conquer/oauth2', 'The authorization code is not found or has been expired.'), Exception::INVALID_CLIENT); 153 | } 154 | } 155 | return $this->_authCode; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/granttypes/ClientCredentials.php: -------------------------------------------------------------------------------- 1 | 'client_credentials'], 49 | [['client_id'], 'string', 'max' => 80], 50 | [['client_id'], 'validateClientId'], 51 | [['client_secret'], 'validateClientSecret'], 52 | [['scope'], 'validateScope'], 53 | ]; 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | public function getResponseData() 60 | { 61 | $accessToken = AccessToken::createAccessToken([ 62 | 'client_id' => $this->client_id, 63 | 'expires' => $this->accessTokenLifetime + time(), 64 | 'scope' => $this->scope, 65 | ]); 66 | 67 | return [ 68 | 'access_token' => $accessToken->access_token, 69 | 'expires_in' => $this->accessTokenLifetime, 70 | 'token_type' => $this->tokenType, 71 | 'scope' => $this->scope, 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/granttypes/JwtBearer.php: -------------------------------------------------------------------------------- 1 | 80], 57 | [['refresh_token'], 'string', 'max' => 40], 58 | [['client_id'], 'validateClientId'], 59 | [['client_secret'], 'validateClientSecret'], 60 | [['refresh_token'], 'validateRefreshToken'], 61 | ]; 62 | } 63 | 64 | /** 65 | * @return array 66 | * @throws \Exception 67 | * @throws \Throwable 68 | * @throws \conquer\oauth2\Exception 69 | * @throws \yii\base\Exception 70 | * @throws \yii\db\StaleObjectException 71 | */ 72 | public function getResponseData() 73 | { 74 | $refreshToken = $this->getRefreshToken(); 75 | 76 | $acessToken = AccessToken::createAccessToken([ 77 | 'client_id' => $this->client_id, 78 | 'user_id' => $refreshToken->user_id, 79 | 'expires' => $this->accessTokenLifetime + time(), 80 | 'scope' => $refreshToken->scope, 81 | ]); 82 | 83 | $refreshToken->delete(); 84 | 85 | $refreshToken = \conquer\oauth2\models\RefreshToken::createRefreshToken([ 86 | 'client_id' => $this->client_id, 87 | 'user_id' => $refreshToken->user_id, 88 | 'expires' => $this->refreshTokenLifetime + time(), 89 | 'scope' => $refreshToken->scope, 90 | ]); 91 | 92 | return [ 93 | 'access_token' => $acessToken->access_token, 94 | 'expires_in' => $this->accessTokenLifetime, 95 | 'token_type' => $this->tokenType, 96 | 'scope' => $refreshToken->scope, 97 | 'refresh_token' => $refreshToken->refresh_token, 98 | ]; 99 | } 100 | 101 | /** 102 | * @throws \conquer\oauth2\Exception 103 | */ 104 | public function validateRefreshToken() 105 | { 106 | $this->getRefreshToken(); 107 | } 108 | 109 | /** 110 | * @return \conquer\oauth2\models\RefreshToken 111 | * @throws \conquer\oauth2\Exception 112 | */ 113 | public function getRefreshToken() 114 | { 115 | if (is_null($this->_refreshToken)) { 116 | if (empty($this->refresh_token)) { 117 | $this->errorServer(Yii::t('conquer/oauth2', 'The request is missing "refresh_token" parameter.')); 118 | } 119 | if (!$this->_refreshToken = \conquer\oauth2\models\RefreshToken::findOne(['refresh_token' => $this->refresh_token])) { 120 | $this->errorServer(Yii::t('conquer/oauth2', 'The Refresh Token is invalid.')); 121 | } 122 | } 123 | return $this->_refreshToken; 124 | } 125 | 126 | // phpcs:disable PSR1.Methods.CamelCapsMethodName 127 | /** 128 | * @return array|mixed|string 129 | */ 130 | public function getRefresh_token() 131 | { 132 | return $this->getRequestValue('refresh_token'); 133 | } 134 | // phpcs:enable 135 | } 136 | -------------------------------------------------------------------------------- /src/granttypes/UserCredentials.php: -------------------------------------------------------------------------------- 1 | 'password'], 81 | [['client_id'], 'string', 'max' => 80], 82 | [['client_id'], 'validateClientId'], 83 | [['client_secret'], 'validateClientSecret'], 84 | [['scope'], 'validateScope'], 85 | // password is validated by validatePassword() 86 | ['password', 'validatePassword'], 87 | ]; 88 | } 89 | 90 | /** 91 | * Validates the password. 92 | * This method serves as the inline validation for password. 93 | * @param string $attribute the attribute currently being validated 94 | * @throws \conquer\oauth2\Exception 95 | * @throws \yii\base\InvalidConfigException 96 | */ 97 | public function validatePassword($attribute) 98 | { 99 | if (! $this->hasErrors()) { 100 | /** @var OAuth2IdentityInterface $user */ 101 | $user = $this->getUser(); 102 | if (!$user || !$user->validatePassword($this->password)) { 103 | $this->addError($attribute, Yii::t('conquer/oauth2', 'Invalid username or password.')); 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * @inheritdoc 110 | */ 111 | public function getResponseData() 112 | { 113 | /** @var IdentityInterface $identity */ 114 | $identity = $this->getUser(); 115 | 116 | $accessToken = AccessToken::createAccessToken([ 117 | 'client_id' => $this->client_id, 118 | 'user_id' => $identity->getId(), 119 | 'expires' => $this->accessTokenLifetime + time(), 120 | 'scope' => $this->scope, 121 | ]); 122 | 123 | $refreshToken = RefreshToken::createRefreshToken([ 124 | 'client_id' => $this->client_id, 125 | 'user_id' => $identity->getId(), 126 | 'expires' => $this->refreshTokenLifetime + time(), 127 | 'scope' => $this->scope, 128 | ]); 129 | 130 | return [ 131 | 'access_token' => $accessToken->access_token, 132 | 'expires_in' => $this->accessTokenLifetime, 133 | 'token_type' => $this->tokenType, 134 | 'scope' => $this->scope, 135 | 'refresh_token' => $refreshToken->refresh_token, 136 | ]; 137 | } 138 | 139 | /** 140 | * Finds user by [[username]] 141 | * @return IdentityInterface|null 142 | * @throws \yii\base\InvalidConfigException 143 | * @throws \conquer\oauth2\Exception 144 | */ 145 | protected function getUser() 146 | { 147 | /** @var OAuth2IdentityInterface $identityClass */ 148 | $identityClass = Yii::$app->user->identityClass; 149 | 150 | $identityObject = Yii::createObject($identityClass); 151 | if (! $identityObject instanceof OAuth2IdentityInterface) { 152 | $this->errorServer(Yii::t('conquer/oauth2', 'OAuth2IdentityInterface is not implemented.')); 153 | } 154 | 155 | if ($this->_user === null) { 156 | $this->_user = $identityClass::findIdentityByUsername($this->username); 157 | } 158 | 159 | return $this->_user; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/message.php: -------------------------------------------------------------------------------- 1 | null, 14 | 'interactive' => true, 15 | 'help' => null, 16 | 'sourcePath' => 'src', 17 | 'messagePath' => 'src/messages', 18 | 'languages' => [ 19 | 'en', 20 | ], 21 | 'translator' => 'Yii::t', 22 | 'sort' => false, 23 | 'overwrite' => true, 24 | 'removeUnused' => false, 25 | 'markUnused' => true, 26 | 'except' => [ 27 | '.svn', 28 | '.git', 29 | '.gitignore', 30 | '.gitkeep', 31 | '.hgignore', 32 | '.hgkeep', 33 | '/messages', 34 | '/BaseYii.php', 35 | ], 36 | 'only' => [ 37 | '*.php', 38 | ], 39 | 'format' => 'php', 40 | 'db' => 'db', 41 | 'sourceMessageTable' => '{{%source_message}}', 42 | 'messageTable' => '{{%message}}', 43 | 'catalog' => 'messages', 44 | 'ignoreCategories' => [], 45 | 'phpFileHeader' => '', 46 | 'phpDocBlock' => null, 47 | ]; 48 | -------------------------------------------------------------------------------- /src/messages/en/oauth2.php: -------------------------------------------------------------------------------- 1 | '', 21 | 'An unsupported response type was requested.' => '', 22 | 'Authorization code is missing.' => '', 23 | 'Client secret' => '', 24 | 'Invalid or missing response type.' => '', 25 | 'Invalid server state or the User Session has expired.' => '', 26 | 'Invalid username or password.' => '', 27 | 'OAuth2IdentityInterface is not implemented.' => '', 28 | 'Only one method may be used to authenticate at a time (Auth header, POST or GET).' => '', 29 | 'Redirect URI used for Authorization Grant' => '', 30 | 'Space-delimited list of approved scopes' => '', 31 | 'Space-delimited list of grant types permitted, null = all' => '', 32 | 'The Refresh Token is invalid.' => '', 33 | 'The User denied access to your application.' => '', 34 | 'The access token does not have required scopes.' => '', 35 | 'The access token provided has expired.' => '', 36 | 'The access token provided is invalid.' => '', 37 | 'The access token was not found.' => '', 38 | 'The authorization code is not found or has been expired.' => '', 39 | 'The client credentials are invalid.' => '', 40 | 'The content type for POST requests must be "application/x-www-form-urlencoded".' => '', 41 | 'The grant type was not specified in the request.' => '', 42 | 'The redirect URI provided does not match.' => '', 43 | 'The redirect URI provided is missing or does not match.' => '', 44 | 'The request is missing "refresh_token" parameter.' => '', 45 | 'The requested scope is invalid, unknown, or malformed.' => '', 46 | 'Unable to create access token.' => '', 47 | 'Unable to create authorization code.' => '', 48 | 'Unable to create refresh token.' => '', 49 | 'Unique client identifier' => '', 50 | 'Unknown client.' => '', 51 | 'User is not found.' => '', 52 | 'When putting the token in the body, the method must be POST.' => '', 53 | 'You are requesting with an invalid credential.' => '', 54 | ]; 55 | -------------------------------------------------------------------------------- /src/migrations/m150610_162817_oauth.php: -------------------------------------------------------------------------------- 1 | createTable('{{%oauth2_client}}', [ 24 | 'client_id' => Schema::TYPE_STRING . '(80) NOT NULL', 25 | 'client_secret' => Schema::TYPE_STRING . '(80) NOT NULL', 26 | 'redirect_uri' => Schema::TYPE_TEXT . ' NOT NULL', 27 | 'grant_type' => Schema::TYPE_TEXT, 28 | 'scope' => Schema::TYPE_TEXT, 29 | 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', 30 | 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', 31 | 'created_by' => Schema::TYPE_INTEGER . ' NOT NULL', 32 | 'updated_by' => Schema::TYPE_INTEGER . ' NOT NULL', 33 | 'PRIMARY KEY (client_id)', 34 | ]); 35 | 36 | $this->createTable('{{%oauth2_access_token}}', [ 37 | 'access_token' => Schema::TYPE_STRING . '(40) NOT NULL', 38 | 'client_id' => Schema::TYPE_STRING . '(80) NOT NULL', 39 | 'user_id' => Schema::TYPE_INTEGER, 40 | 'expires' => Schema::TYPE_INTEGER . ' NOT NULL', 41 | 'scope' => Schema::TYPE_TEXT, 42 | 'PRIMARY KEY (access_token)', 43 | ]); 44 | 45 | $this->createTable('{{%oauth2_refresh_token}}', [ 46 | 'refresh_token' => Schema::TYPE_STRING . '(40) NOT NULL', 47 | 'client_id' => Schema::TYPE_STRING . '(80) NOT NULL', 48 | 'user_id' => Schema::TYPE_INTEGER, 49 | 'expires' => Schema::TYPE_INTEGER . ' NOT NULL', 50 | 'scope' => Schema::TYPE_TEXT, 51 | 'PRIMARY KEY (refresh_token)', 52 | ]); 53 | 54 | $this->createTable('{{%oauth2_authorization_code}}', [ 55 | 'authorization_code' => Schema::TYPE_STRING . '(40) NOT NULL', 56 | 'client_id' => Schema::TYPE_STRING . '(80) NOT NULL', 57 | 'user_id' => Schema::TYPE_INTEGER, 58 | 'redirect_uri' => Schema::TYPE_TEXT . ' NOT NULL', 59 | 'expires' => Schema::TYPE_INTEGER . ' NOT NULL', 60 | 'scope' => Schema::TYPE_TEXT, 61 | 'PRIMARY KEY (authorization_code)', 62 | ]); 63 | 64 | $this->addforeignkey('fk_refresh_token_oauth2_client_client_id', '{{%oauth2_refresh_token}}', 'client_id', '{{%oauth2_client}}', 'client_id', 'cascade', 'cascade'); 65 | $this->addforeignkey('fk_authorization_code_oauth2_client_client_id', '{{%oauth2_authorization_code}}', 'client_id', '{{%oauth2_client}}', 'client_id', 'cascade', 'cascade'); 66 | $this->addforeignkey('fk_access_token_oauth2_client_client_id', '{{%oauth2_access_token}}', 'client_id', '{{%oauth2_client}}', 'client_id', 'cascade', 'cascade'); 67 | 68 | $this->createIndex('ix_authorization_code_expires', '{{%oauth2_authorization_code}}', 'expires'); 69 | $this->createIndex('ix_refresh_token_expires', '{{%oauth2_refresh_token}}', 'expires'); 70 | $this->createIndex('ix_access_token_expires', '{{%oauth2_access_token}}', 'expires'); 71 | } 72 | 73 | public function safeDown() 74 | { 75 | $this->dropTable('{{%oauth2_authorization_code}}'); 76 | $this->dropTable('{{%oauth2_refresh_token}}'); 77 | $this->dropTable('{{%oauth2_access_token}}'); 78 | $this->dropTable('{{%oauth2_client}}'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/models/AccessToken.php: -------------------------------------------------------------------------------- 1 | 40], 47 | [['client_id'], 'string', 'max' => 80] 48 | ]; 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function attributeLabels() 55 | { 56 | return [ 57 | 'access_token' => 'Access Token', 58 | 'client_id' => 'Client ID', 59 | 'user_id' => 'User ID', 60 | 'expires' => 'Expires', 61 | 'scope' => 'Scopes', 62 | ]; 63 | } 64 | 65 | /** 66 | * @param array $attributes 67 | * @return static 68 | * @throws Exception 69 | * @throws \yii\base\Exception 70 | */ 71 | public static function createAccessToken(array $attributes) 72 | { 73 | static::deleteAll(['<', 'expires', time()]); 74 | 75 | $attributes['access_token'] = Yii::$app->security->generateRandomString(40); 76 | $accessToken = new static($attributes); 77 | 78 | if ($accessToken->save()) { 79 | return $accessToken; 80 | } else { 81 | Yii::error(__CLASS__ . ' validation error:' . VarDumper::dumpAsString($accessToken->errors)); 82 | } 83 | throw new Exception(Yii::t('conquer/oauth2', 'Unable to create access token.'), Exception::SERVER_ERROR); 84 | } 85 | 86 | /** 87 | * @return \yii\db\ActiveQuery 88 | */ 89 | public function getClient() 90 | { 91 | return $this->hasOne(Client::class, ['client_id' => 'client_id']); 92 | } 93 | 94 | /** 95 | * @return \yii\db\ActiveQuery 96 | */ 97 | public function getUser() 98 | { 99 | $identity = isset(Yii::$app->user->identity) ? Yii::$app->user->identity : null; 100 | if ($identity instanceof ActiveRecord) { 101 | return $this->hasOne(get_class($identity), ['user_id' => $identity->primaryKey()]); 102 | } 103 | return null; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/models/AuthorizationCode.php: -------------------------------------------------------------------------------- 1 | 40], 48 | [['client_id'], 'string', 'max' => 80], 49 | [['redirect_uri'], 'url'], 50 | ]; 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function attributeLabels() 57 | { 58 | return [ 59 | 'authorization_code' => 'Authorization Code', 60 | 'client_id' => 'Client ID', 61 | 'user_id' => 'User ID', 62 | 'redirect_uri' => 'Redirect Uri', 63 | 'expires' => 'Expires', 64 | 'scope' => 'Scopes', 65 | ]; 66 | } 67 | 68 | /** 69 | * 70 | * @param array $params 71 | * @throws Exception 72 | * @return AuthorizationCode 73 | * @throws \yii\base\Exception 74 | */ 75 | public static function createAuthorizationCode(array $params) 76 | { 77 | static::deleteAll(['<', 'expires', time()]); 78 | 79 | $params['authorization_code'] = Yii::$app->security->generateRandomString(40); 80 | $authCode = new static($params); 81 | 82 | if ($authCode->save()) { 83 | return $authCode; 84 | } else { 85 | Yii::error(__CLASS__ . ' validation error: ' . VarDumper::dumpAsString($authCode->errors)); 86 | } 87 | throw new Exception(Yii::t('conquer/oauth2', 'Unable to create authorization code.'), Exception::SERVER_ERROR); 88 | } 89 | 90 | /** 91 | * @return \yii\db\ActiveQuery 92 | */ 93 | public function getClient() 94 | { 95 | return $this->hasOne(Client::class, ['client_id' => 'client_id']); 96 | } 97 | 98 | /** 99 | * @return \yii\db\ActiveQuery 100 | */ 101 | public function getUser() 102 | { 103 | $identity = isset(Yii::$app->user->identity) ? Yii::$app->user->identity : null; 104 | if ($identity instanceof ActiveRecord) { 105 | return $this->hasOne(get_class($identity), ['user_id' => $identity->primaryKey()]); 106 | } 107 | return null; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/models/Client.php: -------------------------------------------------------------------------------- 1 | 80], 48 | [['redirect_uri'], 'string', 'max' => 2000] 49 | ]; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function behaviors() 56 | { 57 | return [ 58 | TimestampBehavior::class, 59 | BlameableBehavior::class, 60 | ]; 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function attributeLabels() 67 | { 68 | return [ 69 | 'client_id' => Yii::t('conquer/oauth2', 'Unique client identifier'), 70 | 'client_secret' => Yii::t('conquer/oauth2', 'Client secret'), 71 | 'redirect_uri' => Yii::t('conquer/oauth2', 'Redirect URI used for Authorization Grant'), 72 | 'grant_type' => Yii::t('conquer/oauth2', 'Space-delimited list of grant types permitted, null = all'), 73 | 'scope' => Yii::t('conquer/oauth2', 'Space-delimited list of approved scopes'), 74 | ]; 75 | } 76 | 77 | /** 78 | * @return \yii\db\ActiveQuery 79 | */ 80 | public function getAccessTokens() 81 | { 82 | return $this->hasMany(AccessToken::class, ['client_id' => 'client_id']); 83 | } 84 | 85 | /** 86 | * @return \yii\db\ActiveQuery 87 | */ 88 | public function getAuthorizationCodes() 89 | { 90 | return $this->hasMany(AuthorizationCode::class, ['client_id' => 'client_id']); 91 | } 92 | 93 | /** 94 | * @return \yii\db\ActiveQuery 95 | */ 96 | public function getRefreshTokens() 97 | { 98 | return $this->hasMany(RefreshToken::class, ['client_id' => 'client_id']); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/models/RefreshToken.php: -------------------------------------------------------------------------------- 1 | 40], 46 | [['client_id'], 'string', 'max' => 80] 47 | ]; 48 | } 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public function attributeLabels() 54 | { 55 | return [ 56 | 'refresh_token' => 'Refresh Token', 57 | 'client_id' => 'Client ID', 58 | 'user_id' => 'User ID', 59 | 'expires' => 'Expires', 60 | 'scope' => 'Scope', 61 | ]; 62 | } 63 | 64 | /** 65 | * 66 | * @param array $attributes 67 | * @throws Exception 68 | * @return \conquer\oauth2\models\RefreshToken 69 | * @throws \yii\base\Exception 70 | */ 71 | public static function createRefreshToken(array $attributes) 72 | { 73 | static::deleteAll(['<', 'expires', time()]); 74 | 75 | $attributes['refresh_token'] = Yii::$app->security->generateRandomString(40); 76 | $refreshToken = new static($attributes); 77 | 78 | if ($refreshToken->save()) { 79 | return $refreshToken; 80 | } else { 81 | Yii::error(__CLASS__ . ' validation error:' . VarDumper::dumpAsString($refreshToken->errors)); 82 | } 83 | throw new Exception(Yii::t('conquer/oauth2', 'Unable to create refresh token.'), Exception::SERVER_ERROR); 84 | } 85 | 86 | /** 87 | * @return \yii\db\ActiveQuery 88 | */ 89 | public function getClient() 90 | { 91 | return $this->hasOne(Client::class, ['client_id' => 'client_id']); 92 | } 93 | 94 | /** 95 | * @return \yii\db\ActiveQuery 96 | */ 97 | public function getUser() 98 | { 99 | $identity = isset(Yii::$app->user->identity) ? Yii::$app->user->identity : null; 100 | if ($identity instanceof ActiveRecord) { 101 | return $this->hasOne(get_class($identity), ['user_id' => $identity->primaryKey()]); 102 | } 103 | return null; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/request/AccessTokenExtractor.php: -------------------------------------------------------------------------------- 1 | _request = $request; 27 | } 28 | 29 | /** 30 | * Extracts access_token from web request 31 | * @return string 32 | * @throws Exception 33 | */ 34 | public function extract() 35 | { 36 | $headerToken = null; 37 | foreach ($this->_request->getHeaders()->get('Authorization', [], false) as $authHeader) { 38 | if (preg_match('/^Bearer\\s+(.*?)$/', $authHeader, $matches)) { 39 | $headerToken = $matches[1]; 40 | break; 41 | } 42 | } 43 | $postToken = $this->_request->post('access_token'); 44 | $getToken = $this->_request->get('access_token'); 45 | 46 | // Check that exactly one method was used 47 | $methodsCount = isset($headerToken) + isset($postToken) + isset($getToken); 48 | if ($methodsCount > 1) { 49 | throw new Exception(Yii::t('conquer/oauth2', 'Only one method may be used to authenticate at a time (Auth header, POST or GET).')); 50 | } elseif ($methodsCount === 0) { 51 | throw new Exception(Yii::t('conquer/oauth2', 'The access token was not found.')); 52 | } 53 | 54 | // HEADER: Get the access token from the header 55 | if ($headerToken) { 56 | return $headerToken; 57 | } 58 | 59 | // POST: Get the token from POST data 60 | if ($postToken) { 61 | if (!$this->_request->isPost) { 62 | throw new Exception(Yii::t('conquer/oauth2', 'When putting the token in the body, the method must be POST.')); 63 | } 64 | // IETF specifies content-type. NB: Not all webservers populate this _SERVER variable 65 | if (strpos($this->_request->contentType, 'application/x-www-form-urlencoded') !== 0) { 66 | throw new Exception(Yii::t('conquer/oauth2', 'The content type for POST requests must be "application/x-www-form-urlencoded".')); 67 | } 68 | return $postToken; 69 | } 70 | 71 | return $getToken; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/responsetypes/Authorization.php: -------------------------------------------------------------------------------- 1 | 'code'], 57 | [['client_id'], 'string', 'max' => 80], 58 | [['state'], 'string', 'max' => 255], 59 | [['redirect_uri'], 'url'], 60 | [['client_id'], 'validateClientId'], 61 | [['redirect_uri'], 'validateRedirectUri'], 62 | [['scope'], 'validateScope'], 63 | ]; 64 | } 65 | 66 | /** 67 | * @return array 68 | * @throws \conquer\oauth2\Exception 69 | * @throws \yii\base\Exception 70 | */ 71 | public function getResponseData() 72 | { 73 | $authCode = AuthorizationCode::createAuthorizationCode([ 74 | 'client_id' => $this->client_id, 75 | 'user_id' => \Yii::$app->user->id, 76 | 'expires' => $this->authCodeLifetime + time(), 77 | 'scope' => $this->scope, 78 | 'redirect_uri' => $this->redirect_uri 79 | ]); 80 | 81 | $query = [ 82 | 'code' => $authCode->authorization_code, 83 | ]; 84 | 85 | if (isset($this->state)) { 86 | $query['state'] = $this->state; 87 | } 88 | 89 | return [ 90 | 'query' => http_build_query($query), 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/responsetypes/Implicit.php: -------------------------------------------------------------------------------- 1 | 'token'], 68 | [['client_id'], 'string', 'max' => 80], 69 | [['state'], 'string', 'max' => 255], 70 | [['redirect_uri'], 'url'], 71 | [['client_id'], 'validateClientId'], 72 | [['redirect_uri'], 'validateRedirectUri'], 73 | [['scope'], 'validateScope'], 74 | 75 | ]; 76 | } 77 | 78 | /** 79 | * @return array 80 | * @throws \conquer\oauth2\Exception 81 | * @throws \yii\base\Exception 82 | */ 83 | public function getResponseData() 84 | { 85 | $accessToken = AccessToken::createAccessToken([ 86 | 'client_id' => $this->client_id, 87 | 'user_id' => \Yii::$app->user->id, 88 | 'expires' => $this->accessTokenLifetime + time(), 89 | 'scope' => $this->scope, 90 | ]); 91 | 92 | $fragment = [ 93 | 'access_token' => $accessToken->access_token, 94 | 'expires_in' => $this->accessTokenLifetime, 95 | 'token_type' => $this->tokenType, 96 | 'scope' => $this->scope, 97 | ]; 98 | 99 | if (!empty($this->state)) { 100 | $fragment['state'] = $this->state; 101 | } 102 | return [ 103 | 'fragment' => http_build_query($fragment), 104 | ]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | expectException(Exception::class); 18 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 19 | 'getHeaders' => new HeaderCollection, 20 | ])); 21 | $extractor->extract(); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function exceptionWhenMultipleAccessTokensArePresent() 28 | { 29 | $this->expectException(Exception::class); 30 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 31 | 'getHeaders' => (new HeaderCollection)->add('Authorization', 'Bearer 123'), 32 | 'getQueryParams' => ['access_token' => '321'], 33 | ])); 34 | $extractor->extract(); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function exceptionWhenAccessTokenInBodyOfNonPostRequest() 41 | { 42 | $this->expectException(Exception::class); 43 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 44 | 'getBodyParams' => ['access_token' => '321'], 45 | 'getMethod' => 'PATCH', 46 | ])); 47 | $extractor->extract(); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function exceptionWhenWrongContentTypeInRequest() 54 | { 55 | $this->expectException(Exception::class); 56 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 57 | 'getBodyParams' => ['access_token' => '321'], 58 | 'getMethod' => 'POST', 59 | 'getContentType' => 'application/json', 60 | ])); 61 | $extractor->extract(); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function postAccessTokenIsReturned() 68 | { 69 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 70 | 'getBodyParams' => ['access_token' => '321'], 71 | 'getMethod' => 'POST', 72 | 'getContentType' => 'application/x-www-form-urlencoded', 73 | 74 | ])); 75 | $this->assertEquals('321', $extractor->extract()); 76 | } 77 | 78 | /** 79 | * @test 80 | */ 81 | public function getAccessTokenIsReturned() 82 | { 83 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 84 | 'getQueryParams' => ['access_token' => '321'], 85 | 'getMethod' => 'GET', 86 | ])); 87 | $this->assertEquals('321', $extractor->extract()); 88 | } 89 | 90 | /** 91 | * @test 92 | */ 93 | public function headerAccessTokenIsReturned() 94 | { 95 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 96 | 'getHeaders' => (new HeaderCollection)->add('Authorization', 'Bearer 321'), 97 | 'getMethod' => 'GET', 98 | ])); 99 | $this->assertEquals('321', $extractor->extract()); 100 | } 101 | 102 | /** 103 | * @test 104 | */ 105 | public function headerAccessTokenIsReturnedWithOtherAuthHeaders() 106 | { 107 | $extractor = new AccessTokenExtractor($this->make(Request::class, [ 108 | 'getHeaders' => (new HeaderCollection) 109 | ->add('Authorization', 'Basic 111') 110 | ->add('Authorization', 'Bearer 222') 111 | ->add('Custom', 'Header') 112 | ->add('Authorization', 'Advanced 333'), 113 | 'getMethod' => 'GET', 114 | ])); 115 | $this->assertEquals('222', $extractor->extract()); 116 | } 117 | } --------------------------------------------------------------------------------