1 | <?php |
||
2 | |||
3 | /** |
||
4 | * @link http://www.yiiframework.com/ |
||
5 | * @copyright Copyright (c) 2008 Yii Software LLC |
||
6 | * @license http://www.yiiframework.com/license/ |
||
7 | */ |
||
8 | |||
9 | namespace rhertogh\Yii2Oauth2Server; |
||
10 | |||
11 | // phpcs:disable Generic.Files.LineLength.TooLong |
||
12 | use Defuse\Crypto\Exception\BadFormatException; |
||
13 | use Defuse\Crypto\Exception\EnvironmentIsBrokenException; |
||
14 | use GuzzleHttp\Psr7\Response as Psr7Response; |
||
15 | use GuzzleHttp\Psr7\ServerRequest as Psr7ServerRequest; |
||
16 | use Lcobucci\JWT\Configuration; |
||
17 | use Lcobucci\JWT\Signer\Key\InMemory; |
||
18 | use Lcobucci\JWT\Signer\Rsa\Sha256; |
||
19 | use Lcobucci\JWT\Token; |
||
20 | use Lcobucci\JWT\Validation\Constraint\SignedWith; |
||
21 | use League\OAuth2\Server\CryptKey; |
||
22 | use League\OAuth2\Server\Grant\GrantTypeInterface; |
||
23 | use rhertogh\Yii2Oauth2Server\base\Oauth2BaseModule; |
||
24 | use rhertogh\Yii2Oauth2Server\components\server\tokens\Oauth2AccessTokenData; |
||
25 | use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2ClientController; |
||
26 | use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2DebugController; |
||
27 | use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2EncryptionController; |
||
28 | use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2MigrationsController; |
||
29 | use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2PersonalAccessTokenController; |
||
30 | use rhertogh\Yii2Oauth2Server\exceptions\Oauth2ServerException; |
||
31 | use rhertogh\Yii2Oauth2Server\helpers\DiHelper; |
||
32 | use rhertogh\Yii2Oauth2Server\helpers\Psr7Helper; |
||
33 | use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\base\Oauth2BaseAuthorizationRequestInterface; |
||
34 | use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\client\Oauth2ClientAuthorizationRequestInterface; |
||
35 | use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\EndSession\Oauth2EndSessionAuthorizationRequestInterface; |
||
36 | use rhertogh\Yii2Oauth2Server\interfaces\components\common\DefaultAccessTokenTtlInterface; |
||
37 | use rhertogh\Yii2Oauth2Server\interfaces\components\encryption\Oauth2CryptographerInterface; |
||
38 | use rhertogh\Yii2Oauth2Server\interfaces\components\factories\encryption\Oauth2EncryptionKeyFactoryInterface; |
||
39 | use rhertogh\Yii2Oauth2Server\interfaces\components\factories\grants\base\Oauth2GrantTypeFactoryInterface; |
||
40 | use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\request\Oauth2OidcAuthenticationRequestInterface; |
||
41 | use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\scope\Oauth2OidcScopeCollectionInterface; |
||
42 | use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\server\responses\Oauth2OidcBearerTokenResponseInterface; |
||
43 | use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2AuthorizationServerInterface; |
||
44 | use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2ResourceServerInterface; |
||
45 | use rhertogh\Yii2Oauth2Server\interfaces\components\server\responses\Oauth2BearerTokenResponseInterface; |
||
46 | use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2CertificatesControllerInterface; |
||
47 | use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ConsentControllerInterface; |
||
48 | use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2OidcControllerInterface; |
||
49 | use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ServerControllerInterface; |
||
50 | use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2WellKnownControllerInterface; |
||
51 | use rhertogh\Yii2Oauth2Server\interfaces\filters\auth\Oauth2HttpBearerAuthInterface; |
||
52 | use rhertogh\Yii2Oauth2Server\interfaces\models\base\Oauth2EncryptedStorageInterface; |
||
53 | use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2OidcUserInterface; |
||
54 | use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2UserInterface; |
||
55 | use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface; |
||
56 | use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ScopeInterface; |
||
57 | use rhertogh\Yii2Oauth2Server\traits\DefaultAccessTokenTtlTrait; |
||
58 | use Yii; |
||
59 | use yii\base\BootstrapInterface; |
||
60 | use yii\base\InvalidArgumentException; |
||
61 | use yii\base\InvalidCallException; |
||
62 | use yii\base\InvalidConfigException; |
||
63 | use yii\console\Application as ConsoleApplication; |
||
64 | use yii\helpers\ArrayHelper; |
||
65 | use yii\helpers\Json; |
||
66 | use yii\helpers\StringHelper; |
||
67 | use yii\helpers\VarDumper; |
||
68 | use yii\i18n\PhpMessageSource; |
||
69 | use yii\log\Logger; |
||
70 | use yii\validators\IpValidator; |
||
71 | use yii\web\Application as WebApplication; |
||
72 | use yii\web\GroupUrlRule; |
||
73 | use yii\web\IdentityInterface; |
||
74 | use yii\web\Response; |
||
75 | use yii\web\UrlRule; |
||
76 | |||
77 | // phpcs:enable Generic.Files.LineLength.TooLong |
||
78 | |||
79 | /** |
||
80 | * This is the main module class for the Yii2 Oauth2 Server module. |
||
81 | * To use it, include it as a module in the application configuration like the following: |
||
82 | * |
||
83 | * ~~~ |
||
84 | * return [ |
||
85 | * 'bootstrap' => ['oauth2'], |
||
86 | * 'modules' => [ |
||
87 | * 'oauth2' => [ |
||
88 | * 'class' => 'rhertogh\Yii2Oauth2Server\Oauth2Module', |
||
89 | * // ... Please check docs/guide/start-installation.md further details |
||
90 | * ], |
||
91 | * ], |
||
92 | * ] |
||
93 | * ~~~ |
||
94 | * |
||
95 | * @property \DateInterval|string|null $defaultAccessTokenTTL |
||
96 | * @since 1.0.0 |
||
97 | */ |
||
98 | class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface, DefaultAccessTokenTtlInterface |
||
99 | { |
||
100 | use DefaultAccessTokenTtlTrait; |
||
101 | |||
102 | /** |
||
103 | * Application type "web": http response. |
||
104 | * @since 1.0.0 |
||
105 | */ |
||
106 | public const APPLICATION_TYPE_WEB = 'web'; |
||
107 | /** |
||
108 | * Application type "console": cli response. |
||
109 | * @since 1.0.0 |
||
110 | */ |
||
111 | public const APPLICATION_TYPE_CONSOLE = 'console'; |
||
112 | /** |
||
113 | * Supported Application types. |
||
114 | * @since 1.0.0 |
||
115 | */ |
||
116 | public const APPLICATION_TYPES = [ |
||
117 | self::APPLICATION_TYPE_WEB, |
||
118 | self::APPLICATION_TYPE_CONSOLE, |
||
119 | ]; |
||
120 | |||
121 | /** |
||
122 | * "Authorization Server" Role, please see guide for details. |
||
123 | * @since 1.0.0 |
||
124 | */ |
||
125 | public const SERVER_ROLE_AUTHORIZATION_SERVER = 1; |
||
126 | /** |
||
127 | * "Resource Server" Role, please see guide for details. |
||
128 | * @since 1.0.0 |
||
129 | */ |
||
130 | public const SERVER_ROLE_RESOURCE_SERVER = 2; |
||
131 | |||
132 | /** |
||
133 | * Required settings when the server role includes Authorization Server |
||
134 | * @since 1.0.0 |
||
135 | */ |
||
136 | protected const REQUIRED_SETTINGS_AUTHORIZATION_SERVER = [ |
||
137 | 'codesEncryptionKey', |
||
138 | 'storageEncryptionKeys', |
||
139 | 'defaultStorageEncryptionKey', |
||
140 | 'privateKey', |
||
141 | 'publicKey', |
||
142 | ]; |
||
143 | |||
144 | /** |
||
145 | * Encrypted Models |
||
146 | * |
||
147 | * @since 1.0.0 |
||
148 | */ |
||
149 | protected const ENCRYPTED_MODELS = [ |
||
150 | Oauth2ClientInterface::class, |
||
151 | ]; |
||
152 | |||
153 | /** |
||
154 | * Required settings when the server role includes Resource Server |
||
155 | * @since 1.0.0 |
||
156 | */ |
||
157 | protected const REQUIRED_SETTINGS_RESOURCE_SERVER = [ |
||
158 | 'publicKey', |
||
159 | ]; |
||
160 | |||
161 | /** |
||
162 | * Prefix used in session storage of Client Authorization Requests |
||
163 | * @since 1.0.0 |
||
164 | */ |
||
165 | protected const CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OAUTH2_CLIENT_AUTHORIZATION_REQUEST_'; |
||
166 | |||
167 | /** |
||
168 | * Prefix used in session storage of End Session Authorization Requests. |
||
169 | * |
||
170 | * @since 1.0.0 |
||
171 | */ |
||
172 | protected const END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OAUTH2_END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX_'; // phpcs:ignore Generic.Files.LineLength.TooLong |
||
173 | |||
174 | /** |
||
175 | * Controller mapping for the module. Will be parsed on `init()`. |
||
176 | * @since 1.0.0 |
||
177 | */ |
||
178 | protected const CONTROLLER_MAP = [ |
||
179 | self::APPLICATION_TYPE_WEB => [ |
||
180 | Oauth2ServerControllerInterface::CONTROLLER_NAME => [ |
||
181 | 'controller' => Oauth2ServerControllerInterface::class, |
||
182 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
183 | ], |
||
184 | Oauth2ConsentControllerInterface::CONTROLLER_NAME => [ |
||
185 | 'controller' => Oauth2ConsentControllerInterface::class, |
||
186 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
187 | ], |
||
188 | Oauth2WellKnownControllerInterface::CONTROLLER_NAME => [ |
||
189 | 'controller' => Oauth2WellKnownControllerInterface::class, |
||
190 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
191 | ], |
||
192 | Oauth2CertificatesControllerInterface::CONTROLLER_NAME => [ |
||
193 | 'controller' => Oauth2CertificatesControllerInterface::class, |
||
194 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
195 | ], |
||
196 | Oauth2OidcControllerInterface::CONTROLLER_NAME => [ |
||
197 | 'controller' => Oauth2OidcControllerInterface::class, |
||
198 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
199 | ], |
||
200 | ], |
||
201 | self::APPLICATION_TYPE_CONSOLE => [ |
||
202 | 'migrations' => [ |
||
203 | 'controller' => Oauth2MigrationsController::class, |
||
204 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER, |
||
205 | ], |
||
206 | 'client' => [ |
||
207 | 'controller' => Oauth2ClientController::class, |
||
208 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
209 | ], |
||
210 | 'encryption' => [ |
||
211 | 'controller' => Oauth2EncryptionController::class, |
||
212 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
213 | ], |
||
214 | 'debug' => [ |
||
215 | 'controller' => Oauth2DebugController::class, |
||
216 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER, |
||
217 | ], |
||
218 | 'pat' => [ |
||
219 | 'controller' => Oauth2PersonalAccessTokenController::class, |
||
220 | 'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER, |
||
221 | ] |
||
222 | ] |
||
223 | ]; |
||
224 | |||
225 | /** |
||
226 | * Offset of Bearer: in Authorization header |
||
227 | */ |
||
228 | protected const BEARER_TOKEN_OFFSET = 7; |
||
229 | |||
230 | /** |
||
231 | * @inheritdoc |
||
232 | */ |
||
233 | public $controllerNamespace = __NAMESPACE__ . '\-'; // Set explicitly via $controllerMap in `init()`. |
||
234 | |||
235 | /** |
||
236 | * @var string|null The application type. If `null` the type will be automatically detected. |
||
237 | * @see APPLICATION_TYPES |
||
238 | */ |
||
239 | public $appType = null; |
||
240 | |||
241 | /** |
||
242 | * @var int The Oauth 2.0 Server Roles the module will perform. |
||
243 | * @since 1.0.0 |
||
244 | */ |
||
245 | public $serverRole = self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER; |
||
246 | |||
247 | /** |
||
248 | * @var string|null The private key for the server. Can be a string containing the key itself or point to a file. |
||
249 | * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with |
||
250 | * '@' to use a Yii path alias. |
||
251 | * @see $privateKeyPassphrase For setting a passphrase for the private key. |
||
252 | * @since 1.0.0 |
||
253 | */ |
||
254 | public $privateKey = null; |
||
255 | |||
256 | /** |
||
257 | * @var string|null The passphrase for the private key. |
||
258 | * @since 1.0.0 |
||
259 | */ |
||
260 | public $privateKeyPassphrase = null; |
||
261 | /** |
||
262 | * @var string|null The public key for the server. Can be a string containing the key itself or point to a file. |
||
263 | * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with |
||
264 | * '@' to use a Yii path alias. |
||
265 | * @since 1.0.0 |
||
266 | */ |
||
267 | public $publicKey = null; |
||
268 | |||
269 | /** |
||
270 | * @var string|null The encryption key for authorization and refresh codes. |
||
271 | * @since 1.0.0 |
||
272 | */ |
||
273 | public $codesEncryptionKey = null; |
||
274 | |||
275 | /** |
||
276 | * @var string[]|string|null The encryption keys for storage like client secrets. |
||
277 | * Where the array key is the name of the key, and the value the key itself. E.g. |
||
278 | * `['2022-01-01' => 'def00000cb36fd6ed6641e0ad70805b28d....']` |
||
279 | * If a string (instead of an array of strings) is specified it will be JSON decoded |
||
280 | * it should contain an object where each property name is the name of the key, its value the key itself. E.g. |
||
281 | * `{"2022-01-01": "def00000cb36fd6ed6641e0ad70805b28d...."}` |
||
282 | * |
||
283 | * @since 1.0.0 |
||
284 | */ |
||
285 | public $storageEncryptionKeys = null; |
||
286 | |||
287 | /** |
||
288 | * @var string|null The index of the default key in storageEncryptionKeys. E.g. 'myKey'. |
||
289 | * @since 1.0.0 |
||
290 | */ |
||
291 | public $defaultStorageEncryptionKey = null; |
||
292 | |||
293 | /** |
||
294 | * @var string|string[]|null IP addresses, CIDR ranges, or range aliases that are allowed to connect over a |
||
295 | * non-TLS connection. If `null` or an empty array LTS is always required. |
||
296 | * |
||
297 | * Warning: Although you can use '*' or 'any' to allow a non-TLS connection from any ip address, |
||
298 | * doing so would most likely introduce a security risk and should be done for debugging purposes only! |
||
299 | * |
||
300 | * @see \yii\validators\IpValidator::$networks for a list of available alliasses. |
||
301 | */ |
||
302 | public $nonTlsAllowedRanges = 'localhost'; |
||
303 | |||
304 | /** |
||
305 | * @var class-string<Oauth2UserInterface>|null The Identity Class of your application, |
||
306 | * most likely the same as the 'identityClass' of your application's User Component. |
||
307 | * @since 1.0.0 |
||
308 | */ |
||
309 | public $identityClass = null; |
||
310 | |||
311 | /** |
||
312 | * @var null|string Prefix used for url rules. When `null` the module's uniqueId will be used. |
||
313 | * @since 1.0.0 |
||
314 | */ |
||
315 | public $urlRulesPrefix = null; |
||
316 | |||
317 | /** |
||
318 | * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix). |
||
319 | * @since 1.0.0 |
||
320 | */ |
||
321 | public $authorizePath = 'authorize'; |
||
322 | |||
323 | /** |
||
324 | * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix). |
||
325 | * @since 1.0.0 |
||
326 | */ |
||
327 | public $accessTokenPath = 'access-token'; |
||
328 | |||
329 | /** |
||
330 | * @var string URL path for the token revocation endpoint (will be prefixed with $urlRulesPrefix). |
||
331 | * @since 1.0.0 |
||
332 | */ |
||
333 | public $tokenRevocationPath = 'revoke'; |
||
334 | |||
335 | /** |
||
336 | * @var string URL path for the certificates jwks endpoint (will be prefixed with $urlRulesPrefix). |
||
337 | * @since 1.0.0 |
||
338 | */ |
||
339 | public $jwksPath = 'certs'; |
||
340 | |||
341 | /** |
||
342 | * The URL to the page where the user can perform the Client/Scope authorization |
||
343 | * (if `null` the build in page will be used). |
||
344 | * @return string |
||
345 | * @since 1.0.0 |
||
346 | * @see $clientAuthorizationPath |
||
347 | */ |
||
348 | public $clientAuthorizationUrl = null; |
||
349 | |||
350 | /** |
||
351 | * @var string The URL path to the build in page where the user can authorize the Client for the requested Scopes |
||
352 | * (will be prefixed with $urlRulesPrefix). |
||
353 | * Note: This setting will only be used if $clientAuthorizationUrl is `null`. |
||
354 | * @since 1.0.0 |
||
355 | * @see $clientAuthorizationView |
||
356 | */ |
||
357 | public $clientAuthorizationPath = 'authorize-client'; |
||
358 | |||
359 | /** |
||
360 | * @var string The view to use in the "Client Authorization" action for the page where the user can |
||
361 | * authorize the Client for the requested Scopes. |
||
362 | * Note: This setting will only be used if $clientAuthorizationUrl is `null`. |
||
363 | * @since 1.0.0 |
||
364 | * @see $clientAuthorizationPath |
||
365 | */ |
||
366 | public $clientAuthorizationView = 'authorize-client'; |
||
367 | |||
368 | /** |
||
369 | * @var bool Allow clients to invoke token revocation (RFC 7009). |
||
370 | * @see https://datatracker.ietf.org/doc/html/rfc7009 |
||
371 | */ |
||
372 | public $enableTokenRevocation = true; |
||
373 | |||
374 | /** |
||
375 | * @var bool Will the server throw an exception when a Client requests an unknown or unauthorized scope |
||
376 | * (would be silently ignored otherwise). |
||
377 | * Note: this setting can be overwritten per client. |
||
378 | */ |
||
379 | public $exceptionOnInvalidScope = false; |
||
380 | |||
381 | /** |
||
382 | * Configuration for `Oauth2Client::getRedirectUrisEnvVarConfig()` fallback (the |
||
383 | * Oauth2Client::$envVarConfig['redirectUris'] has precedence). |
||
384 | * When configured, environment variables specified in the `Oauth2Client` redirect URI(s) will be substituted with |
||
385 | * their values. Please see `EnvironmentHelper::parseEnvVars()` for more details. |
||
386 | * |
||
387 | * Warning: This setting applies to all clients, for security it's recommended to specify this configuration at the |
||
388 | * individual client level via its `envVarConfig` setting. |
||
389 | * |
||
390 | * @var array{ |
||
391 | * allowList: array, |
||
392 | * denyList: array|null, |
||
393 | * parseNested: bool, |
||
394 | * exceptionWhenNotSet: bool, |
||
395 | * exceptionWhenNotAllowed: bool, |
||
396 | * }|null |
||
397 | * @see Oauth2ClientInterface::setEnvVarConfig() |
||
398 | * @see Oauth2ClientInterface::getRedirectUrisEnvVarConfig() |
||
399 | * @see \rhertogh\Yii2Oauth2Server\helpers\EnvironmentHelper::parseEnvVars() |
||
400 | */ |
||
401 | public $clientRedirectUrisEnvVarConfig = null; |
||
402 | |||
403 | public $userAccountCreationUrl = null; |
||
404 | |||
405 | /** |
||
406 | * @var string|null The URL path to the OpenID Connect Provider Configuration Information Action. |
||
407 | * If set to `null` the endpoint will be disabled. |
||
408 | * Note: This path is defined in the |
||
409 | * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.4) |
||
410 | * specification and should normally not be changed. |
||
411 | * @since 1.0.0 |
||
412 | */ |
||
413 | public $openIdConnectProviderConfigurationInformationPath = '.well-known/openid-configuration'; |
||
414 | |||
415 | /** |
||
416 | * @var string The URL path to the OpenID Connect Userinfo Action (will be prefixed with $urlRulesPrefix). |
||
417 | * Note: This setting will only be used if $enableOpenIdConnect and $openIdConnectUserinfoEndpoint are `true`. |
||
418 | * @since 1.0.0 |
||
419 | * @see $openIdConnectUserinfoEndpoint |
||
420 | */ |
||
421 | public $openIdConnectUserinfoPath = 'oidc/userinfo'; |
||
422 | |||
423 | /** |
||
424 | * @var string The URL path to the OpenID Connect End Session Action (will be prefixed with $urlRulesPrefix). |
||
425 | * Note: This setting will only be used if $enableOpenIdConnect and |
||
426 | * $openIdConnectRpInitiatedLogoutEndpoint are `true`. |
||
427 | * @since 1.0.0 |
||
428 | * @see $openIdConnectRpInitiatedLogoutEndpoint |
||
429 | * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html |
||
430 | */ |
||
431 | public $openIdConnectRpInitiatedLogoutPath = 'oidc/end-session'; |
||
432 | |||
433 | /** |
||
434 | * The URL to the page where the user can perform the End Session (logout) confirmation |
||
435 | * (if `null` the build in page will be used). |
||
436 | * @return string |
||
437 | * @since 1.0.0 |
||
438 | * @see $openIdConnectLogoutConfirmationPath |
||
439 | */ |
||
440 | public $openIdConnectLogoutConfirmationUrl = null; |
||
441 | |||
442 | /** |
||
443 | * @var string The URL path to the build in page where the user can confirm the End Session (logout) request |
||
444 | * (will be prefixed with $urlRulesPrefix). |
||
445 | * Note: This setting will only be used if $openIdConnectLogoutConfirmationUrl is `null`. |
||
446 | * @since 1.0.0 |
||
447 | * @see $openIdConnectLogoutConfirmationView |
||
448 | */ |
||
449 | public $openIdConnectLogoutConfirmationPath = 'confirm-logout'; |
||
450 | |||
451 | /** |
||
452 | * @var string The view to use in the "End Session Authorization" action for the page where the user can |
||
453 | * authorize the End Session (logout) request. |
||
454 | * Note: This setting will only be used if $openIdConnectLogoutConfirmationUrl is `null`. |
||
455 | * @since 1.0.0 |
||
456 | * @see $openIdConnectLogoutConfirmationPath |
||
457 | */ |
||
458 | public $openIdConnectLogoutConfirmationView = 'confirm-logout'; |
||
459 | |||
460 | |||
461 | /** |
||
462 | * @var Oauth2GrantTypeFactoryInterface[]|GrantTypeInterface[]|string[]|Oauth2GrantTypeFactoryInterface|GrantTypeInterface|string|callable |
||
463 | * The Oauth 2.0 Grant Types that the module will serve. |
||
464 | * @since 1.0.0 |
||
465 | */ |
||
466 | public $grantTypes = []; |
||
467 | |||
468 | /** |
||
469 | * @var bool Should the resource server check for revocation of the access token. |
||
470 | * @since 1.0.0 |
||
471 | */ |
||
472 | public $resourceServerAccessTokenRevocationValidation = true; |
||
473 | |||
474 | /** |
||
475 | * @var bool Enable support for OpenIdvConnect. |
||
476 | * @since 1.0.0 |
||
477 | */ |
||
478 | public $enableOpenIdConnect = false; |
||
479 | |||
480 | /** |
||
481 | * @var bool Enable the .well-known/openid-configuration discovery endpoint. |
||
482 | * @since 1.0.0 |
||
483 | */ |
||
484 | public $enableOpenIdConnectDiscovery = true; |
||
485 | |||
486 | /** |
||
487 | * @var bool include `grant_types_supported` in the OpenIdConnect Discovery. |
||
488 | * Note: Since grant types can be specified per client not all clients might support all enabled grant types. |
||
489 | * @since 1.0.0 |
||
490 | */ |
||
491 | public $openIdConnectDiscoveryIncludeSupportedGrantTypes = true; |
||
492 | |||
493 | /** |
||
494 | * @var string URL to include in the OpenID Connect Discovery Service of a page containing |
||
495 | * human-readable information that developers might want or need to know when using the OpenID Provider. |
||
496 | * @see 'service_documentation' in https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 |
||
497 | * @since 1.0.0 |
||
498 | */ |
||
499 | public $openIdConnectDiscoveryServiceDocumentationUrl = null; |
||
500 | |||
501 | /** |
||
502 | * @var string|bool A string to a custom userinfo endpoint or `true` to enable the build in endpoint. |
||
503 | * @since 1.0.0 |
||
504 | * @see $openIdConnectUserinfoPath |
||
505 | */ |
||
506 | public $openIdConnectUserinfoEndpoint = true; |
||
507 | |||
508 | /** |
||
509 | * @var string|bool A string to a custom logout endpoint or `true` to enable the build in endpoint. |
||
510 | * @since 1.0.0 |
||
511 | * @see $openIdConnectRpInitiatedLogoutPath |
||
512 | * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html |
||
513 | */ |
||
514 | public $openIdConnectRpInitiatedLogoutEndpoint = false; |
||
515 | |||
516 | /** |
||
517 | * @var bool Allow access to the "end session" endpoint without user authentication (in the form of the |
||
518 | * `id_token_hint` parameter). If enabled the "end session" endpoint will always prompt the user to verify the |
||
519 | * logout if no `id_token_hint` is provided and no redirect after logout will be performed. |
||
520 | * Note: If disabled the client's `oidc_rp_initiated_logout` will be used |
||
521 | * to determine whether to prompt the end-user for logout validation. |
||
522 | * @since 1.0.0 |
||
523 | * @see $openIdConnectRpInitiatedLogoutPath |
||
524 | * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html |
||
525 | */ |
||
526 | public $openIdConnectAllowAnonymousRpInitiatedLogout = false; |
||
527 | |||
528 | /** |
||
529 | * Warning! Enabling this setting might introduce privacy concerns since the client could poll for the |
||
530 | * online status of a user. |
||
531 | * |
||
532 | * @var bool If this setting is disabled in case of OpenID Connect Context the Access Token won't include a |
||
533 | * Refresh Token when the 'offline_access' scope is not included in the authorization request. |
||
534 | * In some cases it might be needed to always include a Refresh Token, in that case enable this setting and |
||
535 | * implement the `Oauth2OidcUserSessionStatusInterface` on the User Identity model. |
||
536 | * @since 1.0.0 |
||
537 | */ |
||
538 | public $openIdConnectIssueRefreshTokenWithoutOfflineAccessScope = false; |
||
539 | |||
540 | /** |
||
541 | * @var int The default option for "User Account Selection' when not specified for a client. |
||
542 | * @since 1.0.0 |
||
543 | */ |
||
544 | public $defaultUserAccountSelection = self::USER_ACCOUNT_SELECTION_DISABLED; |
||
545 | |||
546 | /** |
||
547 | * @var bool|null Display exception messages that might leak server details. This could be useful for debugging. |
||
548 | * In case of `null` (default) the YII_DEBUG constant will be used. |
||
549 | * Warning: Should NOT be enabled in production! |
||
550 | * @since 1.0.0 |
||
551 | */ |
||
552 | public $displayConfidentialExceptionMessages = null; |
||
553 | |||
554 | /** |
||
555 | * @var string|null The namespace with which migrations will be created (and by which they will be located). |
||
556 | * Note: The specified namespace must be defined as a Yii alias (e.g. '@app'). |
||
557 | * @since 1.0.0 |
||
558 | */ |
||
559 | public $migrationsNamespace = null; |
||
560 | /** |
||
561 | * @var string|null Optional prefix used in the name of generated migrations |
||
562 | * @since 1.0.0 |
||
563 | */ |
||
564 | public $migrationsPrefix = null; |
||
565 | /** |
||
566 | * @var string|array|int|null Sets the file ownership of generated migrations |
||
567 | * @see \yii\helpers\BaseFileHelper::changeOwnership() |
||
568 | * @since 1.0.0 |
||
569 | */ |
||
570 | public $migrationsFileOwnership = null; |
||
571 | /** |
||
572 | * @var int|null Sets the file mode of generated migrations |
||
573 | * @see \yii\helpers\BaseFileHelper::changeOwnership() |
||
574 | * @since 1.0.0 |
||
575 | */ |
||
576 | public $migrationsFileMode = null; |
||
577 | |||
578 | /** |
||
579 | * The log level for HTTP Client Errors (HTTP status code 400 - 499). Can be one of the following: |
||
580 | * - A log level of `\yii\log\Logger` => LEVEL_ERROR, LEVEL_WARNING, LEVEL_INFO, LEVEL_TRACE. |
||
581 | * - `0` => disable logging for HTTP Client Errors |
||
582 | * - null => The `YII_DEBUG` constant will be used to determine the log level. |
||
583 | * If `true` LEVEL_ERROR will be used, LEVEL_INFO otherwise. |
||
584 | * @var int|null |
||
585 | * @see \yii\log\Logger |
||
586 | */ |
||
587 | public $httpClientErrorsLogLevel = null; |
||
588 | |||
589 | /** |
||
590 | * @var Oauth2AuthorizationServerInterface|null Cache for the authorization server |
||
591 | * @since 1.0.0 |
||
592 | */ |
||
593 | protected $_authorizationServer = null; |
||
594 | |||
595 | /** |
||
596 | * @var Oauth2ResourceServerInterface|null Cache for the resource server |
||
597 | * @since 1.0.0 |
||
598 | */ |
||
599 | protected $_resourceServer = null; |
||
600 | |||
601 | /** |
||
602 | * @var Oauth2CryptographerInterface|null Cache for the Oauth2Cryptographer |
||
603 | * @since 1.0.0 |
||
604 | */ |
||
605 | protected $_cryptographer = null; |
||
606 | |||
607 | /** |
||
608 | * @var string|null The authorization header used when the authorization request was validated. |
||
609 | * @since 1.0.0 |
||
610 | */ |
||
611 | protected $_oauthClaimsAuthorizationHeader = null; |
||
612 | |||
613 | /** |
||
614 | * @inheritDoc |
||
615 | * @throws InvalidConfigException |
||
616 | */ |
||
617 | 161 | public function init() |
|
618 | { |
||
619 | 161 | parent::init(); |
|
620 | |||
621 | 161 | $app = Yii::$app; |
|
622 | |||
623 | 161 | if ($app instanceof WebApplication || $this->appType == static::APPLICATION_TYPE_WEB) { |
|
624 | 31 | $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_WEB]; |
|
625 | 161 | } elseif ($app instanceof ConsoleApplication || $this->appType == static::APPLICATION_TYPE_CONSOLE) { |
|
626 | 161 | $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_CONSOLE]; |
|
627 | 161 | $this->defaultRoute = 'debug'; |
|
628 | } else { |
||
629 | 1 | throw new InvalidConfigException( |
|
630 | 1 | 'Unable to detect application type, configure it manually by setting `$appType`.' |
|
631 | 1 | ); |
|
632 | } |
||
633 | 161 | $controllerMap = array_filter( |
|
634 | 161 | $controllerMap, |
|
635 | 161 | fn($controllerSettings) => $controllerSettings['serverRole'] & $this->serverRole |
|
636 | 161 | ); |
|
637 | 161 | $this->controllerMap = ArrayHelper::getColumn($controllerMap, 'controller'); |
|
638 | |||
639 | 161 | if (empty($this->identityClass)) { |
|
640 | 1 | throw new InvalidConfigException('$identityClass must be set.'); |
|
641 | 161 | } elseif (!is_a($this->identityClass, Oauth2UserInterface::class, true)) { |
|
642 | 1 | throw new InvalidConfigException( |
|
643 | 1 | $this->identityClass . ' must implement ' . Oauth2UserInterface::class |
|
644 | 1 | ); |
|
645 | } |
||
646 | |||
647 | 161 | foreach (static::DEFAULT_INTERFACE_IMPLEMENTATIONS as $interface => $implementation) { |
|
648 | 161 | if (!Yii::$container->has($interface)) { |
|
649 | 161 | Yii::$container->set($interface, $implementation); |
|
650 | } |
||
651 | } |
||
652 | |||
653 | 161 | if (empty($this->urlRulesPrefix)) { |
|
654 | 161 | $this->urlRulesPrefix = $this->uniqueId; |
|
655 | } |
||
656 | |||
657 | 161 | $this->registerTranslations(); |
|
658 | } |
||
659 | |||
660 | /** |
||
661 | * @inheritdoc |
||
662 | * @throws InvalidConfigException |
||
663 | */ |
||
664 | 161 | public function bootstrap($app) |
|
665 | { |
||
666 | if ( |
||
667 | 161 | $app instanceof WebApplication |
|
668 | 161 | && $this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER |
|
669 | ) { |
||
670 | 31 | $rules = [ |
|
671 | 31 | $this->accessTokenPath => Oauth2ServerControllerInterface::CONTROLLER_NAME |
|
672 | 31 | . '/' . Oauth2ServerControllerInterface::ACTION_NAME_ACCESS_TOKEN, |
|
673 | 31 | $this->authorizePath => Oauth2ServerControllerInterface::CONTROLLER_NAME |
|
674 | 31 | . '/' . Oauth2ServerControllerInterface::ACTION_NAME_AUTHORIZE, |
|
675 | 31 | $this->jwksPath => Oauth2CertificatesControllerInterface::CONTROLLER_NAME |
|
676 | 31 | . '/' . Oauth2CertificatesControllerInterface::ACTION_NAME_JWKS, |
|
677 | 31 | ]; |
|
678 | |||
679 | 31 | if ($this->enableTokenRevocation) { |
|
680 | 31 | $rules[$this->tokenRevocationPath] = Oauth2ServerControllerInterface::CONTROLLER_NAME |
|
681 | 31 | . '/' . Oauth2ServerControllerInterface::ACTION_NAME_REVOKE; |
|
682 | } |
||
683 | |||
684 | 31 | if (empty($this->clientAuthorizationUrl)) { |
|
685 | 30 | $rules[$this->clientAuthorizationPath] = Oauth2ConsentControllerInterface::CONTROLLER_NAME |
|
686 | 30 | . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT; |
|
687 | } |
||
688 | |||
689 | 31 | if ($this->enableOpenIdConnect && $this->openIdConnectUserinfoEndpoint === true) { |
|
690 | 31 | $rules[$this->openIdConnectUserinfoPath] = |
|
691 | 31 | Oauth2OidcControllerInterface::CONTROLLER_NAME |
|
692 | 31 | . '/' . Oauth2OidcControllerInterface::ACTION_NAME_USERINFO; |
|
693 | } |
||
694 | |||
695 | 31 | if ($this->enableOpenIdConnect && $this->openIdConnectRpInitiatedLogoutEndpoint === true) { |
|
696 | 31 | $rules[$this->openIdConnectRpInitiatedLogoutPath] = |
|
697 | 31 | Oauth2OidcControllerInterface::CONTROLLER_NAME |
|
698 | 31 | . '/' . Oauth2OidcControllerInterface::ACTION_END_SESSION; |
|
699 | |||
700 | 31 | if (empty($this->openIdConnectLogoutConfirmationUrl)) { |
|
701 | 31 | $rules[$this->openIdConnectLogoutConfirmationPath] = |
|
702 | 31 | Oauth2ConsentControllerInterface::CONTROLLER_NAME |
|
703 | 31 | . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_END_SESSION; |
|
704 | } |
||
705 | } |
||
706 | |||
707 | 31 | $urlManager = $app->getUrlManager(); |
|
708 | 31 | $urlManager->addRules([ |
|
709 | 31 | Yii::createObject([ |
|
710 | 31 | 'class' => GroupUrlRule::class, |
|
711 | 31 | 'prefix' => $this->urlRulesPrefix, |
|
712 | 31 | 'routePrefix' => $this->id, |
|
713 | 31 | 'rules' => $rules, |
|
714 | 31 | ]), |
|
715 | 31 | ]); |
|
716 | |||
717 | if ( |
||
718 | 31 | $this->enableOpenIdConnect |
|
719 | 31 | && $this->enableOpenIdConnectDiscovery |
|
720 | 31 | && $this->openIdConnectProviderConfigurationInformationPath |
|
721 | ) { |
||
722 | 31 | $urlManager->addRules([ |
|
723 | 31 | Yii::createObject([ |
|
724 | 31 | 'class' => UrlRule::class, |
|
725 | 31 | 'pattern' => $this->openIdConnectProviderConfigurationInformationPath, |
|
726 | 31 | 'route' => $this->id |
|
727 | 31 | . '/' . Oauth2WellKnownControllerInterface::CONTROLLER_NAME |
|
728 | 31 | . '/' . Oauth2WellKnownControllerInterface::ACTION_NAME_OPENID_CONFIGURATION, |
|
729 | 31 | ]), |
|
730 | 31 | ]); |
|
731 | } |
||
732 | } |
||
733 | } |
||
734 | |||
735 | /** |
||
736 | * Registers the translations for the module |
||
737 | * @param bool $force Force the setting of the translations (even if they are already defined). |
||
738 | * @since 1.0.0 |
||
739 | */ |
||
740 | 161 | public function registerTranslations($force = false) |
|
741 | { |
||
742 | 161 | if ($force || !array_key_exists('oauth2', Yii::$app->i18n->translations)) { |
|
743 | 161 | Yii::$app->i18n->translations['oauth2'] = [ |
|
744 | 161 | 'class' => PhpMessageSource::class, |
|
745 | 161 | 'sourceLanguage' => 'en-US', |
|
746 | 161 | 'basePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages', |
|
747 | 161 | 'fileMap' => [ |
|
748 | 161 | 'oauth2' => 'oauth2.php', |
|
749 | 161 | ], |
|
750 | 161 | ]; |
|
751 | } |
||
752 | } |
||
753 | |||
754 | /** |
||
755 | * @param string $identifier The client identifier |
||
756 | * @param string $name The (user-friendly) name of the client |
||
757 | * @param int $grantTypes The grant types enabled for this client. |
||
758 | * Use bitwise `OR` to combine multiple types, |
||
759 | * e.g. `Oauth2Module::GRANT_TYPE_AUTH_CODE | Oauth2Module::GRANT_TYPE_REFRESH_TOKEN` |
||
760 | * @param string|string[] $redirectURIs One or multiple redirect URIs for the client |
||
761 | * @param int $type The client type (e.g. Confidential or Public) |
||
762 | * See `\rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface::TYPES` for possible values |
||
763 | * @param string|null $secret The client secret in case the client `type` is `confidential`. |
||
764 | * @param string|string[]|array[]|Oauth2ScopeInterface[]|null $scopes |
||
765 | * @param int|null $userId |
||
766 | * @return Oauth2ClientInterface |
||
767 | * @throws InvalidConfigException |
||
768 | * @throws \yii\db\Exception |
||
769 | */ |
||
770 | 5 | public function createClient( |
|
771 | $identifier, |
||
772 | $name, |
||
773 | $grantTypes, |
||
774 | $redirectURIs, |
||
775 | $type, |
||
776 | $secret = null, |
||
777 | $scopes = null, |
||
778 | $userId = null, |
||
779 | $endUsersMayAuthorizeClient = null, |
||
780 | $skipAuthorizationIfScopeIsAllowed = null |
||
781 | ) { |
||
782 | 5 | if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) { |
|
783 | 1 | throw new InvalidCallException('Oauth2 server role does not include authorization server.'); |
|
784 | } |
||
785 | |||
786 | /** @var Oauth2ClientInterface $client */ |
||
787 | 4 | $client = Yii::createObject([ |
|
788 | 4 | 'class' => Oauth2ClientInterface::class, |
|
789 | 4 | 'identifier' => $identifier, |
|
790 | 4 | 'type' => $type, |
|
791 | 4 | 'name' => $name, |
|
792 | 4 | 'redirectUri' => $redirectURIs, |
|
793 | 4 | 'grantTypes' => $grantTypes, |
|
794 | 4 | 'endUsersMayAuthorizeClient' => $endUsersMayAuthorizeClient, |
|
795 | 4 | 'skip_authorization_if_scope_is_allowed' => $skipAuthorizationIfScopeIsAllowed, |
|
796 | 4 | 'clientCredentialsGrantUserId' => $userId |
|
797 | 4 | ]); |
|
798 | |||
799 | 4 | $transaction = $client::getDb()->beginTransaction(); |
|
800 | |||
801 | try { |
||
802 | 4 | if ($type == Oauth2ClientInterface::TYPE_CONFIDENTIAL) { |
|
803 | 4 | $client->setSecret($secret, $this->getCryptographer()); |
|
804 | } |
||
805 | |||
806 | 3 | $client |
|
807 | 3 | ->persist() |
|
808 | 3 | ->syncClientScopes($scopes, $this->getScopeRepository()); |
|
809 | |||
810 | 3 | $transaction->commit(); |
|
811 | 1 | } catch (\Exception $e) { |
|
812 | 1 | $transaction->rollBack(); |
|
813 | 1 | throw $e; |
|
814 | } |
||
815 | |||
816 | 3 | return $client; |
|
817 | } |
||
818 | |||
819 | /** |
||
820 | * @return CryptKey The private key of the server. |
||
821 | * @throws InvalidConfigException |
||
822 | * @since 1.0.0 |
||
823 | */ |
||
824 | 22 | public function getPrivateKey() |
|
825 | { |
||
826 | 22 | $privateKey = $this->privateKey; |
|
827 | 22 | if (StringHelper::startsWith($privateKey, '@')) { |
|
828 | 19 | $privateKey = 'file://' . Yii::getAlias($privateKey); |
|
829 | } |
||
830 | 22 | return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]); |
|
831 | } |
||
832 | |||
833 | /** |
||
834 | * @return CryptKey The public key of the server. |
||
835 | * @throws InvalidConfigException |
||
836 | * @since 1.0.0 |
||
837 | */ |
||
838 | 9 | public function getPublicKey() |
|
839 | { |
||
840 | 9 | $publicKey = $this->publicKey; |
|
841 | 9 | if (StringHelper::startsWith($publicKey, '@')) { |
|
842 | 6 | $publicKey = 'file://' . Yii::getAlias($publicKey); |
|
843 | } |
||
844 | 9 | return Yii::createObject(CryptKey::class, [$publicKey]); |
|
845 | } |
||
846 | |||
847 | /** |
||
848 | * @return Oauth2AuthorizationServerInterface The authorization server. |
||
849 | * @throws InvalidConfigException |
||
850 | * @since 1.0.0 |
||
851 | */ |
||
852 | 27 | public function getAuthorizationServer() |
|
853 | { |
||
854 | 27 | if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) { |
|
855 | 1 | throw new InvalidCallException('Oauth2 server role does not include authorization server.'); |
|
856 | } |
||
857 | |||
858 | 26 | if (!$this->_authorizationServer) { |
|
859 | 26 | $this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER); |
|
860 | |||
861 | 21 | if (!$this->getCryptographer()->hasKey($this->defaultStorageEncryptionKey)) { |
|
862 | 1 | throw new InvalidConfigException( |
|
863 | 1 | 'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys' |
|
864 | 1 | ); |
|
865 | } |
||
866 | |||
867 | /** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */ |
||
868 | 19 | $keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class); |
|
869 | try { |
||
870 | 19 | $codesEncryptionKey = $keyFactory->createFromAsciiSafeString($this->codesEncryptionKey); |
|
871 | 1 | } catch (BadFormatException $e) { |
|
872 | 1 | throw new InvalidConfigException( |
|
873 | 1 | '$codesEncryptionKey is malformed: ' . $e->getMessage(), |
|
874 | 1 | 0, |
|
875 | 1 | $e |
|
876 | 1 | ); |
|
877 | } catch (EnvironmentIsBrokenException $e) { |
||
878 | throw new InvalidConfigException( |
||
879 | 'Could not instantiate $codesEncryptionKey: ' . $e->getMessage(), |
||
880 | 0, |
||
881 | $e |
||
882 | ); |
||
883 | } |
||
884 | |||
885 | 18 | if ($this->enableOpenIdConnect) { |
|
886 | 18 | $responseTypeClass = Oauth2OidcBearerTokenResponseInterface::class; |
|
887 | } else { |
||
888 | $responseTypeClass = Oauth2BearerTokenResponseInterface::class; |
||
889 | } |
||
890 | 18 | $responseType = Yii::createObject($responseTypeClass, [ |
|
891 | 18 | $this, |
|
892 | 18 | ]); |
|
893 | |||
894 | 18 | $this->_authorizationServer = Yii::createObject(Oauth2AuthorizationServerInterface::class, [ |
|
895 | 18 | $this->getClientRepository(), |
|
896 | 18 | $this->getAccessTokenRepository(), |
|
897 | 18 | $this->getScopeRepository(), |
|
898 | 18 | $this->getPrivateKey(), |
|
899 | 18 | $codesEncryptionKey, |
|
900 | 18 | $responseType |
|
901 | 18 | ]); |
|
902 | |||
903 | 18 | if (!empty($this->grantTypes)) { |
|
904 | 18 | $grantTypes = $this->grantTypes; |
|
905 | |||
906 | 18 | if (is_callable($grantTypes)) { |
|
907 | 1 | call_user_func($grantTypes, $this->_authorizationServer, $this); |
|
908 | } else { |
||
909 | 17 | if (!is_array($grantTypes)) { |
|
910 | 2 | $grantTypes = [$grantTypes]; |
|
911 | } |
||
912 | |||
913 | 17 | foreach ($grantTypes as $grantTypeDefinition) { |
|
914 | 17 | if ($grantTypeDefinition instanceof GrantTypeInterface) { |
|
915 | 1 | $accessTokenTTL = $this->getDefaultAccessTokenTTL(); |
|
916 | 1 | $this->_authorizationServer->enableGrantType($grantTypeDefinition, $accessTokenTTL); |
|
917 | } elseif ( |
||
918 | ( |
||
919 | 16 | is_numeric($grantTypeDefinition) |
|
920 | 16 | && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES) |
|
921 | ) |
||
922 | 16 | || is_a($grantTypeDefinition, Oauth2GrantTypeFactoryInterface::class, true) |
|
923 | ) { |
||
924 | if ( |
||
925 | 15 | is_numeric($grantTypeDefinition) |
|
926 | 15 | && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES) |
|
927 | ) { |
||
928 | 15 | $grantTypeDefinition = static::DEFAULT_GRANT_TYPE_FACTORIES[$grantTypeDefinition]; |
|
929 | } |
||
930 | |||
931 | /** @var Oauth2GrantTypeFactoryInterface $factory */ |
||
932 | 15 | $factory = Yii::createObject([ |
|
933 | 15 | 'class' => $grantTypeDefinition, |
|
934 | 15 | 'module' => $this, |
|
935 | 15 | ]); |
|
936 | 15 | $accessTokenTTL = $factory->getDefaultAccessTokenTTL() |
|
937 | 15 | ?? $this->getDefaultAccessTokenTTL(); |
|
938 | 15 | $this->_authorizationServer->enableGrantType($factory->getGrantType(), $accessTokenTTL); |
|
939 | } else { |
||
940 | 1 | throw new InvalidConfigException( |
|
941 | 1 | 'Unknown grantType ' |
|
942 | 1 | . ( |
|
943 | 1 | is_scalar($grantTypeDefinition) |
|
944 | 1 | ? '"' . $grantTypeDefinition . '".' |
|
945 | 1 | : 'with data type ' . gettype($grantTypeDefinition) |
|
946 | 1 | ) |
|
947 | 1 | ); |
|
948 | } |
||
949 | } |
||
950 | } |
||
951 | } |
||
952 | } |
||
953 | |||
954 | 17 | return $this->_authorizationServer; |
|
955 | } |
||
956 | |||
957 | /** |
||
958 | * @inheritDoc |
||
959 | * @throws InvalidConfigException |
||
960 | */ |
||
961 | 6 | public function getOidcScopeCollection() |
|
962 | { |
||
963 | 6 | if ($this->_oidcScopeCollection === null) { |
|
964 | 6 | $openIdConnectScopes = $this->getOpenIdConnectScopes(); |
|
965 | 6 | if ($openIdConnectScopes instanceof Oauth2OidcScopeCollectionInterface) { |
|
966 | 1 | $this->_oidcScopeCollection = $openIdConnectScopes; |
|
967 | 5 | } elseif (is_callable($openIdConnectScopes)) { |
|
968 | 1 | $this->_oidcScopeCollection = call_user_func($openIdConnectScopes, $this); |
|
969 | 1 | if (!($this->_oidcScopeCollection instanceof Oauth2OidcScopeCollectionInterface)) { |
|
970 | 1 | throw new InvalidConfigException( |
|
971 | 1 | '$openIdConnectScopes must return an instance of ' |
|
972 | 1 | . Oauth2OidcScopeCollectionInterface::class |
|
973 | 1 | ); |
|
974 | } |
||
975 | 4 | } elseif (is_array($openIdConnectScopes) || is_string($openIdConnectScopes)) { |
|
976 | 3 | $this->_oidcScopeCollection = Yii::createObject([ |
|
977 | 3 | 'class' => Oauth2OidcScopeCollectionInterface::class, |
|
978 | 3 | 'oidcScopes' => (array)$openIdConnectScopes, |
|
979 | 3 | ]); |
|
980 | } else { |
||
981 | 1 | throw new InvalidConfigException( |
|
982 | 1 | '$openIdConnectScopes must be a callable, array, string or ' |
|
983 | 1 | . Oauth2OidcScopeCollectionInterface::class |
|
984 | 1 | ); |
|
985 | } |
||
986 | } |
||
987 | |||
988 | 5 | return $this->_oidcScopeCollection; |
|
989 | } |
||
990 | |||
991 | /** |
||
992 | * @return Oauth2ResourceServerInterface The resource server. |
||
993 | * @throws InvalidConfigException |
||
994 | * @since 1.0.0 |
||
995 | */ |
||
996 | 7 | public function getResourceServer() |
|
997 | { |
||
998 | 7 | if (!($this->serverRole & static::SERVER_ROLE_RESOURCE_SERVER)) { |
|
999 | 1 | throw new InvalidCallException('Oauth2 server role does not include resource server.'); |
|
1000 | } |
||
1001 | |||
1002 | 6 | if (!$this->_resourceServer) { |
|
1003 | 6 | $this->ensureProperties(static::REQUIRED_SETTINGS_RESOURCE_SERVER); |
|
1004 | |||
1005 | 5 | $accessTokenRepository = $this->getAccessTokenRepository() |
|
1006 | 5 | ->setRevocationValidation($this->resourceServerAccessTokenRevocationValidation); |
|
1007 | |||
1008 | 5 | $this->_resourceServer = Yii::createObject(Oauth2ResourceServerInterface::class, [ |
|
1009 | 5 | $accessTokenRepository, |
|
1010 | 5 | $this->getPublicKey(), |
|
1011 | 5 | ]); |
|
1012 | } |
||
1013 | |||
1014 | 5 | return $this->_resourceServer; |
|
1015 | } |
||
1016 | |||
1017 | /** |
||
1018 | * @return Oauth2CryptographerInterface The data cryptographer for the module. |
||
1019 | * @throws InvalidConfigException |
||
1020 | * @since 1.0.0 |
||
1021 | */ |
||
1022 | 27 | public function getCryptographer() |
|
1023 | { |
||
1024 | 27 | if (!$this->_cryptographer) { |
|
1025 | 27 | $this->_cryptographer = Yii::createObject([ |
|
1026 | 27 | 'class' => Oauth2CryptographerInterface::class, |
|
1027 | 27 | 'keys' => $this->storageEncryptionKeys, |
|
1028 | 27 | 'defaultKeyName' => $this->defaultStorageEncryptionKey, |
|
1029 | 27 | ]); |
|
1030 | } |
||
1031 | |||
1032 | 26 | return $this->_cryptographer; |
|
1033 | } |
||
1034 | |||
1035 | /** |
||
1036 | * @param string|null $newKeyName |
||
1037 | * @return array |
||
1038 | * @throws InvalidConfigException |
||
1039 | */ |
||
1040 | 1 | public function rotateStorageEncryptionKeys($newKeyName = null) |
|
1041 | { |
||
1042 | 1 | $cryptographer = $this->getCryptographer(); |
|
1043 | |||
1044 | 1 | $result = []; |
|
1045 | 1 | foreach (static::ENCRYPTED_MODELS as $modelInterface) { |
|
1046 | 1 | $modelClass = DiHelper::getValidatedClassName($modelInterface); |
|
1047 | 1 | if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) { |
|
1048 | throw new InvalidConfigException($modelInterface . ' must implement ' |
||
1049 | . Oauth2EncryptedStorageInterface::class); |
||
1050 | } |
||
1051 | 1 | $result[$modelClass] = $modelClass::rotateStorageEncryptionKeys($cryptographer, $newKeyName); |
|
1052 | } |
||
1053 | |||
1054 | 1 | return $result; |
|
1055 | } |
||
1056 | |||
1057 | /** |
||
1058 | * Checks if the connection is using TLS or if the remote IP address is allowed to connect without TLS. |
||
1059 | * @return bool |
||
1060 | */ |
||
1061 | 12 | public function validateTlsConnection() |
|
1062 | { |
||
1063 | 12 | if (Yii::$app->request->getIsSecureConnection()) { |
|
1064 | 1 | return true; |
|
1065 | } |
||
1066 | |||
1067 | if ( |
||
1068 | 11 | !empty($this->nonTlsAllowedRanges) |
|
1069 | 11 | && (new IpValidator(['ranges' => $this->nonTlsAllowedRanges]))->validate(Yii::$app->request->getRemoteIP()) |
|
1070 | ) { |
||
1071 | 7 | return true; |
|
1072 | } |
||
1073 | |||
1074 | 4 | return false; |
|
1075 | } |
||
1076 | |||
1077 | /** |
||
1078 | * @return array |
||
1079 | * @throws InvalidConfigException |
||
1080 | */ |
||
1081 | public function getStorageEncryptionKeyUsage() |
||
1082 | { |
||
1083 | $cryptographer = $this->getCryptographer(); |
||
1084 | |||
1085 | $result = []; |
||
1086 | foreach (static::ENCRYPTED_MODELS as $modelInterface) { |
||
1087 | $modelClass = DiHelper::getValidatedClassName($modelInterface); |
||
1088 | if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) { |
||
1089 | throw new InvalidConfigException($modelInterface . ' must implement ' |
||
1090 | . Oauth2EncryptedStorageInterface::class); |
||
1091 | } |
||
1092 | |||
1093 | $result[$modelClass] = $modelClass::getUsedStorageEncryptionKeys($cryptographer); |
||
1094 | } |
||
1095 | |||
1096 | return $result; |
||
1097 | } |
||
1098 | |||
1099 | /** |
||
1100 | * @param Oauth2ClientInterface $client |
||
1101 | * @param string[] $requestedScopeIdentifiers |
||
1102 | * @throws Oauth2ServerException |
||
1103 | */ |
||
1104 | 6 | public function validateAuthRequestScopes($client, $requestedScopeIdentifiers, $redirectUri = null) |
|
1105 | { |
||
1106 | 6 | if (!$client->validateAuthRequestScopes($requestedScopeIdentifiers, $unknownScopes, $unauthorizedScopes)) { |
|
1107 | Yii::info('Invalid scope for client "' . $client->getIdentifier() . '": ' |
||
1108 | . VarDumper::export(['unauthorizedScopes' => $unauthorizedScopes, 'unknownScopes' => $unknownScopes])); |
||
1109 | |||
1110 | if ( |
||
1111 | $client->getExceptionOnInvalidScope() === true |
||
1112 | || ( |
||
1113 | $client->getExceptionOnInvalidScope() === null |
||
1114 | && $this->exceptionOnInvalidScope === true |
||
1115 | ) |
||
1116 | ) { |
||
1117 | if ($unknownScopes) { |
||
1118 | throw Oauth2ServerException::unknownScope(array_shift($unknownScopes), $redirectUri); |
||
1119 | } |
||
1120 | throw Oauth2ServerException::scopeNotAllowedForClient(array_shift($unauthorizedScopes), $redirectUri); |
||
1121 | } |
||
1122 | } |
||
1123 | } |
||
1124 | |||
1125 | /** |
||
1126 | * Generates a redirect Response to the Client Authorization page where the user is prompted to authorize the |
||
1127 | * Client and requested Scope. |
||
1128 | * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest |
||
1129 | * @return Response |
||
1130 | * @since 1.0.0 |
||
1131 | */ |
||
1132 | 5 | public function generateClientAuthReqRedirectResponse($clientAuthorizationRequest) |
|
1133 | { |
||
1134 | 5 | $this->setClientAuthReqSession($clientAuthorizationRequest); |
|
1135 | 5 | if (!empty($this->clientAuthorizationUrl)) { |
|
1136 | 1 | $url = $this->clientAuthorizationUrl; |
|
1137 | } else { |
||
1138 | 4 | $url = $this->uniqueId |
|
1139 | 4 | . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME |
|
1140 | 4 | . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT; |
|
1141 | } |
||
1142 | 5 | return Yii::$app->response->redirect([ |
|
1143 | 5 | $url, |
|
1144 | 5 | 'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(), |
|
1145 | 5 | ]); |
|
1146 | } |
||
1147 | |||
1148 | /** |
||
1149 | * Generates a redirect Response to the End Session Authorization page where the user is prompted to authorize the |
||
1150 | * logout. |
||
1151 | * @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest |
||
1152 | * @return Response |
||
1153 | * @since 1.0.0 |
||
1154 | */ |
||
1155 | public function generateEndSessionAuthReqRedirectResponse($endSessionAuthorizationRequest) |
||
1156 | { |
||
1157 | $this->setEndSessionAuthReqSession($endSessionAuthorizationRequest); |
||
1158 | if (!empty($this->openIdConnectLogoutConfirmationUrl)) { |
||
1159 | $url = $this->openIdConnectLogoutConfirmationUrl; |
||
1160 | } else { |
||
1161 | $url = $this->uniqueId |
||
1162 | . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME |
||
1163 | . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_END_SESSION; |
||
1164 | } |
||
1165 | return Yii::$app->response->redirect([ |
||
1166 | $url, |
||
1167 | 'endSessionAuthorizationRequestId' => $endSessionAuthorizationRequest->getRequestId(), |
||
1168 | ]); |
||
1169 | } |
||
1170 | |||
1171 | /** |
||
1172 | * Get a previously stored Client Authorization Request from the session. |
||
1173 | * @param string $requestId |
||
1174 | * @return Oauth2ClientAuthorizationRequestInterface|null |
||
1175 | * @since 1.0.0 |
||
1176 | */ |
||
1177 | 5 | public function getClientAuthReqSession($requestId) |
|
1178 | { |
||
1179 | 5 | return $this->getAuthReqSession( |
|
1180 | 5 | $requestId, |
|
1181 | 5 | static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX, |
|
1182 | 5 | Oauth2ClientAuthorizationRequestInterface::class, |
|
1183 | 5 | ); |
|
1184 | } |
||
1185 | |||
1186 | /** |
||
1187 | * Get a previously stored OIDC End Session Authorization Request from the session. |
||
1188 | * @param string $requestId |
||
1189 | * @return Oauth2EndSessionAuthorizationRequestInterface|null |
||
1190 | * @since 1.0.0 |
||
1191 | */ |
||
1192 | public function getEndSessionAuthReqSession($requestId) |
||
1193 | { |
||
1194 | return $this->getAuthReqSession( |
||
1195 | $requestId, |
||
1196 | static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX, |
||
1197 | Oauth2EndSessionAuthorizationRequestInterface::class, |
||
1198 | ); |
||
1199 | } |
||
1200 | |||
1201 | /** |
||
1202 | * Get a previously stored Authorization Request from the session. |
||
1203 | * @template T of Oauth2BaseAuthorizationRequestInterface |
||
1204 | * @param string $requestId |
||
1205 | * @param string $cachePrefix |
||
1206 | * @param class-string<T> $expectedInterface |
||
1207 | * @return T|null |
||
1208 | * @since 1.0.0 |
||
1209 | */ |
||
1210 | 5 | protected function getAuthReqSession($requestId, $cachePrefix, $expectedInterface) |
|
1211 | { |
||
1212 | 5 | if (empty($requestId)) { |
|
1213 | return null; |
||
1214 | } |
||
1215 | 5 | $key = $cachePrefix . $requestId; |
|
1216 | 5 | $authorizationRequest = Yii::$app->session->get($key); |
|
1217 | 5 | if (!($authorizationRequest instanceof $expectedInterface)) { |
|
1218 | 2 | if (!empty($authorizationRequest)) { |
|
1219 | 1 | Yii::warning( |
|
1220 | 1 | 'Found a Authorization Request Session with key "' . $key |
|
1221 | 1 | . '", but it\'s not a ' . $expectedInterface |
|
1222 | 1 | ); |
|
1223 | } |
||
1224 | 2 | return null; |
|
1225 | } |
||
1226 | 5 | if ($authorizationRequest->getRequestId() !== $requestId) { |
|
1227 | 1 | Yii::warning( |
|
1228 | 1 | 'Found a Authorization Request Session with key "' . $key |
|
1229 | 1 | . '", but its request id does not match "' . $requestId . '".' |
|
1230 | 1 | ); |
|
1231 | 1 | return null; |
|
1232 | } |
||
1233 | 5 | $authorizationRequest->setModule($this); |
|
1234 | |||
1235 | 5 | return $authorizationRequest; |
|
1236 | } |
||
1237 | |||
1238 | /** |
||
1239 | * Stores the Client Authorization Request in the session. |
||
1240 | * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest |
||
1241 | * @since 1.0.0 |
||
1242 | */ |
||
1243 | 8 | public function setClientAuthReqSession($clientAuthorizationRequest) |
|
1244 | { |
||
1245 | 8 | $this->setAuthReqSession( |
|
1246 | 8 | $clientAuthorizationRequest, |
|
1247 | 8 | static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX |
|
1248 | 8 | ); |
|
1249 | } |
||
1250 | |||
1251 | /** |
||
1252 | * Stores the OIDC End Session Authorization Request in the session. |
||
1253 | * @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest |
||
1254 | * @since 1.0.0 |
||
1255 | */ |
||
1256 | public function setEndSessionAuthReqSession($endSessionAuthorizationRequest) |
||
1257 | { |
||
1258 | $this->setAuthReqSession( |
||
1259 | $endSessionAuthorizationRequest, |
||
1260 | static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX |
||
1261 | ); |
||
1262 | } |
||
1263 | |||
1264 | /** |
||
1265 | * Stores the Authorization Request in the session. |
||
1266 | * @param Oauth2BaseAuthorizationRequestInterface $authorizationRequest |
||
1267 | * @param string $cachePrefix |
||
1268 | * @since 1.0.0 |
||
1269 | */ |
||
1270 | 8 | protected function setAuthReqSession($authorizationRequest, $cachePrefix) |
|
1271 | { |
||
1272 | 8 | $requestId = $authorizationRequest->getRequestId(); |
|
1273 | 8 | if (empty($requestId)) { |
|
1274 | 1 | throw new InvalidArgumentException('$authorizationRequest must return a request id.'); |
|
1275 | } |
||
1276 | 7 | $key = $cachePrefix . $requestId; |
|
1277 | 7 | Yii::$app->session->set($key, $authorizationRequest); |
|
1278 | } |
||
1279 | |||
1280 | /** |
||
1281 | * Clears a Client Authorization Request from the session storage. |
||
1282 | * @param string $requestId |
||
1283 | * @since 1.0.0 |
||
1284 | */ |
||
1285 | 2 | public function removeClientAuthReqSession($requestId) |
|
1286 | { |
||
1287 | 2 | $this->removeAuthReqSession($requestId, static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX); |
|
1288 | } |
||
1289 | |||
1290 | /** |
||
1291 | * Clears an End Session Authorization Request from the session storage. |
||
1292 | * @param string $requestId |
||
1293 | * @since 1.0.0 |
||
1294 | */ |
||
1295 | public function removeEndSessionAuthReqSession($requestId) |
||
1296 | { |
||
1297 | $this->removeAuthReqSession($requestId, static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX); |
||
1298 | } |
||
1299 | |||
1300 | /** |
||
1301 | * Clears an Authorization Request from the session storage. |
||
1302 | * @param string $requestId |
||
1303 | * @param string $cachePrefix |
||
1304 | * @since 1.0.0 |
||
1305 | */ |
||
1306 | 2 | public function removeAuthReqSession($requestId, $cachePrefix) |
|
1307 | { |
||
1308 | 2 | if (empty($requestId)) { |
|
1309 | 1 | throw new InvalidArgumentException('$requestId can not be empty.'); |
|
1310 | } |
||
1311 | 1 | $key = $cachePrefix . $requestId; |
|
1312 | 1 | Yii::$app->session->remove($key); |
|
1313 | } |
||
1314 | |||
1315 | /** |
||
1316 | * Stores whether the user was authenticated during the completion of the Client Authorization Request. |
||
1317 | * @param string $clientAuthorizationRequestId |
||
1318 | * @param bool $authenticatedDuringRequest |
||
1319 | * @since 1.0.0 |
||
1320 | */ |
||
1321 | public function setUserAuthenticatedDuringClientAuthRequest( |
||
1322 | $clientAuthorizationRequestId, |
||
1323 | $authenticatedDuringRequest |
||
1324 | ) { |
||
1325 | $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId); |
||
1326 | if ($clientAuthorizationRequest) { |
||
1327 | $clientAuthorizationRequest->setUserAuthenticatedDuringRequest($authenticatedDuringRequest); |
||
1328 | $this->setClientAuthReqSession($clientAuthorizationRequest); |
||
1329 | } |
||
1330 | } |
||
1331 | |||
1332 | /** |
||
1333 | * Stores the user identity selected during the completion of the Client Authorization Request. |
||
1334 | * @param string $clientAuthorizationRequestId |
||
1335 | * @param Oauth2UserInterface $userIdentity |
||
1336 | * @since 1.0.0 |
||
1337 | */ |
||
1338 | public function setClientAuthRequestUserIdentity($clientAuthorizationRequestId, $userIdentity) |
||
1339 | { |
||
1340 | $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId); |
||
1341 | if ($clientAuthorizationRequest) { |
||
1342 | $clientAuthorizationRequest->setUserIdentity($userIdentity); |
||
1343 | $this->setClientAuthReqSession($clientAuthorizationRequest); |
||
1344 | } |
||
1345 | } |
||
1346 | |||
1347 | /** |
||
1348 | * Generates a redirect Response when the Client Authorization Request is completed. |
||
1349 | * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest |
||
1350 | * @return Response |
||
1351 | * @since 1.0.0 |
||
1352 | */ |
||
1353 | 1 | public function generateClientAuthReqCompledRedirectResponse($clientAuthorizationRequest) |
|
1354 | { |
||
1355 | 1 | $clientAuthorizationRequest->processAuthorization(); |
|
1356 | 1 | $this->setClientAuthReqSession($clientAuthorizationRequest); |
|
1357 | 1 | return Yii::$app->response->redirect($clientAuthorizationRequest->getAuthorizationRequestUrl()); |
|
1358 | } |
||
1359 | |||
1360 | /** |
||
1361 | * Generates a redirect Response when the End Session Authorization Request is completed. |
||
1362 | * @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest |
||
1363 | * @return Response |
||
1364 | * @since 1.0.0 |
||
1365 | */ |
||
1366 | public function generateEndSessionAuthReqCompledRedirectResponse($endSessionAuthorizationRequest) |
||
1367 | { |
||
1368 | $endSessionAuthorizationRequest->processAuthorization(); |
||
1369 | $this->setEndSessionAuthReqSession($endSessionAuthorizationRequest); |
||
1370 | return Yii::$app->response->redirect($endSessionAuthorizationRequest->getEndSessionRequestUrl()); |
||
1371 | } |
||
1372 | |||
1373 | /** |
||
1374 | * @return IdentityInterface|Oauth2UserInterface|Oauth2OidcUserInterface|null |
||
1375 | * @throws InvalidConfigException |
||
1376 | * @since 1.0.0 |
||
1377 | */ |
||
1378 | 5 | public function getUserIdentity() |
|
1379 | { |
||
1380 | 5 | $user = Yii::$app->user->identity; |
|
1381 | 5 | if (!empty($user) && !($user instanceof Oauth2UserInterface)) { |
|
1382 | 1 | throw new InvalidConfigException( |
|
1383 | 1 | 'Yii::$app->user->identity (currently ' . get_class($user) |
|
1384 | 1 | . ') must implement ' . Oauth2UserInterface::class |
|
1385 | 1 | ); |
|
1386 | } |
||
1387 | 4 | return $user; |
|
1388 | } |
||
1389 | |||
1390 | /** |
||
1391 | * Validates a bearer token authenticated request. Note: this method does not return a result but will throw |
||
1392 | * an exception when the authentication fails. |
||
1393 | * @throws InvalidConfigException |
||
1394 | * @throws Oauth2ServerException |
||
1395 | * @since 1.0.0 |
||
1396 | */ |
||
1397 | 3 | public function validateAuthenticatedRequest() |
|
1398 | { |
||
1399 | 3 | $psr7Request = Psr7Helper::yiiToPsr7Request(Yii::$app->request); |
|
1400 | |||
1401 | 3 | $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request); |
|
1402 | |||
1403 | 3 | $token = substr(Yii::$app->request->headers->get('Authorization'), self::BEARER_TOKEN_OFFSET); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
1404 | |||
1405 | 3 | if ($token) { |
|
1406 | 3 | $claims = $this->getAccessToken($token)->claims(); |
|
1407 | |||
1408 | 3 | foreach ($claims->all() as $claimKey => $claimValue) { |
|
1409 | 3 | if (!$this->isDefaultClaimKey($claimKey)) { |
|
1410 | $psr7Request = $psr7Request->withAttribute($claimKey, $claimValue); |
||
1411 | } |
||
1412 | } |
||
1413 | } |
||
1414 | |||
1415 | 3 | $this->_oauthClaims = $psr7Request->getAttributes(); |
|
1416 | 3 | $this->_oauthClaimsAuthorizationHeader = Yii::$app->request->getHeaders()->get('Authorization'); |
|
1417 | } |
||
1418 | |||
1419 | /** |
||
1420 | * Find a user identity bases on an access token. |
||
1421 | * Note: validateAuthenticatedRequest() must be called before this method is called. |
||
1422 | * @param string $token |
||
1423 | * @param string $type |
||
1424 | * @return Oauth2UserInterface|null |
||
1425 | * @throws InvalidConfigException |
||
1426 | * @throws Oauth2ServerException |
||
1427 | * @see validateAuthenticatedRequest() |
||
1428 | * @since 1.0.0 |
||
1429 | */ |
||
1430 | 4 | public function findIdentityByAccessToken($token, $type) |
|
1431 | { |
||
1432 | 4 | if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) { |
|
1433 | 1 | throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class); |
|
1434 | } |
||
1435 | |||
1436 | if ( |
||
1437 | 3 | !preg_match('/^Bearer\s+(.*?)$/', $this->_oauthClaimsAuthorizationHeader, $matches) |
|
1438 | 3 | || !Yii::$app->security->compareString($matches[1], $token) |
|
1439 | ) { |
||
1440 | 1 | throw new InvalidCallException( |
|
1441 | 1 | 'validateAuthenticatedRequest() must be called before findIdentityByAccessToken().' |
|
1442 | 1 | ); |
|
1443 | } |
||
1444 | |||
1445 | 2 | $userId = $this->getRequestOauthUserId(); |
|
1446 | 2 | if (empty($userId)) { |
|
1447 | 1 | return null; |
|
1448 | } |
||
1449 | |||
1450 | 1 | return $this->identityClass::findIdentity($userId); |
|
1451 | } |
||
1452 | |||
1453 | /** |
||
1454 | * Generate a "Personal Access Token" (PAT) which can be used as an alternative to using passwords |
||
1455 | * for authentication (e.g. when using an API or command line). |
||
1456 | * |
||
1457 | * Note: Personal Access Tokens are intended to access resources on behalf users themselves. |
||
1458 | * To grant access to resources on behalf of an organization, or for long-lived integrations, |
||
1459 | * you most likely want to define an Oauth2 Client with the "Client Credentials" grant |
||
1460 | * (https://oauth.net/2/grant-types/client-credentials). |
||
1461 | * |
||
1462 | * @param string $clientIdentifier The Oauth2 client identifier for which the PAT should be generated. |
||
1463 | * @param int|string $userIdentifier The identifier (primary key) of the user for which the PAT should be generated. |
||
1464 | * @param Oauth2ScopeInterface[]|string[]|string|null $scope The Access Token scope. |
||
1465 | * @param string|true|null $clientSecret If the client is a "confidential" client the secret is required. |
||
1466 | * If the boolean value `true` is passed, the client secret is automatically injected. |
||
1467 | * @return Oauth2AccessTokenData |
||
1468 | */ |
||
1469 | 3 | public function generatePersonalAccessToken($clientIdentifier, $userIdentifier, $scope = null, $clientSecret = null) |
|
1470 | { |
||
1471 | 3 | if (is_array($scope)) { |
|
1472 | 2 | $scopeIdentifiers = []; |
|
1473 | 2 | foreach ($scope as $scopeItem) { |
|
1474 | 2 | if (is_string($scopeItem)) { |
|
1475 | 1 | $scopeIdentifiers[] = $scopeItem; |
|
1476 | 1 | } elseif ($scopeItem instanceof Oauth2ScopeInterface) { |
|
1477 | 1 | $scopeIdentifiers[] = $scopeItem->getIdentifier(); |
|
1478 | } else { |
||
1479 | throw new InvalidArgumentException('If $scope is an array its elements must be either' |
||
1480 | . ' a string or an instance of ' . Oauth2ScopeInterface::class); |
||
1481 | } |
||
1482 | } |
||
1483 | 2 | $scope = implode(' ', $scopeIdentifiers); |
|
1484 | } |
||
1485 | |||
1486 | 3 | if ($clientSecret === true) { |
|
1487 | /** @var Oauth2ClientInterface $client */ |
||
1488 | 3 | $client = $this->getClientRepository()->findModelByIdentifier($clientIdentifier); |
|
1489 | 3 | if ($client && $client->isConfidential()) { |
|
1490 | 3 | $clientSecret = $client->getDecryptedSecret($this->getCryptographer()); |
|
1491 | } else { |
||
1492 | $clientSecret = null; |
||
1493 | } |
||
1494 | } |
||
1495 | |||
1496 | 3 | $request = (new Psr7ServerRequest('POST', ''))->withParsedBody([ |
|
1497 | 3 | 'grant_type' => static::GRANT_TYPE_IDENTIFIER_PERSONAL_ACCESS_TOKEN, |
|
1498 | 3 | 'client_id' => $clientIdentifier, |
|
1499 | 3 | 'client_secret' => $clientSecret, |
|
1500 | 3 | 'user_id' => $userIdentifier, |
|
1501 | 3 | 'scope' => $scope, |
|
1502 | 3 | ]); |
|
1503 | |||
1504 | 3 | return new Oauth2AccessTokenData(Json::decode( |
|
1505 | 3 | $this->getAuthorizationServer() |
|
1506 | 3 | ->respondToAccessTokenRequest( |
|
1507 | 3 | $request, |
|
1508 | 3 | new Psr7Response() |
|
1509 | 3 | ) |
|
1510 | 3 | ->getBody() |
|
1511 | 3 | ->__toString() |
|
1512 | 3 | )); |
|
1513 | } |
||
1514 | |||
1515 | /** |
||
1516 | * @inheritDoc |
||
1517 | */ |
||
1518 | 5 | public function getRequestOauthClaim($attribute, $default = null) |
|
1519 | { |
||
1520 | 5 | if (empty($this->_oauthClaimsAuthorizationHeader)) { |
|
1521 | // User authorization was not processed by Oauth2Module. |
||
1522 | 1 | return $default; |
|
1523 | } |
||
1524 | 4 | if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) { |
|
1525 | 1 | throw new InvalidCallException( |
|
1526 | 1 | 'App Request Authorization header does not match the processed Oauth header.' |
|
1527 | 1 | ); |
|
1528 | } |
||
1529 | 3 | return $this->_oauthClaims[$attribute] ?? $default; |
|
1530 | } |
||
1531 | |||
1532 | /** |
||
1533 | * Helper function to ensure the required properties are configured for the module. |
||
1534 | * @param string[] $properties |
||
1535 | * @throws InvalidConfigException |
||
1536 | * @since 1.0.0 |
||
1537 | */ |
||
1538 | 32 | protected function ensureProperties($properties) |
|
1539 | { |
||
1540 | 32 | foreach ($properties as $property) { |
|
1541 | 32 | if (empty($this->$property)) { |
|
1542 | 6 | throw new InvalidConfigException(__CLASS__ . '::$' . $property . ' must be set.'); |
|
1543 | } |
||
1544 | } |
||
1545 | } |
||
1546 | |||
1547 | /** |
||
1548 | * @throws InvalidConfigException |
||
1549 | */ |
||
1550 | public function logoutUser($revokeTokens = true) |
||
1551 | { |
||
1552 | $identity = $this->getUserIdentity(); |
||
1553 | |||
1554 | if ($identity) { |
||
1555 | if ($revokeTokens) { |
||
1556 | $this->revokeTokensByUserId($identity->getId()); |
||
1557 | } |
||
1558 | |||
1559 | Yii::$app->user->logout(); |
||
1560 | } |
||
1561 | } |
||
1562 | |||
1563 | public function revokeTokensByUserId($userId) |
||
1564 | { |
||
1565 | $accessTokens = $this->getAccessTokenRepository()->revokeAccessTokensByUserId($userId); |
||
1566 | $accessTokenIds = array_map(fn($accessToken) => $accessToken->getPrimaryKey(), $accessTokens); |
||
1567 | $this->getRefreshTokenRepository()->revokeRefreshTokensByAccessTokenIds($accessTokenIds); |
||
1568 | } |
||
1569 | |||
1570 | 4 | public function getSupportedPromptValues() |
|
1571 | { |
||
1572 | 4 | $supportedPromptValues = [ |
|
1573 | 4 | Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE, |
|
1574 | 4 | Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_LOGIN, |
|
1575 | 4 | Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CONSENT, |
|
1576 | 4 | Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_SELECT_ACCOUNT, |
|
1577 | 4 | ]; |
|
1578 | |||
1579 | 4 | if (!empty($this->userAccountCreationUrl)) { |
|
1580 | 4 | $supportedPromptValues[] = Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CREATE; |
|
1581 | } |
||
1582 | |||
1583 | 4 | return $supportedPromptValues; |
|
1584 | } |
||
1585 | |||
1586 | /** |
||
1587 | * @return int |
||
1588 | */ |
||
1589 | 2 | public function getElaboratedHttpClientErrorsLogLevel() |
|
1590 | { |
||
1591 | 2 | if ($this->httpClientErrorsLogLevel === null) { |
|
1592 | 1 | return YII_DEBUG ? Logger::LEVEL_ERROR : Logger::LEVEL_INFO; |
|
1593 | } |
||
1594 | |||
1595 | 1 | return $this->httpClientErrorsLogLevel; |
|
1596 | } |
||
1597 | |||
1598 | |||
1599 | 3 | public function getJwtConfiguration(): Configuration |
|
1600 | { |
||
1601 | // Based on \League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator::initJwtConfiguration(). |
||
1602 | 3 | $jwtConfiguration = Configuration::forSymmetricSigner( |
|
1603 | 3 | new Sha256(), |
|
1604 | 3 | InMemory::plainText('empty', 'empty') |
|
1605 | 3 | ); |
|
1606 | |||
1607 | 3 | $publicKey = $this->getPublicKey(); |
|
1608 | 3 | $jwtConfiguration->setValidationConstraints( |
|
1609 | 3 | new SignedWith( |
|
1610 | 3 | new Sha256(), |
|
1611 | 3 | InMemory::plainText($publicKey->getKeyContents(), $publicKey->getPassPhrase() ?? '') |
|
1612 | 3 | ) |
|
1613 | 3 | ); |
|
1614 | |||
1615 | 3 | return $jwtConfiguration; |
|
1616 | } |
||
1617 | |||
1618 | 3 | public function getAccessToken(string $token): Token |
|
1619 | { |
||
1620 | 3 | $jwtConfiguration = $this->getJwtConfiguration(); |
|
1621 | 3 | $accessToken = $jwtConfiguration->parser()->parse($token); |
|
1622 | 3 | $jwtConfiguration->validator()->assert($accessToken, ...$jwtConfiguration->validationConstraints()); |
|
1623 | 3 | Yii::debug('Found access token: ' . $token, __METHOD__); |
|
1624 | |||
1625 | 3 | return $accessToken; |
|
1626 | } |
||
1627 | |||
1628 | 3 | protected function isDefaultClaimKey(string $claimKey): bool |
|
1629 | { |
||
1630 | 3 | return in_array( |
|
1631 | 3 | $claimKey, |
|
1632 | 3 | [ |
|
1633 | 3 | 'aud', |
|
1634 | 3 | 'jti', |
|
1635 | 3 | 'iat', |
|
1636 | 3 | 'nbf', |
|
1637 | 3 | 'exp', |
|
1638 | 3 | 'sub', |
|
1639 | 3 | 'scopes', |
|
1640 | 3 | 'client_id', |
|
1641 | 3 | ] |
|
1642 | 3 | ); |
|
1643 | } |
||
1644 | } |
||
1645 |