diff --git a/config/packages/roadiz_rozier.yaml b/config/packages/roadiz_rozier.yaml index 94486874..a04e416a 100644 --- a/config/packages/roadiz_rozier.yaml +++ b/config/packages/roadiz_rozier.yaml @@ -12,6 +12,9 @@ roadiz_rozier: oauth_client_id: '%env(string:OPEN_ID_CLIENT_ID)%' # OpenID identity provider OAuth2 client secret oauth_client_secret: '%env(string:OPEN_ID_CLIENT_SECRET)%' + requires_local_user: true + # Only when local users are not required, creating virtual users + # with following roles. granted_roles: - ROLE_USER - ROLE_BACKEND_USER diff --git a/lib/OpenId/src/Authentication/OpenIdAuthenticator.php b/lib/OpenId/src/Authentication/OpenIdAuthenticator.php index 409d6534..320b9786 100644 --- a/lib/OpenId/src/Authentication/OpenIdAuthenticator.php +++ b/lib/OpenId/src/Authentication/OpenIdAuthenticator.php @@ -23,6 +23,8 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; @@ -40,7 +42,6 @@ final class OpenIdAuthenticator extends AbstractAuthenticator private JwtRoleStrategy $roleStrategy; private OpenIdJwtConfigurationFactory $jwtConfigurationFactory; private UrlGeneratorInterface $urlGenerator; - private string $returnPath; private string $defaultRoute; private ?string $oauthClientId; @@ -49,6 +50,8 @@ final class OpenIdAuthenticator extends AbstractAuthenticator private string $targetPathParameter; private array $defaultRoles; private bool $forceSsl; + private UserProviderInterface $accountProvider; + private bool $requiresLocalUsers; public function __construct( HttpUtils $httpUtils, @@ -56,10 +59,12 @@ public function __construct( JwtRoleStrategy $roleStrategy, OpenIdJwtConfigurationFactory $jwtConfigurationFactory, UrlGeneratorInterface $urlGenerator, + UserProviderInterface $accountProvider, string $returnPath, string $defaultRoute, ?string $oauthClientId, ?string $oauthClientSecret, + bool $requiresLocalUsers = true, string $usernameClaim = 'email', string $targetPathParameter = '_target_path', array $defaultRoles = [], @@ -82,6 +87,8 @@ public function __construct( $this->urlGenerator = $urlGenerator; $this->jwtConfigurationFactory = $jwtConfigurationFactory; $this->forceSsl = $forceSsl; + $this->accountProvider = $accountProvider; + $this->requiresLocalUsers = $requiresLocalUsers; } /** @@ -90,6 +97,7 @@ public function __construct( public function supports(Request $request): ?bool { return null !== $this->discovery && + $this->discovery->isValid() && $this->httpUtils->checkRequestPath($request, $this->returnPath) && $request->query->has('state') && $request->query->has('scope') && @@ -191,39 +199,66 @@ public function authenticate(Request $request): Passport 'JWT “' . $this->usernameClaim . '” claim is not valid.' ); } + + /* + * Validate JWT token in CustomCredentials + */ + $customCredentials = new CustomCredentials( + function (Plain $jwt) { + $configuration = $this->jwtConfigurationFactory->create(); + $constraints = $configuration->validationConstraints(); + + try { + $configuration->validator()->assert($jwt, ...$constraints); + } catch (RequiredConstraintsViolated $e) { + throw new OpenIdAuthenticationException($e->getMessage(), 0, $e); + } + return true; + }, + $jwt + ); + + /* + * If local users are required, we don't need to load user from + * Identity provider, we can just use local user. + * But still need to validate JWT token. + */ + if ($this->requiresLocalUsers) { + return new Passport( + new UserBadge($username), + $customCredentials + ); + } $passport = new Passport( new UserBadge($username, function () use ($jwt, $username) { - $roles = $this->defaultRoles; - if ($this->roleStrategy->supports()) { - $roles = array_merge($roles, $this->roleStrategy->getRoles() ?? []); - } - return new OpenIdAccount( - $username, - array_unique($roles), - $jwt - ); + /* + * Load user from Identity provider, create a virtual user + * with roles configured in config/packages/roadiz_rozier.yaml + * and need to validate JWT token. + */ + return $this->loadUser($jwt->claims()->all(), $username, $jwt); }), - new CustomCredentials( - function (Plain $jwt) { - $configuration = $this->jwtConfigurationFactory->create(); - $constraints = $configuration->validationConstraints(); - try { - $configuration->validator()->assert($jwt, ...$constraints); - } catch (RequiredConstraintsViolated $e) { - throw new OpenIdAuthenticationException($e->getMessage(), 0, $e); - } - return true; - }, - $jwt - ) + $customCredentials ); - $passport->setAttribute('jwt', $jwt); $passport->setAttribute('token', !empty($jsonResponse['access_token']) ? $jsonResponse['access_token'] : $jwt->toString()); return $passport; } + protected function loadUser(array $payload, string $identity, Plain $jwt): UserInterface + { + $roles = $this->defaultRoles; + if ($this->roleStrategy->supports()) { + $roles = array_merge($roles, $this->roleStrategy->getRoles() ?? []); + } + return new OpenIdAccount( + $identity, + array_unique($roles), + $jwt + ); + } + /** * @inheritDoc */ diff --git a/lib/OpenId/src/Discovery.php b/lib/OpenId/src/Discovery.php index 1f79ee16..cbd79f2f 100644 --- a/lib/OpenId/src/Discovery.php +++ b/lib/OpenId/src/Discovery.php @@ -36,6 +36,11 @@ public function __construct(string $discoveryUri, CacheItemPoolInterface $cacheA $this->cacheAdapter = $cacheAdapter; } + public function isValid(): bool + { + return !empty($this->discoveryUri) && filter_var($this->discoveryUri, FILTER_VALIDATE_URL); + } + protected function populateParameters(): void { $cacheItem = $this->cacheAdapter->getItem(static::CACHE_KEY); @@ -70,7 +75,7 @@ protected function populateParameters(): void */ public function canVerifySignature(): bool { - return $this->has('jwks_uri'); + return $this->isValid() && $this->has('jwks_uri'); } /** diff --git a/lib/OpenId/src/OAuth2LinkGenerator.php b/lib/OpenId/src/OAuth2LinkGenerator.php index 49d1e93e..ac6cc591 100644 --- a/lib/OpenId/src/OAuth2LinkGenerator.php +++ b/lib/OpenId/src/OAuth2LinkGenerator.php @@ -40,7 +40,7 @@ public function __construct( */ public function isSupported(Request $request): bool { - return null !== $this->discovery; + return null !== $this->discovery && $this->discovery->isValid(); } public function generate( diff --git a/lib/OpenId/src/OpenIdJwtConfigurationFactory.php b/lib/OpenId/src/OpenIdJwtConfigurationFactory.php index bb91cdb6..5ffa41a6 100644 --- a/lib/OpenId/src/OpenIdJwtConfigurationFactory.php +++ b/lib/OpenId/src/OpenIdJwtConfigurationFactory.php @@ -52,7 +52,7 @@ protected function getValidationConstraints(): array $validators[] = new HostedDomain(trim($this->openIdHostedDomain)); } - if (null !== $this->discovery) { + if (null !== $this->discovery && $this->discovery->isValid()) { $issuer = $this->discovery->get('issuer'); $userinfoEndpoint = $this->discovery->get('userinfo_endpoint'); if (is_string($issuer) && !empty($issuer)) { diff --git a/lib/RoadizRozierBundle/config/packages/roadiz_rozier.yaml b/lib/RoadizRozierBundle/config/packages/roadiz_rozier.yaml index 463dd0ea..7564c246 100644 --- a/lib/RoadizRozierBundle/config/packages/roadiz_rozier.yaml +++ b/lib/RoadizRozierBundle/config/packages/roadiz_rozier.yaml @@ -12,9 +12,13 @@ roadiz_rozier: oauth_client_id: '%env(string:OPEN_ID_CLIENT_ID)%' # OpenID identity provider OAuth2 client secret oauth_client_secret: '%env(string:OPEN_ID_CLIENT_SECRET)%' + requires_local_user: false + # Only when local users are not required, creating virtual users + # with following roles. granted_roles: - ROLE_USER - ROLE_BACKEND_USER + - ROLE_SUPERADMIN entries: dashboard: name: dashboard diff --git a/lib/RoadizRozierBundle/src/DependencyInjection/Configuration.php b/lib/RoadizRozierBundle/src/DependencyInjection/Configuration.php index aa35dde4..7a4d04f7 100644 --- a/lib/RoadizRozierBundle/src/DependencyInjection/Configuration.php +++ b/lib/RoadizRozierBundle/src/DependencyInjection/Configuration.php @@ -111,6 +111,13 @@ protected function addOpenIdNode() ->defaultValue('email') ->info(<<end() + ->booleanNode('requires_local_user') + ->defaultValue('true') + ->info(<<end() @@ -127,7 +134,7 @@ protected function addOpenIdNode() ->prototype('scalar') ->defaultValue(['ROLE_USER']) ->info(<<end() diff --git a/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php b/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php index e0bf8d85..bfbe3a13 100644 --- a/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php +++ b/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php @@ -44,15 +44,13 @@ private function registerOpenId(array $config, ContainerBuilder $container): voi $container->setParameter('roadiz_rozier.open_id.hosted_domain', $config['open_id']['hosted_domain']); $container->setParameter('roadiz_rozier.open_id.oauth_client_id', $config['open_id']['oauth_client_id']); $container->setParameter('roadiz_rozier.open_id.oauth_client_secret', $config['open_id']['oauth_client_secret']); + $container->setParameter('roadiz_rozier.open_id.requires_local_user', $config['open_id']['requires_local_user']); $container->setParameter('roadiz_rozier.open_id.openid_username_claim', $config['open_id']['openid_username_claim']); $container->setParameter('roadiz_rozier.open_id.scopes', $config['open_id']['scopes'] ?? []); $container->setParameter('roadiz_rozier.open_id.granted_roles', $config['open_id']['granted_roles'] ?? []); - if ( - \is_string($config['open_id']['discovery_url']) && - !empty($config['open_id']['discovery_url']) && - filter_var($config['open_id']['discovery_url'], FILTER_VALIDATE_URL) - ) { + // Do not test URL here, as DotEnv could not be loaded yet. + if (!empty($config['open_id']['discovery_url'])) { /* * Register OpenID discovery service only when discovery URL is set. */ @@ -95,10 +93,12 @@ private function registerOpenId(array $config, ContainerBuilder $container): voi new Reference(\RZ\Roadiz\OpenId\Authentication\Provider\ChainJwtRoleStrategy::class), new Reference('roadiz_rozier.open_id.jwt_configuration_factory'), new Reference(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class), + new Reference(\RZ\Roadiz\OpenId\Authentication\Provider\OpenIdAccountProvider::class), 'loginPage', 'adminHomePage', $config['open_id']['oauth_client_id'], $config['open_id']['oauth_client_secret'], + $config['open_id']['requires_local_user'], $config['open_id']['openid_username_claim'], '_target_path', $config['open_id']['granted_roles'],