commit
619f40f3c5
@ -0,0 +1,15 @@ |
||||
.external-logins { |
||||
@apply flex flex-col gap-2 items-center; |
||||
|
||||
&__divider { |
||||
@apply w-60 mx-auto uppercase; |
||||
} |
||||
|
||||
&__button-list { |
||||
@apply space-y-4; |
||||
} |
||||
|
||||
&__button { |
||||
@apply border border-gray-25 bg-white rounded-lg text-gray-90 py-4 px-12 block font-semibold; |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
<script setup> |
||||
import BaseAppLink from "../basecomponents/BaseAppLink.vue" |
||||
import BaseDivider from "../basecomponents/BaseDivider.vue" |
||||
import { useI18n } from "vue-i18n" |
||||
import { usePlatformConfig } from "../../store/platformConfig" |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const platformConfig = usePlatformConfig() |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
v-if="platformConfig.externalAuthentication.length > 0" |
||||
class="external-logins" |
||||
> |
||||
<BaseDivider |
||||
:title="t('Or')" |
||||
align="center" |
||||
class="external-logins__divider" |
||||
/> |
||||
|
||||
<ul class="external-logins__button-list"> |
||||
<li |
||||
v-for="(extAuth, idx) in platformConfig.externalAuthentication" |
||||
:key="idx" |
||||
> |
||||
<BaseAppLink |
||||
:url="extAuth.url" |
||||
class="external-logins__button" |
||||
> |
||||
{{ t("Continue with %s", [extAuth.title]) }} |
||||
</BaseAppLink> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</template> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,55 @@ |
||||
# authentication configuration for each Access URL |
||||
# Access URL Id / authentication method / params |
||||
parameters: |
||||
authentication: |
||||
default: |
||||
generic: |
||||
enabled: false |
||||
title: 'External' |
||||
client_id: '' |
||||
client_secret: '' |
||||
provider_options: |
||||
urlAuthorize: '' |
||||
urlAccessToken: '' |
||||
urlResourceOwnerDetails: '' |
||||
responseResourceOwnerId: 'sub' |
||||
# accessTokenMethod: 'POST' |
||||
# responseError: 'error' |
||||
# responseCode: '' |
||||
# scopeSeparator: ' ' |
||||
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 |
||||
resource_owner_urls_field: null |
||||
|
||||
facebook: |
||||
enabled: false |
||||
title: 'Facebook' |
||||
client_id: '' |
||||
client_secret: '' |
||||
graph_api_version: 'v20.0' |
||||
redirect_params: { } |
||||
|
||||
keycloak: |
||||
enabled: false |
||||
title: 'Keycloak' |
||||
client_id: '' |
||||
client_secret: '' |
||||
auth_server_url: '' |
||||
realm: '' |
||||
version: '' |
||||
encryption_algorithm: null |
||||
encryption_key_path: null |
||||
encryption_key: null |
||||
redirect_params: { } |
@ -1,3 +1,27 @@ |
||||
knpu_oauth2_client: |
||||
clients: |
||||
generic: |
||||
type: generic |
||||
provider_class: League\OAuth2\Client\Provider\GenericProvider |
||||
client_id: '' |
||||
client_secret: '' |
||||
redirect_route: chamilo.oauth2_generic_check |
||||
|
||||
facebook: |
||||
type: facebook |
||||
client_id: '' |
||||
client_secret: '' |
||||
redirect_route: chamilo.oauth2_facebook_check |
||||
graph_api_version: '' |
||||
redirect_params: { } |
||||
|
||||
keycloak: |
||||
type: keycloak |
||||
client_id: '' |
||||
client_secret: '' |
||||
redirect_route: chamilo.oauth2_keycloak_check |
||||
redirect_params: { } |
||||
auth_server_url: null |
||||
realm: null |
||||
|
||||
# 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 FacebookProviderController extends AbstractProviderController |
||||
{ |
||||
#[Route('/connect/facebook', name: 'chamilo.oauth2_facebook_start')] |
||||
public function connect( |
||||
ClientRegistry $clientRegistry, |
||||
AuthenticationConfigHelper $authenticationConfigHelper, |
||||
): Response { |
||||
return $this->getStartResponse('facebook', $clientRegistry, $authenticationConfigHelper); |
||||
} |
||||
|
||||
#[Route('/connect/facebook/check', name: 'chamilo.oauth2_facebook_check')] |
||||
public function connectCheck(): void {} |
||||
} |
@ -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,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 KeycloakProviderController extends AbstractProviderController |
||||
{ |
||||
#[Route('/connect/keycloak', name: 'chamilo.oauth2_keycloak_start')] |
||||
public function connect( |
||||
ClientRegistry $clientRegistry, |
||||
AuthenticationConfigHelper $authenticationConfigHelper, |
||||
): Response { |
||||
return $this->getStartResponse('keycloak', $clientRegistry, $authenticationConfigHelper); |
||||
} |
||||
|
||||
#[Route('/connect/keycloak/check', name: 'chamilo.oauth2_keycloak_check')] |
||||
public function connectCheck(): void {} |
||||
} |
@ -0,0 +1,54 @@ |
||||
<?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\Facebook; |
||||
use League\OAuth2\Client\Provider\GenericProvider; |
||||
use Stevenmaguire\OAuth2\Client\Provider\Keycloak; |
||||
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'), |
||||
Facebook::class => $this->getProviderOptions('facebook'), |
||||
Keycloak::class => $this->getProviderOptions('keycloak'), |
||||
}; |
||||
|
||||
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,85 @@ |
||||
<?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\AccessUrlHelper; |
||||
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, |
||||
protected readonly AccessUrlHelper $urlHelper, |
||||
) { |
||||
$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); |
||||
} |
||||
|
||||
/** |
||||
* Find or create and save the new user. |
||||
*/ |
||||
abstract protected function userLoader(AccessToken $accessToken): User; |
||||
} |
@ -0,0 +1,83 @@ |
||||
<?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\AccessUrlHelper; |
||||
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper; |
||||
use Cocur\Slugify\SlugifyInterface; |
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; |
||||
use League\OAuth2\Client\Provider\FacebookUser; |
||||
use League\OAuth2\Client\Token\AccessToken; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\Routing\RouterInterface; |
||||
|
||||
class FacebookAuthenticator extends AbstractAuthenticator |
||||
{ |
||||
protected string $providerName = 'facebook'; |
||||
|
||||
public function __construct( |
||||
ClientRegistry $clientRegistry, |
||||
RouterInterface $router, |
||||
UserRepository $userRepository, |
||||
AuthenticationConfigHelper $authenticationConfigHelper, |
||||
AccessUrlHelper $urlHelper, |
||||
protected readonly SlugifyInterface $slugify, |
||||
) { |
||||
parent::__construct( |
||||
$clientRegistry, |
||||
$router, |
||||
$userRepository, |
||||
$authenticationConfigHelper, |
||||
$urlHelper, |
||||
); |
||||
} |
||||
|
||||
public function supports(Request $request): ?bool |
||||
{ |
||||
return 'chamilo.oauth2_facebook_check' === $request->attributes->get('_route'); |
||||
} |
||||
|
||||
protected function userLoader(AccessToken $accessToken): User |
||||
{ |
||||
/** @var FacebookUser $resourceOwner */ |
||||
$resourceOwner = $this->client->fetchUserFromToken($accessToken); |
||||
|
||||
$user = $this->userRepository->findOneBy(['email' => $resourceOwner->getEmail()]); |
||||
|
||||
if (!$user) { |
||||
$user = (new User()) |
||||
->setCreatorId($this->userRepository->getRootUser()->getId()) |
||||
; |
||||
} |
||||
|
||||
$user |
||||
->setFirstname($resourceOwner->getFirstName()) |
||||
->setLastname($resourceOwner->getLastName()) |
||||
// ->setLocale($resourceOwner->getLocale()) |
||||
->setEmail($resourceOwner->getEmail()) |
||||
->setUsername($this->changeToValidChamiloLogin($resourceOwner->getEmail())) |
||||
->setPlainPassword('facebook') |
||||
->setStatus(STUDENT) |
||||
->setAuthSource('facebook') |
||||
->setRoleFromStatus(STUDENT) |
||||
; |
||||
|
||||
$this->userRepository->updateUser($user); |
||||
|
||||
$url = $this->urlHelper->getCurrent(); |
||||
$url->addUser($user); |
||||
|
||||
return $user; |
||||
} |
||||
|
||||
private function changeToValidChamiloLogin(string $email): string |
||||
{ |
||||
return $this->slugify->slugify($email); |
||||
} |
||||
} |
@ -0,0 +1,273 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Security\Authenticator\OAuth2; |
||||
|
||||
use Chamilo\CoreBundle\Entity\AccessUrl; |
||||
use Chamilo\CoreBundle\Entity\AccessUrlRelUser; |
||||
use Chamilo\CoreBundle\Entity\User; |
||||
use Chamilo\CoreBundle\Repository\ExtraFieldRepository; |
||||
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository; |
||||
use Chamilo\CoreBundle\Repository\Node\AccessUrlRepository; |
||||
use Chamilo\CoreBundle\Repository\Node\UserRepository; |
||||
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper; |
||||
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
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, |
||||
AccessUrlHelper $urlHelper, |
||||
protected readonly ExtraFieldRepository $extraFieldRepository, |
||||
protected readonly ExtraFieldValuesRepository $extraFieldValuesRepository, |
||||
protected readonly AccessUrlRepository $accessUrlRepository, |
||||
protected readonly EntityManagerInterface $entityManager, |
||||
) { |
||||
parent::__construct( |
||||
$clientRegistry, |
||||
$router, |
||||
$userRepository, |
||||
$authenticationConfigHelper, |
||||
$urlHelper, |
||||
); |
||||
} |
||||
|
||||
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() ?: 'localhost')) |
||||
->setUsername($username) |
||||
->setPlainPassword($username) |
||||
->setStatus(STUDENT) |
||||
->setCreatorId($this->userRepository->getRootUser()->getId()) |
||||
; |
||||
} |
||||
|
||||
$this->saveUserInfo($user, $resourceOwnerData, $providerParams); |
||||
|
||||
$this->extraFieldValuesRepository->updateItemData( |
||||
$extraField, |
||||
$user, |
||||
$resourceOwnerId |
||||
); |
||||
|
||||
$this->updateUrls($user, $resourceOwnerData, $providerParams); |
||||
} else { |
||||
/** @var User $user */ |
||||
$user = $this->userRepository->find( |
||||
$existingUserExtraFieldValue->getItemId() |
||||
); |
||||
|
||||
if ($providerParams['allow_update_user_info']) { |
||||
$this->saveUserInfo($user, $resourceOwnerData, $providerParams); |
||||
|
||||
$this->updateUrls($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); |
||||
|
||||
$url = $this->urlHelper->getCurrent(); |
||||
$url->addUser($user); |
||||
} |
||||
|
||||
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; |
||||
} |
||||
|
||||
private function updateUrls(User $user, array $resourceOwnerData, array $providerParams): void |
||||
{ |
||||
if (!($urlsField = $providerParams['resource_owner_urls_field'])) { |
||||
return; |
||||
} |
||||
|
||||
$availableUrls = []; |
||||
|
||||
$urls = $this->accessUrlRepository->findAll(); |
||||
|
||||
/** @var AccessUrl $existingUrl */ |
||||
foreach ($urls as $existingUrl) { |
||||
$availableUrls[(string) $existingUrl->getId()] = $existingUrl->getId(); |
||||
$availableUrls[$existingUrl->getUrl()] = $existingUrl->getId(); |
||||
} |
||||
|
||||
$allowedUrlIds = []; |
||||
|
||||
foreach ($this->getValueByKey($resourceOwnerData, $urlsField) as $value) { |
||||
if (array_key_exists($value, $availableUrls)) { |
||||
$allowedUrlIds[] = $availableUrls[$value]; |
||||
} else { |
||||
$newValue = ($value[-1] === '/') ? substr($value, 0, -1) : $value.'/'; |
||||
|
||||
if (array_key_exists($newValue, $availableUrls)) { |
||||
$allowedUrlIds[] = $availableUrls[$newValue]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
$grantedUrlIds = []; |
||||
|
||||
foreach ($this->accessUrlRepository->findByUser($user) as $grantedUrl) { |
||||
$grantedUrlIds[] = $grantedUrl->getId(); |
||||
} |
||||
|
||||
$urlRelUserRepo = $this->entityManager->getRepository(AccessUrlRelUser::class); |
||||
|
||||
foreach (array_diff($grantedUrlIds, $allowedUrlIds) as $extraUrlId) { |
||||
$urlRelUser = $urlRelUserRepo->findOneBy(['user' => $user, 'url' => $extraUrlId]); |
||||
|
||||
if ($urlRelUser) { |
||||
$this->entityManager->remove($urlRelUser); |
||||
} |
||||
} |
||||
|
||||
$this->entityManager->flush(); |
||||
|
||||
foreach (array_diff($allowedUrlIds, $grantedUrlIds) as $missingUrlId) { |
||||
/** @var AccessUrl $missingUrl */ |
||||
$missingUrl = $this->accessUrlRepository->find($missingUrlId); |
||||
$missingUrl->addUser($user); |
||||
} |
||||
|
||||
$this->entityManager->flush(); |
||||
} |
||||
} |
@ -0,0 +1,58 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\Security\Authenticator\OAuth2; |
||||
|
||||
use Chamilo\CoreBundle\Entity\User; |
||||
use League\OAuth2\Client\Token\AccessToken; |
||||
use Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
class KeycloakAuthenticator extends AbstractAuthenticator |
||||
{ |
||||
protected string $providerName = 'keycloak'; |
||||
|
||||
public function supports(Request $request): ?bool |
||||
{ |
||||
return 'chamilo.oauth2_keycloak_check' === $request->attributes->get('_route'); |
||||
} |
||||
|
||||
protected function userLoader(AccessToken $accessToken): User |
||||
{ |
||||
/** @var KeycloakResourceOwner $resourceOwner */ |
||||
$resourceOwner = $this->client->fetchUserFromToken($accessToken); |
||||
|
||||
$user = $this->userRepository->findOneBy(['username' => $resourceOwner->getUsername()]) |
||||
?: |
||||
$this->userRepository->findOneBy(['username' => $resourceOwner->getId()]); |
||||
|
||||
if (!$user) { |
||||
$user = (new User()) |
||||
->setCreatorId($this->userRepository->getRootUser()->getId()) |
||||
; |
||||
} |
||||
|
||||
$username = $resourceOwner->getUsername() ?: $resourceOwner->getId(); |
||||
|
||||
$user |
||||
->setFirstname($resourceOwner->getFirstName()) |
||||
->setLastname($resourceOwner->getLastName()) |
||||
->setEmail($resourceOwner->getEmail()) |
||||
->setUsername($username) |
||||
->setPlainPassword('keycloak') |
||||
->setStatus(STUDENT) |
||||
->setAuthSource('keycloak') |
||||
->setRoleFromStatus(STUDENT) |
||||
; |
||||
|
||||
$this->userRepository->updateUser($user); |
||||
|
||||
$url = $this->urlHelper->getCurrent(); |
||||
$url->addUser($user); |
||||
|
||||
return $user; |
||||
} |
||||
} |
@ -0,0 +1,77 @@ |
||||
<?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; |
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
||||
|
||||
use function Symfony\Component\String\u; |
||||
|
||||
readonly class AuthenticationConfigHelper |
||||
{ |
||||
public function __construct( |
||||
private ParameterBagInterface $parameterBag, |
||||
private AccessUrlHelper $urlHelper, |
||||
private UrlGeneratorInterface $urlGenerator, |
||||
) {} |
||||
|
||||
public function getParams(string $providerName, ?AccessUrl $url = null): array |
||||
{ |
||||
$providers = $this->getProvidersForUrl($url); |
||||
|
||||
if (!isset($providers[$providerName])) { |
||||
throw new InvalidArgumentException('Invalid authentication provider for access URL'); |
||||
} |
||||
|
||||
return $providers[$providerName]; |
||||
} |
||||
|
||||
public function isEnabled(string $methodName, ?AccessUrl $url = null): bool |
||||
{ |
||||
$configParams = $this->getParams($methodName, $url); |
||||
|
||||
return $configParams['enabled'] ?? false; |
||||
} |
||||
|
||||
public function getEnabledProviders(?AccessUrl $url = null): array |
||||
{ |
||||
$urlProviders = $this->getProvidersForUrl($url); |
||||
|
||||
$enabledProviders = []; |
||||
|
||||
foreach ($urlProviders as $providerName => $providerParams) { |
||||
if ($providerParams['enabled'] ?? false) { |
||||
$enabledProviders[] = [ |
||||
'name' => $providerName, |
||||
'title' => $providerParams['title'] ?? u($providerName)->title(), |
||||
'url' => $this->urlGenerator->generate("chamilo.oauth2_{$providerName}_start"), |
||||
]; |
||||
} |
||||
} |
||||
|
||||
return $enabledProviders; |
||||
} |
||||
|
||||
private function getProvidersForUrl(?AccessUrl $url): array |
||||
{ |
||||
$urlId = $url ? $url->getId() : $this->urlHelper->getCurrent()->getId(); |
||||
|
||||
$authentication = $this->parameterBag->get('authentication'); |
||||
|
||||
if (isset($authentication[$urlId])) { |
||||
return $authentication[$urlId]; |
||||
} |
||||
|
||||
if (isset($authentication['default'])) { |
||||
return $authentication['default']; |
||||
} |
||||
|
||||
throw new InvalidArgumentException('Invalid access URL configuration'); |
||||
} |
||||
} |
Loading…
Reference in new issue