parent
d71542d19d
commit
21d5f05b0d
@ -0,0 +1,29 @@ |
||||
# authentication configuration for each Access URL |
||||
# Access URL Id / authentication method / params |
||||
parameters: |
||||
authentication: |
||||
1: |
||||
generic: |
||||
enabled: false |
||||
client_id: '' |
||||
client_secret: '' |
||||
provider_options: |
||||
urlAuthorize: '' |
||||
urlAccessToken: '' |
||||
urlResourceOwnerDetails: '' |
||||
responseResourceOwnerId: 'sub' |
||||
accessTokenMethod: 'POST' |
||||
scopes: |
||||
- openid |
||||
allow_create_new_users: true |
||||
allow_update_user_info: false |
||||
resource_owner_username_field: null |
||||
resource_owner_firstname_field: null |
||||
resource_owner_lastname_field: null |
||||
resource_owner_email_field: null |
||||
resource_owner_status_field: null |
||||
resource_owner_teacher_status_field: null |
||||
resource_owner_sessadmin_status_field: null |
||||
resource_owner_hr_status_field: null |
||||
resource_owner_status_status_field: null |
||||
resource_owner_anon_status_field: null |
||||
@ -1,3 +1,10 @@ |
||||
knpu_oauth2_client: |
||||
clients: |
||||
generic: |
||||
type: generic |
||||
provider_class: League\OAuth2\Client\Provider\GenericProvider |
||||
client_id: '' |
||||
client_secret: '' |
||||
redirect_route: chamilo.oauth2_generic_check |
||||
|
||||
# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration |
||||
|
||||
@ -0,0 +1,27 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Controller\OAuth2; |
||||
|
||||
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper; |
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; |
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
|
||||
abstract class AbstractProviderController extends AbstractController |
||||
{ |
||||
protected function getStartResponse( |
||||
string $providerName, |
||||
ClientRegistry $clientRegistry, |
||||
AuthenticationConfigHelper $authenticationConfigHelper, |
||||
): Response { |
||||
if (!$authenticationConfigHelper->isEnabled($providerName)) { |
||||
throw $this->createAccessDeniedException(); |
||||
} |
||||
|
||||
return $clientRegistry->getClient($providerName)->redirect(); |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Controller\OAuth2; |
||||
|
||||
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper; |
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\Routing\Attribute\Route; |
||||
|
||||
class GenericProviderController extends AbstractProviderController |
||||
{ |
||||
#[Route('/connect/generic', name: 'chamilo.oauth2_generic_start')] |
||||
public function connect( |
||||
ClientRegistry $clientRegistry, |
||||
AuthenticationConfigHelper $authenticationConfigHelper, |
||||
): Response { |
||||
return $this->getStartResponse('generic', $clientRegistry, $authenticationConfigHelper); |
||||
} |
||||
|
||||
#[Route('/connect/generic/check', name: 'chamilo.oauth2_generic_check')] |
||||
public function connectCheck(): void {} |
||||
} |
||||
@ -0,0 +1,50 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Decorator; |
||||
|
||||
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper; |
||||
use KnpU\OAuth2ClientBundle\DependencyInjection\KnpUOAuth2ClientExtension; |
||||
use KnpU\OAuth2ClientBundle\DependencyInjection\ProviderFactory; |
||||
use KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle; |
||||
use League\OAuth2\Client\Provider\AbstractProvider; |
||||
use League\OAuth2\Client\Provider\GenericProvider; |
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; |
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; |
||||
|
||||
#[AsDecorator(decorates: 'knpu.oauth2.provider_factory')] |
||||
readonly class OAuth2ProviderFactoryDecorator |
||||
{ |
||||
public function __construct( |
||||
#[AutowireDecorated] |
||||
private ProviderFactory $inner, |
||||
private AuthenticationConfigHelper $authenticationConfigHelper, |
||||
) {} |
||||
|
||||
public function createProvider( |
||||
$class, |
||||
array $options, |
||||
?string $redirectUri = null, |
||||
array $redirectParams = [], |
||||
array $collaborators = [] |
||||
): AbstractProvider { |
||||
$options = match ($class) { |
||||
GenericProvider::class => $this->getProviderOptions('generic'), |
||||
}; |
||||
|
||||
return $this->inner->createProvider($class, $options, $redirectUri, $redirectParams, $collaborators); |
||||
} |
||||
|
||||
private function getProviderOptions(string $providerName): array |
||||
{ |
||||
/** @var KnpUOAuth2ClientExtension $extension */ |
||||
$extension = (new KnpUOAuth2ClientBundle())->getContainerExtension(); |
||||
|
||||
$configParams = $this->authenticationConfigHelper->getParams($providerName); |
||||
|
||||
return $extension->getConfigurator($providerName)->getProviderOptions($configParams); |
||||
} |
||||
} |
||||
@ -0,0 +1,80 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Security\Authenticator\OAuth2; |
||||
|
||||
use Chamilo\CoreBundle\Entity\User; |
||||
use Chamilo\CoreBundle\Repository\Node\UserRepository; |
||||
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper; |
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; |
||||
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface; |
||||
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator; |
||||
use League\OAuth2\Client\Token\AccessToken; |
||||
use Symfony\Component\HttpFoundation\RedirectResponse; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\Routing\RouterInterface; |
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; |
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException; |
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; |
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; |
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; |
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; |
||||
|
||||
abstract class AbstractAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface |
||||
{ |
||||
protected string $providerName = ''; |
||||
|
||||
protected OAuth2ClientInterface $client; |
||||
|
||||
public function __construct( |
||||
protected readonly ClientRegistry $clientRegistry, |
||||
protected readonly RouterInterface $router, |
||||
protected readonly UserRepository $userRepository, |
||||
protected readonly AuthenticationConfigHelper $authenticationConfigHelper, |
||||
) { |
||||
$this->client = $this->clientRegistry->getClient($this->providerName); |
||||
} |
||||
|
||||
public function start(Request $request, ?AuthenticationException $authException = null): Response |
||||
{ |
||||
$targetUrl = $this->router->generate('login'); |
||||
|
||||
return new RedirectResponse($targetUrl); |
||||
} |
||||
|
||||
abstract public function supports(Request $request): ?bool; |
||||
|
||||
public function authenticate(Request $request): Passport |
||||
{ |
||||
/** @var AccessToken $accessToken */ |
||||
$accessToken = $this->fetchAccessToken($this->client); |
||||
|
||||
$user = $this->userLoader($accessToken); |
||||
|
||||
return new SelfValidatingPassport( |
||||
new UserBadge( |
||||
$user->getUserIdentifier() |
||||
), |
||||
); |
||||
} |
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response |
||||
{ |
||||
$targetUrl = $this->router->generate('index'); |
||||
|
||||
return new RedirectResponse($targetUrl); |
||||
} |
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response |
||||
{ |
||||
$message = strtr($exception->getMessage(), $exception->getMessageData()); |
||||
|
||||
return new Response($message, Response::HTTP_FORBIDDEN); |
||||
} |
||||
|
||||
abstract protected function userLoader(AccessToken $accessToken): User; |
||||
} |
||||
@ -0,0 +1,201 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Security\Authenticator\OAuth2; |
||||
|
||||
use Chamilo\CoreBundle\Entity\User; |
||||
use Chamilo\CoreBundle\Repository\ExtraFieldRepository; |
||||
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository; |
||||
use Chamilo\CoreBundle\Repository\Node\UserRepository; |
||||
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper; |
||||
use ExtraField; |
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; |
||||
use League\OAuth2\Client\Provider\GenericResourceOwner; |
||||
use League\OAuth2\Client\Token\AccessToken; |
||||
use League\OAuth2\Client\Tool\ArrayAccessorTrait; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\Routing\RouterInterface; |
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException; |
||||
use UnexpectedValueException; |
||||
|
||||
class GenericAuthenticator extends AbstractAuthenticator |
||||
{ |
||||
use ArrayAccessorTrait; |
||||
|
||||
public const EXTRA_FIELD_OAUTH2_ID = 'oauth2_id'; |
||||
|
||||
protected string $providerName = 'generic'; |
||||
|
||||
public function __construct( |
||||
ClientRegistry $clientRegistry, |
||||
RouterInterface $router, |
||||
UserRepository $userRepository, |
||||
AuthenticationConfigHelper $authenticationConfigHelper, |
||||
protected readonly ExtraFieldRepository $extraFieldRepository, |
||||
protected readonly ExtraFieldValuesRepository $extraFieldValuesRepository, |
||||
) { |
||||
parent::__construct( |
||||
$clientRegistry, |
||||
$router, |
||||
$userRepository, |
||||
$authenticationConfigHelper |
||||
); |
||||
} |
||||
|
||||
public function supports(Request $request): ?bool |
||||
{ |
||||
return 'chamilo.oauth2_generic_check' === $request->attributes->get('_route'); |
||||
} |
||||
|
||||
protected function userLoader(AccessToken $accessToken): User |
||||
{ |
||||
$providerParams = $this->authenticationConfigHelper->getParams('generic'); |
||||
|
||||
/** @var GenericResourceOwner $resourceOwner */ |
||||
$resourceOwner = $this->client->fetchUserFromToken($accessToken); |
||||
$resourceOwnerData = $resourceOwner->toArray(); |
||||
$resourceOwnerId = $resourceOwner->getId(); |
||||
|
||||
if (empty($resourceOwnerId)) { |
||||
throw new UnexpectedValueException('Value for the resource owner identifier not found at the configured key'); |
||||
} |
||||
|
||||
$fieldType = (int) ExtraField::getExtraFieldTypeFromString('user'); |
||||
$extraField = $this->extraFieldRepository->findByVariable($fieldType, self::EXTRA_FIELD_OAUTH2_ID); |
||||
|
||||
$existingUserExtraFieldValue = $this->extraFieldValuesRepository->findByVariableAndValue( |
||||
$extraField, |
||||
$resourceOwnerId |
||||
); |
||||
|
||||
if (null === $existingUserExtraFieldValue) { |
||||
$username = $this->getValueByKey( |
||||
$resourceOwnerData, |
||||
$providerParams['resource_owner_username_field'], |
||||
"oauth2user_$resourceOwnerId" |
||||
); |
||||
|
||||
/** @var User $user */ |
||||
$user = $this->userRepository->findOneBy(['username' => $username]); |
||||
|
||||
if (!$user || 'platform' !== $user->getAuthSource()) { |
||||
if (!$providerParams['allow_create_new_users']) { |
||||
throw new AuthenticationException('This user doesn\'t have an account yet and auto-provisioning is not enabled. Please contact this portal administration team to request access.'); |
||||
} |
||||
|
||||
// set default values, real values are set in self::updateUserInfo method |
||||
$user = (new User()) |
||||
->setFirstname('OAuth2 User default firstname') |
||||
->setLastname('OAuth2 User default firstname') |
||||
->setEmail('oauth2user_'.$resourceOwnerId.'@'.(gethostname() or 'localhost')) |
||||
->setUsername($username) |
||||
->setPlainPassword($username) |
||||
->setStatus(STUDENT) |
||||
->setCreatorId($this->userRepository->getRootUser()->getId()) |
||||
; |
||||
} |
||||
|
||||
$this->saveUserInfo($user, $resourceOwnerData, $providerParams); |
||||
|
||||
$this->extraFieldValuesRepository->updateItemData( |
||||
$extraField, |
||||
$user, |
||||
$resourceOwnerId |
||||
); |
||||
} else { |
||||
/** @var User $user */ |
||||
$user = $this->userRepository->find( |
||||
$existingUserExtraFieldValue->getItemId() |
||||
); |
||||
|
||||
if ($providerParams['allow_update_user_info']) { |
||||
$this->saveUserInfo($user, $resourceOwnerData, $providerParams); |
||||
} |
||||
} |
||||
|
||||
return $user; |
||||
} |
||||
|
||||
/** |
||||
* Set user information from the resource owner's data or the user itself |
||||
*/ |
||||
public function saveUserInfo(User $user, array $resourceOwnerData, array $providerParams): void |
||||
{ |
||||
$status = $this->getUserStatus($resourceOwnerData, $user->getStatus(), $providerParams); |
||||
|
||||
$user |
||||
->setFirstname( |
||||
$this->getValueByKey( |
||||
$resourceOwnerData, |
||||
$providerParams['resource_owner_firstname_field'], |
||||
$user->getFirstname() |
||||
) |
||||
) |
||||
->setLastname( |
||||
$this->getValueByKey( |
||||
$resourceOwnerData, |
||||
$providerParams['resource_owner_lastname_field'], |
||||
$user->getLastname() |
||||
) |
||||
) |
||||
->setUsername( |
||||
$this->getValueByKey( |
||||
$resourceOwnerData, |
||||
$providerParams['resource_owner_username_field'], |
||||
$user->getUsername() |
||||
) |
||||
) |
||||
->setEmail( |
||||
$this->getValueByKey( |
||||
$resourceOwnerData, |
||||
$providerParams['resource_owner_email_field'], |
||||
$user->getEmail() |
||||
) |
||||
) |
||||
->setAuthSource('oauth2') |
||||
->setStatus($status) |
||||
->setRoleFromStatus($status) |
||||
; |
||||
|
||||
$this->userRepository->updateUser($user); |
||||
// updateAccessUrls ? |
||||
} |
||||
|
||||
private function getUserStatus(array $resourceOwnerData, int $defaultStatus, array $providerParams): int |
||||
{ |
||||
$status = $this->getValueByKey( |
||||
$resourceOwnerData, |
||||
$providerParams['resource_owner_status_field'], |
||||
$defaultStatus |
||||
); |
||||
|
||||
$responseStatus = []; |
||||
|
||||
if ($teacherStatus = $providerParams['resource_owner_teacher_status_field']) { |
||||
$responseStatus[COURSEMANAGER] = $teacherStatus; |
||||
} |
||||
|
||||
if ($sessAdminStatus = $providerParams['resource_owner_sessadmin_status_field']) { |
||||
$responseStatus[SESSIONADMIN] = $sessAdminStatus; |
||||
} |
||||
|
||||
if ($drhStatus = $providerParams['resource_owner_hr_status_field']) { |
||||
$responseStatus[DRH] = $drhStatus; |
||||
} |
||||
|
||||
if ($studentStatus = $providerParams['resource_owner_status_status_field']) { |
||||
$responseStatus[STUDENT] = $studentStatus; |
||||
} |
||||
|
||||
if ($anonStatus = $providerParams['resource_owner_anon_status_field']) { |
||||
$responseStatus[ANONYMOUS] = $anonStatus; |
||||
} |
||||
|
||||
$map = array_flip($responseStatus); |
||||
|
||||
return $map[$status] ?? $status; |
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\ServiceHelper; |
||||
|
||||
use Chamilo\CoreBundle\Entity\AccessUrl; |
||||
use InvalidArgumentException; |
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; |
||||
|
||||
readonly class AuthenticationConfigHelper |
||||
{ |
||||
public function __construct( |
||||
private ParameterBagInterface $parameterBag, |
||||
private AccessUrlHelper $urlHelper, |
||||
) {} |
||||
|
||||
public function getParams(string $providerName, ?AccessUrl $url = null): array |
||||
{ |
||||
$urlId = $url ? $url->getId() : $this->urlHelper->getCurrent()->getId(); |
||||
|
||||
$authentication = $this->parameterBag->get('authentication'); |
||||
|
||||
if (!isset($authentication[$urlId])) { |
||||
throw new InvalidArgumentException('Invalid access URL Id'); |
||||
} |
||||
|
||||
if (!isset($authentication[$urlId][$providerName])) { |
||||
throw new InvalidArgumentException('Invalid authentication source'); |
||||
} |
||||
|
||||
return $authentication[$urlId][$providerName]; |
||||
} |
||||
|
||||
public function isEnabled(string $methodName, ?AccessUrl $url = null): bool |
||||
{ |
||||
$configParams = $this->getParams($methodName, $url); |
||||
|
||||
return $configParams['enabled'] ?? false; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue