Skip to content

Commit

Permalink
Support user confirmation for OpenID Connect RP-Initiated Logout
Browse files Browse the repository at this point in the history
  • Loading branch information
rhertogh committed Jun 18, 2024
1 parent 608cb86 commit d156787
Show file tree
Hide file tree
Showing 37 changed files with 1,160 additions and 183 deletions.
3 changes: 3 additions & 0 deletions sample/migrations/m210103_000000_oauth2_sample.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\scope\Oauth2OidcScopeCollectionInterface;
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
use rhertogh\Yii2Oauth2Server\models\Oauth2Scope;
use yii\db\Migration;

Expand Down Expand Up @@ -120,8 +121,10 @@ public function safeUp()
'secret' => '2021-01-01::3vUCADtKx59NPQl3/1fJXmppRbiug3iccJc1S9XY6TPvLE02/+ggB8GtIc24J5oMTj38NIPIpNt8ClNDS7ZBI4+ykNxYOuEHQfdkDiUf5WVKtLegx43gLXfq', # "secret"
'name' => 'Sample client for OpenID Connect with Grant Type Auth Code',
'redirect_uris' => '["http://localhost/redirect_uri/", "https://oauth.pstmn.io/v1/callback"]',
'post_logout_redirect_uris' => '["http://localhost/post_logout_redirect_uri"]',
'token_types' => 1, # Bearer
'grant_types' => 5, # AUTH_CODE | REFRESH_TOKEN
'oidc_rp_initiated_logout' => Oauth2ClientInterface::OIDC_RP_INITIATED_LOGOUT_ENABLED,
'enabled' => 1,
'created_at' => time(),
'updated_at' => time(),
Expand Down
252 changes: 214 additions & 38 deletions src/Oauth2Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
use rhertogh\Yii2Oauth2Server\exceptions\Oauth2ServerException;
use rhertogh\Yii2Oauth2Server\helpers\DiHelper;
use rhertogh\Yii2Oauth2Server\helpers\Psr7Helper;
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\base\Oauth2BaseAuthorizationRequestInterface;
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\client\Oauth2ClientAuthorizationRequestInterface;
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\EndSession\Oauth2EndSessionAuthorizationRequestInterface;
use rhertogh\Yii2Oauth2Server\interfaces\components\common\DefaultAccessTokenTtlInterface;
use rhertogh\Yii2Oauth2Server\interfaces\components\encryption\Oauth2CryptographerInterface;
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\encryption\Oauth2EncryptionKeyFactoryInterface;
Expand Down Expand Up @@ -151,7 +153,13 @@ class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface, Defau
* Prefix used in session storage of Client Authorization Requests
* @since 1.0.0
*/
protected const CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OATH2_CLIENT_AUTHORIZATION_REQUEST_';
protected const CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OAUTH2_CLIENT_AUTHORIZATION_REQUEST_';

/**
* Prefix used in session storage of End Session Authorization Requests
* @since 1.0.0
*/
protected const END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OAUTH2_END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX_';

/**
* Controller mapping for the module. Will be parsed on `init()`.
Expand Down Expand Up @@ -316,26 +324,29 @@ class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface, Defau
public $jwksPath = 'certs';

/**
* The URL to the page where the user can perform the client/scope authorization
* The URL to the page where the user can perform the Client/Scope authorization
* (if `null` the build in page will be used).
* @return string
* @since 1.0.0
* @see $clientAuthorizationPath
*/
public $clientAuthorizationUrl = null;

/**
* @var string The URL path to the build in page where the user can authorize the client for the requested scopes
* @var string The URL path to the build in page where the user can authorize the Client for the requested Scopes
* (will be prefixed with $urlRulesPrefix).
* Note: This setting will only be used if $clientAuthorizationUrl is `null`.
* @since 1.0.0
* @see $clientAuthorizationView
*/
public $clientAuthorizationPath = 'authorize-client';

/**
* @var string The view to use in the "client authorization action" for the page where the user can
* authorize the client for the requested scopes.
* @var string The view to use in the "Client Authorization" action for the page where the user can
* authorize the Client for the requested Scopes.
* Note: This setting will only be used if $clientAuthorizationUrl is `null`.
* @since 1.0.0
* @see $clientAuthorizationPath
*/
public $clientAuthorizationView = 'authorize-client';

Expand Down Expand Up @@ -401,6 +412,34 @@ class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface, Defau
*/
public $openIdConnectRpInitiatedLogoutPath = 'oidc/end-session';

/**
* The URL to the page where the user can perform the End Session (logout) confirmation
* (if `null` the build in page will be used).
* @return string
* @since 1.0.0
* @see $openIdConnectLogoutConfirmationPath
*/
public $openIdConnectLogoutConfirmationUrl = null;

