Skip to content

Commit

Permalink
add oauth refresh event (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
solverat authored Feb 12, 2024
1 parent 49d0f78 commit 9bd03e8
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 13 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/codeception.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Codeception
on:
push:
branches: [ 'master' ]
branches: [ '4.x' ]
pull_request:
branches: [ 'master' ]
branches: [ '4.x' ]

jobs:
codeception:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ecs.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Easy Coding Standards
on:
push:
branches: [ 'master' ]
branches: [ '4.x' ]
pull_request:
branches: [ 'master' ]
branches: [ '4.x' ]

jobs:
ecs:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/php-stan.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: PHP Stan
on:
push:
branches: [ 'master' ]
branches: [ '4.x' ]
pull_request:
branches: [ 'master' ]
branches: [ '4.x' ]

jobs:
stan:
Expand Down
3 changes: 3 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
13 changes: 11 additions & 2 deletions docs/40_Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand All @@ -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
Expand Down
15 changes: 14 additions & 1 deletion docs/SSO/12_ResourceMapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
}

Expand All @@ -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'])) {
Expand Down
18 changes: 18 additions & 0 deletions src/MembersBundle/Event/OAuth/OAuthResourceRefreshEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace MembersBundle\Event\OAuth;

class OAuthResourceRefreshEvent extends OAuthResourceEvent
{
protected bool $hasChanged = false;

public function hasChanged(): bool
{
return $this->hasChanged === true;
}

public function setHasChanged(bool $hasChanged): void
{
$this->hasChanged = $hasChanged;
}
}
7 changes: 7 additions & 0 deletions src/MembersBundle/Exception/EntityNotRefreshedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace MembersBundle\Exception;

final class EntityNotRefreshedException extends \Exception
{
}
9 changes: 9 additions & 0 deletions src/MembersBundle/MembersEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,15 @@ final class MembersEvents
*/
public const OAUTH_RESOURCE_MAPPING_REGISTRATION = 'members.oauth.resource_mapping.registration';

/**
* The 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.
*
* @Event("MembersBundle\Event\OAuth\OAuthResourceRefreshEvent")
*/
public const OAUTH_RESOURCE_MAPPING_REFRESH = 'members.oauth.resource_mapping.refresh';

/**
* The OAUTH_IDENTITY_STATUS_PROFILE_COMPLETION event occurs before a user enters the profile completion step.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use MembersBundle\Adapter\User\UserInterface;
use MembersBundle\Manager\SsoIdentityManagerInterface;
use MembersBundle\Security\OAuth\Dispatcher\Router\DispatchRouter;
use MembersBundle\Security\OAuth\Exception\AccountNotLinkedException;
use MembersBundle\Security\OAuth\OAuthRegistrationHandler;
use MembersBundle\Security\OAuth\OAuthResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -29,7 +29,7 @@ class OAuthIdentityAuthenticator extends OAuth2Authenticator implements Authenti
public function __construct(
protected UrlGeneratorInterface $router,
protected ClientRegistry $clientRegistry,
protected SsoIdentityManagerInterface $ssoIdentityManager,
protected OAuthRegistrationHandler $oAuthRegistrationHandler,
protected DispatchRouter $dispatchRouter
) {
}
Expand Down Expand Up @@ -67,9 +67,10 @@ public function authenticate(Request $request): Passport

return new SelfValidatingPassport(
new UserBadge($accessToken->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;
Expand Down
13 changes: 13 additions & 0 deletions src/MembersBundle/Security/OAuth/AccountConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
21 changes: 21 additions & 0 deletions src/MembersBundle/Security/OAuth/OAuthRegistrationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down
19 changes: 18 additions & 1 deletion src/MembersBundle/Service/ResourceMappingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
{
Expand All @@ -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'];

Expand Down

0 comments on commit 9bd03e8

Please sign in to comment.