rhertogh /
yii2-oauth2-server
| 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; |
||
|
0 ignored issues
–
show
Coding Style
Documentation
introduced
by
Loading history...
|
|||
| 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); |
|
| 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 |