/**
* @var string The URL path to the build in page where the user can confirm the End Session (logout) request
* (will be prefixed with $urlRulesPrefix).
* Note: This setting will only be used if $openIdConnectLogoutConfirmationUrl is `null`.
* @since 1.0.0
* @see $openIdConnectLogoutConfirmationView
*/
public $openIdConnectLogoutConfirmationPath = 'confirm-logout';

/**
* @var string The view to use in the "End Session Authorization" action for the page where the user can
* authorize the End Session (logout) request.
* Note: This setting will only be used if $openIdConnectLogoutConfirmationUrl is `null`.
* @since 1.0.0
* @see $openIdConnectLogoutConfirmationPath
*/
public $openIdConnectLogoutConfirmationView = 'confirm-logout';


/**
* @var Oauth2GrantTypeFactoryInterface[]|GrantTypeInterface[]|string[]|Oauth2GrantTypeFactoryInterface|GrantTypeInterface|string|callable
* The Oauth 2.0 Grant Types that the module will serve.
Expand Down Expand Up @@ -638,6 +677,11 @@ public function bootstrap($app)
$rules[$this->openIdConnectRpInitiatedLogoutPath] =
Oauth2OidcControllerInterface::CONTROLLER_NAME
. '/' . Oauth2OidcControllerInterface::ACTION_END_SESSION;

if (empty($this->openIdConnectLogoutConfirmationUrl)) {
$rules[$this->openIdConnectLogoutConfirmationPath] = Oauth2ConsentControllerInterface::CONTROLLER_NAME
. '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_END_SESSION;
}
}

$urlManager = $app->getUrlManager();
Expand Down Expand Up @@ -1059,8 +1103,8 @@ public function validateAuthRequestScopes($client, $requestedScopeIdentifiers, $
}

/**
* Generates a redirect Response to the client authorization page where the user is prompted to authorize the
* client and requested scope.
* Generates a redirect Response to the Client Authorization page where the user is prompted to authorize the
* Client and requested Scope.
* @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
* @return Response
* @since 1.0.0
Expand All @@ -1081,38 +1125,94 @@ public function generateClientAuthReqRedirectResponse($clientAuthorizationReques
]);
}

/**
* Generates a redirect Response to the End Session Authorization page where the user is prompted to authorize the
* logout.
* @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest
* @return Response
* @since 1.0.0
*/
public function generateEndSessionAuthReqRedirectResponse($endSessionAuthorizationRequest)
{
$this->setEndSessionAuthReqSession($endSessionAuthorizationRequest);
if (!empty($this->openIdConnectLogoutConfirmationUrl)) {
$url = $this->openIdConnectLogoutConfirmationUrl;
} else {
$url = $this->uniqueId
. '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
. '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_END_SESSION;
}
return Yii::$app->response->redirect([
$url,
'endSessionAuthorizationRequestId' => $endSessionAuthorizationRequest->getRequestId(),
]);
}

/**
* Get a previously stored Client Authorization Request from the session.
* @param string $requestId
* @return Oauth2ClientAuthorizationRequestInterface|null
* @since 1.0.0
*/
public function getClientAuthReqSession($requestId)
{
return $this->getAuthReqSession(
$requestId,
static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX,
Oauth2ClientAuthorizationRequestInterface::class,
);
}

/**
* Get a previously stored OIDC End Session Authorization Request from the session.
* @param string $requestId
* @return Oauth2EndSessionAuthorizationRequestInterface|null
* @since 1.0.0
*/
public function getEndSessionAuthReqSession($requestId)
{
return $this->getAuthReqSession(
$requestId,
static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX,
Oauth2EndSessionAuthorizationRequestInterface::class,
);
}

