From 9bd03e8bf509e88d62104116a20b38b77dabcd2d Mon Sep 17 00:00:00 2001 From: Stefan Hagspiel Date: Mon, 12 Feb 2024 17:22:51 +0100 Subject: [PATCH] add oauth refresh event (#195) --- .github/workflows/codeception.yml | 4 ++-- .github/workflows/ecs.yml | 4 ++-- .github/workflows/php-stan.yml | 4 ++-- UPGRADE.md | 3 +++ docs/40_Events.md | 13 ++++++++++-- docs/SSO/12_ResourceMapping.md | 15 ++++++++++++- .../Event/OAuth/OAuthResourceRefreshEvent.php | 18 ++++++++++++++++ .../Exception/EntityNotRefreshedException.php | 7 +++++++ src/MembersBundle/MembersEvents.php | 9 ++++++++ .../OAuthIdentityAuthenticator.php | 7 ++++--- .../Security/OAuth/AccountConnector.php | 13 ++++++++++++ .../OAuth/OAuthRegistrationHandler.php | 21 +++++++++++++++++++ .../Service/ResourceMappingService.php | 19 ++++++++++++++++- 13 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 src/MembersBundle/Event/OAuth/OAuthResourceRefreshEvent.php create mode 100644 src/MembersBundle/Exception/EntityNotRefreshedException.php diff --git a/.github/workflows/codeception.yml b/.github/workflows/codeception.yml index be3eb13f..eb693e2c 100644 --- a/.github/workflows/codeception.yml +++ b/.github/workflows/codeception.yml @@ -1,9 +1,9 @@ name: Codeception on: push: - branches: [ 'master' ] + branches: [ '4.x' ] pull_request: - branches: [ 'master' ] + branches: [ '4.x' ] jobs: codeception: diff --git a/.github/workflows/ecs.yml b/.github/workflows/ecs.yml index ebd92ea6..a5deaab4 100644 --- a/.github/workflows/ecs.yml +++ b/.github/workflows/ecs.yml @@ -1,9 +1,9 @@ name: Easy Coding Standards on: push: - branches: [ 'master' ] + branches: [ '4.x' ] pull_request: - branches: [ 'master' ] + branches: [ '4.x' ] jobs: ecs: diff --git a/.github/workflows/php-stan.yml b/.github/workflows/php-stan.yml index 16bed284..46d9ca9a 100644 --- a/.github/workflows/php-stan.yml +++ b/.github/workflows/php-stan.yml @@ -1,9 +1,9 @@ name: PHP Stan on: push: - branches: [ 'master' ] + branches: [ '4.x' ] pull_request: - branches: [ 'master' ] + branches: [ '4.x' ] jobs: stan: diff --git a/UPGRADE.md b/UPGRADE.md index 79d4fb13..47830a1d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,8 @@ # Upgrade Notes +### 4.1.2 +- **[IMPROVEMENT]**: Introduce `OAUTH_RESOURCE_MAPPING_REFRESH` Event + ### 4.1.1 - **[BUGFIX]**: Also respect original asset paths in protected env diff --git a/docs/40_Events.md b/docs/40_Events.md index abab5534..aaf17f4e 100644 --- a/docs/40_Events.md +++ b/docs/40_Events.md @@ -306,7 +306,7 @@ | Type | Reference | |:--- |:--- | | **const** | `\MembersEvent:OAUTH_RESOURCE_MAPPING_PROFILE` | -| **name** | `members.oauth.connection.success` | +| **name** | `members.oauth.resource_mapping.profile` | | **class** | `\MembersBundle\Event\OAuth\OAuthResourceEvent` | | **description** | The OAUTH_RESOURCE_MAPPING_PROFILE event occurs before a sso identity gets assigned to given user profile. This event allows you to map resource data (e.g. google) to your user identity. | @@ -315,10 +315,19 @@ | Type | Reference | |:--- |:--- | | **const** | `\MembersEvent:OAUTH_RESOURCE_MAPPING_REGISTRATION` | -| **name** | `members.oauth.connection.success` | +| **name** | `members.oauth.resource_mapping.registration` | | **class** | `\MembersBundle\Event\OAuth\OAuthResourceEvent` | | **description** | The OAUTH_RESOURCE_MAPPING_REGISTRATION event occurs before the registration form gets rendered. This event allows you to map resource data (e.g. google) to your registration form. | +### members.oauth.resource_mapping.refresh + +| Type | Reference | +|:--- |:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **const** | `\MembersEvent:OAUTH_RESOURCE_MAPPING_REFRESH` | +| **name** | `members.oauth.resource_mapping.refresh` | +| **class** | `\MembersBundle\Event\OAuth\OAuthResourceRefreshEvent` | +| **description** | TThe OAUTH_RESOURCE_MAPPING_REFRESH event occurs after an existing sso identity has been found. This event allows you to map resource data (e.g. google) to your existing user identity. | + *** ### members.oauth.identity_status.profile_completion diff --git a/docs/SSO/12_ResourceMapping.md b/docs/SSO/12_ResourceMapping.md index 1b568a0c..4541fd05 100644 --- a/docs/SSO/12_ResourceMapping.md +++ b/docs/SSO/12_ResourceMapping.md @@ -40,7 +40,8 @@ class MembersResourceMappingListener implements EventSubscriberInterface { return [ MembersEvents::OAUTH_RESOURCE_MAPPING_PROFILE => 'onProfileMapping', - MembersEvents::OAUTH_RESOURCE_MAPPING_REGISTRATION => 'onRegistrationMapping' + MembersEvents::OAUTH_RESOURCE_MAPPING_REGISTRATION => 'onRegistrationMapping', + MembersEvents::OAUTH_RESOURCE_MAPPING_REFRESH => 'onRegistrationRefresh', ]; } @@ -62,6 +63,18 @@ class MembersResourceMappingListener implements EventSubscriberInterface $this->mapData($user, $ownerDetails); } + public function onRegistrationRefresh(OAuthResourceRefreshEvent $event) + { + $user = $event->getUser(); + $resourceOwner = $event->getResourceOwner(); + $ownerDetails = $resourceOwner->toArray(); + + $user->setUserName($ownerDetails['name']); + + // ATTENTION! You need to inform event about changes! + $event->setHasChanged(true); + } + protected function mapData(UserInterface $user, array $ownerDetails): void { if (empty($user->getEmail()) && isset($ownerDetails['email'])) { diff --git a/src/MembersBundle/Event/OAuth/OAuthResourceRefreshEvent.php b/src/MembersBundle/Event/OAuth/OAuthResourceRefreshEvent.php new file mode 100644 index 00000000..0c8e5677 --- /dev/null +++ b/src/MembersBundle/Event/OAuth/OAuthResourceRefreshEvent.php @@ -0,0 +1,18 @@ +hasChanged === true; + } + + public function setHasChanged(bool $hasChanged): void + { + $this->hasChanged = $hasChanged; + } +} diff --git a/src/MembersBundle/Exception/EntityNotRefreshedException.php b/src/MembersBundle/Exception/EntityNotRefreshedException.php new file mode 100644 index 00000000..83a0282f --- /dev/null +++ b/src/MembersBundle/Exception/EntityNotRefreshedException.php @@ -0,0 +1,7 @@ +getToken(), function () use ($accessToken, $client, $provider, $type, $parameter) { + $user = $client->fetchUserFromToken($accessToken); $oAuthResponse = new OAuthResponse($provider, $accessToken, $user, $parameter); - $memberUser = $this->ssoIdentityManager->getUserBySsoIdentity($oAuthResponse->getProvider(), $oAuthResponse->getResourceOwner()->getId()); + $memberUser = $this->oAuthRegistrationHandler->getRefreshedUserFromUserResponse($oAuthResponse); if ($memberUser instanceof UserInterface) { return $memberUser; diff --git a/src/MembersBundle/Security/OAuth/AccountConnector.php b/src/MembersBundle/Security/OAuth/AccountConnector.php index 48ece572..542f16b6 100755 --- a/src/MembersBundle/Security/OAuth/AccountConnector.php +++ b/src/MembersBundle/Security/OAuth/AccountConnector.php @@ -4,6 +4,7 @@ use MembersBundle\Adapter\Sso\SsoIdentityInterface; use MembersBundle\Adapter\User\UserInterface as MembersUserInterface; +use MembersBundle\Exception\EntityNotRefreshedException; use MembersBundle\Manager\SsoIdentityManagerInterface; use MembersBundle\Service\ResourceMappingService; use Symfony\Component\Security\Core\User\UserInterface; @@ -60,6 +61,18 @@ public function connectToSsoIdentity(UserInterface $user, OAuthResponseInterface return $ssoIdentity; } + /** + * @throws EntityNotRefreshedException + */ + public function refreshSsoIdentityUser(UserInterface $user, OAuthResponseInterface $oAuthResponse): void + { + if (!$user instanceof MembersUserInterface) { + throw new \InvalidArgumentException('User is not supported'); + } + + $this->resourceMappingService->mapResourceData($user, $oAuthResponse->getResourceOwner(), ResourceMappingService::MAP_FOR_REFRESH); + } + protected function applyCredentialsToSsoIdentity(SsoIdentityInterface $ssoIdentity, OAuthResponseInterface $oAuthResponse): void { $token = $oAuthResponse->getAccessToken(); diff --git a/src/MembersBundle/Security/OAuth/OAuthRegistrationHandler.php b/src/MembersBundle/Security/OAuth/OAuthRegistrationHandler.php index 1d1b03a6..0fc6d35a 100644 --- a/src/MembersBundle/Security/OAuth/OAuthRegistrationHandler.php +++ b/src/MembersBundle/Security/OAuth/OAuthRegistrationHandler.php @@ -4,6 +4,7 @@ use MembersBundle\Adapter\Sso\SsoIdentityInterface; use MembersBundle\Adapter\User\UserInterface; +use MembersBundle\Exception\EntityNotRefreshedException; use MembersBundle\Manager\SsoIdentityManagerInterface; use MembersBundle\Manager\UserManagerInterface; use MembersBundle\Service\RequestPropertiesForUserExtractorServiceInterface; @@ -33,6 +34,26 @@ public function getUserFromUserResponse(OAuthResponseInterface $OAuthResponse): return $this->ssoIdentityManager->getUserBySsoIdentity($OAuthResponse->getProvider(), $OAuthResponse->getResourceOwner()->getId()); } + public function getRefreshedUserFromUserResponse(OAuthResponseInterface $oAuthResponse): ?UserInterface + { + $user = $this->ssoIdentityManager->getUserBySsoIdentity($oAuthResponse->getProvider(), $oAuthResponse->getResourceOwner()->getId()); + + if (!$user instanceof UserInterface) { + return null; + } + + try { + $this->accountConnector->refreshSsoIdentityUser($user, $oAuthResponse); + } catch (EntityNotRefreshedException $e) { + // entity hasn't changed. return + return $user; + } + + $this->userManager->updateUser($user); + + return $user; + } + /** * @throws \Exception */ diff --git a/src/MembersBundle/Service/ResourceMappingService.php b/src/MembersBundle/Service/ResourceMappingService.php index 4bc1a8ce..d528012a 100644 --- a/src/MembersBundle/Service/ResourceMappingService.php +++ b/src/MembersBundle/Service/ResourceMappingService.php @@ -4,6 +4,8 @@ use MembersBundle\Adapter\User\UserInterface; use MembersBundle\Event\OAuth\OAuthResourceEvent; +use MembersBundle\Event\OAuth\OAuthResourceRefreshEvent; +use MembersBundle\Exception\EntityNotRefreshedException; use MembersBundle\MembersEvents; use League\OAuth2\Client\Provider\ResourceOwnerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -13,6 +15,7 @@ class ResourceMappingService { public const MAP_FOR_PROFILE = 'profile'; public const MAP_FOR_REGISTRATION = 'registration'; + public const MAP_FOR_REFRESH = 'refresh'; protected string $authIdentifier; protected EventDispatcherInterface $eventDispatcher; @@ -25,6 +28,7 @@ public function __construct(string $authIdentifier, EventDispatcherInterface $ev /** * @throws \Exception + * @throws EntityNotRefreshedException */ public function mapResourceData(UserInterface $user, ResourceOwnerInterface $resourceOwner, string $type): void { @@ -42,11 +46,24 @@ public function mapResourceData(UserInterface $user, ResourceOwnerInterface $res return; } - $this->eventDispatcher->dispatch(new OAuthResourceEvent($user, $resourceOwner), $eventName); + $eventClass = $type === self::MAP_FOR_REFRESH ? OAuthResourceRefreshEvent::class : OAuthResourceEvent::class; + $event = new $eventClass($user, $resourceOwner); + + $this->eventDispatcher->dispatch($event, $eventName); + + if ($event instanceof OAuthResourceRefreshEvent && $event->hasChanged() === false) { + throw new EntityNotRefreshedException(sprintf('entity %d has not changed', $user->getId())); + } + } public function addDefaults(UserInterface $user, ResourceOwnerInterface $resourceOwner, string $type): void { + // do not add default values in refresh mode + if ($type === self::MAP_FOR_REFRESH) { + return; + } + $ownerDetails = $resourceOwner->toArray(); $disallowedProperties = ['lastLogin', 'password', 'confirmationToken', 'passwordRequestedAt', 'groups', 'ssoIdentities'];