Skip to content

Commit

Permalink
feat(OpenID): Added new OpenID mode with `roadiz_rozier.open_id.requi…
Browse files Browse the repository at this point in the history
…res_local_user` (default: true) which requires an existing Roadiz account before authenticating SSO users.

- Users authenticated against SSO with real user account will use their real roles and groups instead of open_id permissions
- Fixed `discovery_url` configuration when using a DotEnv placeholder.
  • Loading branch information
ambroisemaupate committed Aug 3, 2023
1 parent 53d0a1d commit 639e1a5
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 33 deletions.
3 changes: 3 additions & 0 deletions config/packages/roadiz_rozier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 59 additions & 24 deletions lib/OpenId/src/Authentication/OpenIdAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -49,17 +50,21 @@ 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,
?Discovery $discovery,
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 = [],
Expand All @@ -82,6 +87,8 @@ public function __construct(
$this->urlGenerator = $urlGenerator;
$this->jwtConfigurationFactory = $jwtConfigurationFactory;
$this->forceSsl = $forceSsl;
$this->accountProvider = $accountProvider;
$this->requiresLocalUsers = $requiresLocalUsers;
}

/**
Expand All @@ -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') &&
Expand Down Expand Up @@ -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
*/
Expand Down
7 changes: 6 additions & 1 deletion lib/OpenId/src/Discovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -70,7 +75,7 @@ protected function populateParameters(): void
*/
public function canVerifySignature(): bool
{
return $this->has('jwks_uri');
return $this->isValid() && $this->has('jwks_uri');
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/OpenId/src/OAuth2LinkGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion lib/OpenId/src/OpenIdJwtConfigurationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
4 changes: 4 additions & 0 deletions lib/RoadizRozierBundle/config/packages/roadiz_rozier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ protected function addOpenIdNode()
->defaultValue('email')
->info(<<<EOD
OpenID identity provider identifier claim field
EOD
)
->end()
->booleanNode('requires_local_user')
->defaultValue('true')
->info(<<<EOD
A local account must exists for each OpenID user.
EOD
)
->end()
Expand All @@ -127,7 +134,7 @@ protected function addOpenIdNode()
->prototype('scalar')
->defaultValue(['ROLE_USER'])
->info(<<<EOD
Roles granted to user logged in with OpenId authentication process.
Roles granted to user logged in with OpenId authentication process. Only when local users are not required, creating virtual users.
EOD
)
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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'],
Expand Down

0 comments on commit 639e1a5

Please sign in to comment.