/**
* Get a previously stored Authorization Request from the session.
* @template T of Oauth2BaseAuthorizationRequestInterface
* @param string $requestId
* @param string $cachePrefix
* @param class-string<T> $expectedInterface
* @return T|null
* @since 1.0.0
*/
protected function getAuthReqSession($requestId, $cachePrefix, $expectedInterface)
{
if (empty($requestId)) {
return null;
}
$key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
$clientAuthorizationRequest = Yii::$app->session->get($key);
if (!($clientAuthorizationRequest instanceof Oauth2ClientAuthorizationRequestInterface)) {
if (!empty($clientAuthorizationRequest)) {
$key = $cachePrefix . $requestId;
$authorizationRequest = Yii::$app->session->get($key);
if (!($authorizationRequest instanceof $expectedInterface)) {
if (!empty($authorizationRequest)) {
Yii::warning(
'Found a ClientAuthorizationRequestSession with key "' . $key
. '", but it\'s not a ' . Oauth2ClientAuthorizationRequestInterface::class
'Found a Authorization Request Session with key "' . $key
. '", but it\'s not a ' . $expectedInterface
);
}
return null;
}
if ($clientAuthorizationRequest->getRequestId() !== $requestId) {
if ($authorizationRequest->getRequestId() !== $requestId) {
Yii::warning(
'Found a ClientAuthorizationRequestSession with key "' . $key
. '", but its request id does not match "' . $requestId . '".'
'Found a Authorization Request Session with key "' . $key
. '", but its request id does not match "' . $requestId . '".'
);
return null;
}
$clientAuthorizationRequest->setModule($this);
$authorizationRequest->setModule($this);

return $clientAuthorizationRequest;
return $authorizationRequest;
}

/**
Expand All @@ -1122,12 +1222,68 @@ public function getClientAuthReqSession($requestId)
*/
public function setClientAuthReqSession($clientAuthorizationRequest)
{
$requestId = $clientAuthorizationRequest->getRequestId();
$this->setAuthReqSession($clientAuthorizationRequest, static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX);
}

/**
* Stores the OIDC End Session Authorization Request in the session.
* @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest
* @since 1.0.0
*/
public function setEndSessionAuthReqSession($endSessionAuthorizationRequest)
{
$this->setAuthReqSession($endSessionAuthorizationRequest, static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX);
}

/**
* Stores the Authorization Request in the session.
* @param Oauth2BaseAuthorizationRequestInterface $authorizationRequest
* @param string $cachePrefix
* @since 1.0.0
*/
protected function setAuthReqSession($authorizationRequest, $cachePrefix)
{
$requestId = $authorizationRequest->getRequestId();
if (empty($requestId)) {
throw new InvalidArgumentException('$scopeAuthorization must return a request id.');
throw new InvalidArgumentException('$authorizationRequest must return a request id.');
}
$key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
Yii::$app->session->set($key, $clientAuthorizationRequest);
$key = $cachePrefix . $requestId;
Yii::$app->session->set($key, $authorizationRequest);
}

/**
* Clears a Client Authorization Request from the session storage.
* @param string $requestId
* @since 1.0.0
*/
public function removeClientAuthReqSession($requestId)
{
$this->removeAuthReqSession($requestId, static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX);
}

/**
* Clears an End Session Authorization Request from the session storage.
* @param string $requestId
* @since 1.0.0
*/
public function removeEndSessionAuthReqSession($requestId)
{
$this->removeAuthReqSession($requestId, static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX);
}

/**
* Clears an Authorization Request from the session storage.
* @param string $requestId
* @param string $cachePrefix
* @since 1.0.0
*/
public function removeAuthReqSession($requestId, $cachePrefix)
{
if (empty($requestId)) {
throw new InvalidArgumentException('$requestId can not be empty.');
}
$key = $cachePrefix . $requestId;
Yii::$app->session->remove($key);
}

/**
Expand Down Expand Up @@ -1162,20 +1318,6 @@ public function setClientAuthRequestUserIdentity($clientAuthorizationRequestId,
}
}

/**
* Clears a Client Authorization Request from the session storage.
* @param string $requestId
* @since 1.0.0
*/
public function removeClientAuthReqSession($requestId)
{
if (empty($requestId)) {
throw new InvalidArgumentException('$requestId can not be empty.');
}
$key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
Yii::$app->session->remove($key);
}

/**
* Generates a redirect Response when the Client Authorization Request is completed.
* @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
Expand All @@ -1189,6 +1331,19 @@ public function generateClientAuthReqCompledRedirectResponse($clientAuthorizatio
return Yii::$app->response->redirect($clientAuthorizationRequest->getAuthorizationRequestUrl());
}

/**
* Generates a redirect Response when the End Session Authorization Request is completed.
* @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest
* @return Response
* @since 1.0.0
*/
public function generateEndSessionAuthReqCompledRedirectResponse($endSessionAuthorizationRequest)
{
$endSessionAuthorizationRequest->processAuthorization();
$this->setEndSessionAuthReqSession($endSessionAuthorizationRequest);
return Yii::$app->response->redirect($endSessionAuthorizationRequest->getEndSessionRequestUrl());
}

/**
* @return IdentityInterface|Oauth2UserInterface|Oauth2OidcUserInterface|null
* @throws InvalidConfigException
Expand Down Expand Up @@ -1351,11 +1506,32 @@ protected function ensureProperties($properties)
}
}

public function logoutUser()
/**
* @throws InvalidConfigException
*/
public function logoutUser($revokeTokens = true)
{
Yii::$app->user->logout();
$identity = $this->getUserIdentity();

if ($identity) {
if ($revokeTokens) {
$this->revokeTokensByUserId($identity->getId());
}

Yii::$app->user->logout();
}
}

public function revokeTokensByUserId($userId)
{
$accessTokens = $this->getAccessTokenRepository()->revokeAccessTokensByUserId($userId);
$accessTokenIds = array_map(fn($accessToken) => $accessToken->getPrimaryKey(), $accessTokens);
$this->getRefreshTokenRepository()->revokeRefreshTokensByAccessTokenIds($accessTokenIds);
}

/**
* @return int
*/
public function getElaboratedHttpClientErrorsLogLevel()
{
if ($this->httpClientErrorsLogLevel === null) {
Expand Down
Loading

0 comments on commit d156787

Please sign in to comment.