Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authentication;
|
||||
|
||||
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\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
class AuthenticationHandler implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
|
||||
{
|
||||
// Remove post_logout if set
|
||||
$request->getSession()->remove('post_logout');
|
||||
|
||||
$format = $request->request->get('format');
|
||||
|
||||
if ('json' == $format) {
|
||||
$array = ['success' => true];
|
||||
$response = new Response(json_encode($array));
|
||||
$response->headers->set('Content-Type', 'application/json');
|
||||
|
||||
return $response;
|
||||
} else {
|
||||
$redirectUrl = $request->getSession()->get('_security.main.target_path', $this->router->generate('mautic_dashboard_index'));
|
||||
|
||||
return new RedirectResponse($redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
// Remove post_logout if set
|
||||
$request->getSession()->remove('post_logout');
|
||||
|
||||
$format = $request->request->get('format');
|
||||
|
||||
if ('json' == $format) {
|
||||
$array = ['success' => false, 'message' => $exception->getMessage()];
|
||||
$response = new Response(json_encode($array));
|
||||
$response->headers->set('Content-Type', 'application/json');
|
||||
|
||||
return $response;
|
||||
} else {
|
||||
$request->getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);
|
||||
|
||||
return new RedirectResponse($this->router->generate('login'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authentication\Token\Permissions;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use FOS\OAuthServerBundle\Model\TokenInterface as OAuthTokenInterface;
|
||||
use FOS\OAuthServerBundle\Security\Authenticator\Token\OAuthToken;
|
||||
use Mautic\ApiBundle\Entity\oAuth2\AccessToken;
|
||||
use Mautic\ApiBundle\Entity\oAuth2\Client;
|
||||
use Mautic\UserBundle\Entity\PermissionRepository;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class TokenPermissions
|
||||
{
|
||||
public function __construct(private TokenStorageInterface $tokenStorage, private PermissionRepository $permissionRepository, private EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active permissions on the current user.
|
||||
*/
|
||||
public function setActivePermissionsOnAuthToken(TokenInterface|OAuthTokenInterface|null $token = null): ?UserInterface
|
||||
{
|
||||
if (null === $token) {
|
||||
$token = $this->tokenStorage->getToken();
|
||||
}
|
||||
|
||||
if (null === $token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $token->getUser();
|
||||
\assert(null === $user || $user instanceof User);
|
||||
|
||||
// If no user is associated with a token, it's a client credentials grant type. Handle accordingly.
|
||||
if (null === $user && ($token instanceof OAuthToken || $token instanceof OAuthTokenInterface)) {
|
||||
$user = $this->assignRoleFromToken($token->getToken());
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$this->setPermissionsOnUser($user);
|
||||
}
|
||||
|
||||
if (null === $user) {
|
||||
throw new \RuntimeException('The user should be either already set in the token, or come from assignRoleFromToken.');
|
||||
}
|
||||
|
||||
$token->setUser($user);
|
||||
|
||||
if ($token instanceof TokenInterface) {
|
||||
$this->tokenStorage->setToken($token);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle permission for Client Credential grant type.
|
||||
*/
|
||||
private function assignRoleFromToken(string $tokenIdentifier): User
|
||||
{
|
||||
/** @var AccessToken|null $accessToken assert ill yield phpstan error. */
|
||||
$accessToken = $this->entityManager->getRepository(AccessToken::class)->findOneBy(['token' => $tokenIdentifier]);
|
||||
|
||||
if (null === $accessToken) {
|
||||
throw new UserNotFoundException('API access token not found.');
|
||||
}
|
||||
|
||||
$client = $accessToken->getClient();
|
||||
if (!$client instanceof Client) {
|
||||
// There are no tests for this part, so an exception will reveal any inconsistencies earlier.
|
||||
throw new \RuntimeException('The client is not a valid API client.');
|
||||
}
|
||||
|
||||
$role = $client->getRole();
|
||||
|
||||
// Create a pseudo user and assign the role
|
||||
$user = new User();
|
||||
$user->setRole($role);
|
||||
|
||||
// Set for the audit log and the entity's "created by user" metadata which takes the first and last name
|
||||
$user->setFirstName($client->getName());
|
||||
$user->setLastName(sprintf('[%s]', $client->getId()));
|
||||
$user->setUsername($user->getName());
|
||||
defined('MAUTIC_AUDITLOG_USER') || define('MAUTIC_AUDITLOG_USER', $user->getName());
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function setPermissionsOnUser(User $user): void
|
||||
{
|
||||
if (!$user->isAdmin() && (null === $user->getActivePermissions() || [] === $user->getActivePermissions())) {
|
||||
$activePermissions = $this->permissionRepository->getPermissionsByRole($user->getRole());
|
||||
|
||||
$user->setActivePermissions($activePermissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authentication\Token;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class PluginToken extends AbstractToken
|
||||
{
|
||||
private ?string $providerKey;
|
||||
|
||||
/**
|
||||
* @param UserInterface|string|null $user
|
||||
* @param array<string> $roles
|
||||
*/
|
||||
public function __construct(
|
||||
?string $providerKey,
|
||||
private ?string $authenticatingService = null,
|
||||
$user = null,
|
||||
private string $credentials = '',
|
||||
array $roles = [],
|
||||
private ?Response $response = null,
|
||||
) {
|
||||
parent::__construct($roles);
|
||||
|
||||
if ('' === $providerKey) {
|
||||
throw new \InvalidArgumentException('$providerKey must not be empty.');
|
||||
}
|
||||
|
||||
if (is_string($user)) {
|
||||
$user = null;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$this->setUser($user);
|
||||
}
|
||||
|
||||
$this->providerKey = $providerKey;
|
||||
}
|
||||
|
||||
public function getCredentials(): string
|
||||
{
|
||||
return $this->credentials;
|
||||
}
|
||||
|
||||
public function getProviderKey(): ?string
|
||||
{
|
||||
return $this->providerKey;
|
||||
}
|
||||
|
||||
public function getAuthenticatingService(): ?string
|
||||
{
|
||||
return $this->authenticatingService;
|
||||
}
|
||||
|
||||
public function getResponse(): ?Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return array_merge([$this->authenticatingService, $this->credentials, $this->providerKey, parent::__serialize()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $data
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
[$this->authenticatingService, $this->credentials, $this->providerKey, $parentArray] = $data;
|
||||
parent::__unserialize($parentArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator;
|
||||
|
||||
use OAuth2\OAuth2ServerException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
|
||||
class Oauth2Authenticator extends \FOS\OAuthServerBundle\Security\Authenticator\Oauth2Authenticator
|
||||
{
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// needed until the oAuth2 library will not be updated to 4.0.5
|
||||
return null !== $this->serverService->getBearerToken($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* A BC compatible response.
|
||||
*/
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$previous = $exception->getPrevious();
|
||||
if ($previous instanceof OAuth2ServerException) {
|
||||
return $previous->getHttpResponse();
|
||||
}
|
||||
|
||||
return parent::onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator\Passport\Badge;
|
||||
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
class PasswordStrengthBadge implements BadgeInterface
|
||||
{
|
||||
private bool $resolved = false;
|
||||
|
||||
public function __construct(private ?string $presentedPassword)
|
||||
{
|
||||
}
|
||||
|
||||
public function getPresentedPassword(): ?string
|
||||
{
|
||||
return $this->presentedPassword;
|
||||
}
|
||||
|
||||
public function setResolved(): void
|
||||
{
|
||||
$this->resolved = true;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return $this->resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator\Passport\Badge;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
class PluginBadge implements BadgeInterface
|
||||
{
|
||||
public function __construct(private ?TokenInterface $preAuthenticatedToken, private ?Response $pluginResponse, private ?string $authenticatingService)
|
||||
{
|
||||
}
|
||||
|
||||
public function getPreAuthenticatedToken(): ?TokenInterface
|
||||
{
|
||||
return $this->preAuthenticatedToken;
|
||||
}
|
||||
|
||||
public function getPluginResponse(): ?Response
|
||||
{
|
||||
return $this->pluginResponse;
|
||||
}
|
||||
|
||||
public function getAuthenticatingService(): ?string
|
||||
{
|
||||
return $this->authenticatingService;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return null !== $this->preAuthenticatedToken || null !== $this->pluginResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator;
|
||||
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Event\AuthenticationEvent;
|
||||
use Mautic\UserBundle\Security\Authentication\AuthenticationHandler;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\Permissions\TokenPermissions;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
|
||||
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PluginBadge;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use OAuth2\OAuth2;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\LazyResponseException;
|
||||
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\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
||||
use Symfony\Component\Security\Http\SecurityEvents;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
final class PluginAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public function __construct(private TokenPermissions $tokenPermissions, private EventDispatcherInterface $dispatcher, private IntegrationHelper $integrationHelper, private UserProviderInterface $userProvider, private AuthenticationHandler $authenticationHandler, private OAuth2 $oAuth2, private LoggerInterface $logger, private string $firewallName)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// Pass to oAuth2 if the token is present, but try to execute in case the Request does not look like oAuth2.
|
||||
return null === $this->oAuth2->getBearerToken($request) ? null : false;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$authenticatingService = $request->get('integration');
|
||||
\assert(null === $authenticatingService || is_string($authenticatingService));
|
||||
$token = new PluginToken($this->firewallName, $authenticatingService);
|
||||
|
||||
$user = null;
|
||||
$response = null;
|
||||
$authenticated = false;
|
||||
$authenticatedToken = null;
|
||||
|
||||
// Try authenticating with a plugin
|
||||
if ($this->dispatcher->hasListeners(UserEvents::USER_PRE_AUTHENTICATION)) {
|
||||
$integrations = $this->integrationHelper->getIntegrationObjects($authenticatingService, ['sso_service'], false, null, true);
|
||||
|
||||
$loginCheck = 'mautic_sso_login_check' === $request->attributes->get('_route');
|
||||
$authEvent = new AuthenticationEvent(
|
||||
null,
|
||||
$token,
|
||||
$this->userProvider,
|
||||
$request,
|
||||
$loginCheck,
|
||||
$authenticatingService,
|
||||
$integrations
|
||||
);
|
||||
$authEvent = $this->dispatcher->dispatch($authEvent, UserEvents::USER_PRE_AUTHENTICATION);
|
||||
\assert($authEvent instanceof AuthenticationEvent);
|
||||
|
||||
if ($authenticated = $authEvent->isAuthenticated()) {
|
||||
$eventToken = $authEvent->getToken();
|
||||
$authenticatingService = $authEvent->getAuthenticatingService();
|
||||
|
||||
// Return passport with the token set in the event, if the event set a different token.
|
||||
if ($eventToken !== $token) {
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($eventToken->getUserIdentifier(), function () use ($eventToken): UserInterface {
|
||||
return $eventToken->getUser();
|
||||
}),
|
||||
[new PluginBadge($eventToken, null, $authenticatingService)]
|
||||
);
|
||||
}
|
||||
|
||||
// Set the user in the token.
|
||||
$user = $authEvent->getUser();
|
||||
|
||||
if (null === $user) {
|
||||
throw new \RuntimeException('User must be set in the authenticated token.');
|
||||
}
|
||||
|
||||
$authenticatedToken = $eventToken;
|
||||
$authenticatedToken->setUser($user);
|
||||
}
|
||||
|
||||
$response = $authEvent->getResponse();
|
||||
|
||||
if (!$authenticated && $loginCheck && null === $response) {
|
||||
// Set an empty JSON response
|
||||
$response = new JsonResponse([]);
|
||||
}
|
||||
}
|
||||
|
||||
// The check is intended to catch: Plugin authenticator must be authenticated and have $user. oAuth should have a response.
|
||||
if (!$user instanceof User && !$authenticated && null === $response) {
|
||||
throw new AuthenticationException('mautic.user.auth.error.invalidlogin');
|
||||
}
|
||||
|
||||
// Otherwise if the plugin authenticator has a response, then pass it to the Symfony.
|
||||
if (null === $user && !$authenticated && null !== $response) {
|
||||
throw new LazyResponseException($response);
|
||||
}
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge(
|
||||
$user instanceof User ? $user->getUserIdentifier() : $user,
|
||||
function (string $userIdentifier) use ($user): UserInterface {
|
||||
if ($user instanceof User) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return $this->userProvider->loadUserByIdentifier($userIdentifier);
|
||||
}
|
||||
),
|
||||
[new PluginBadge($authenticatedToken, $response, $authenticatingService)]
|
||||
);
|
||||
}
|
||||
|
||||
public function createToken(Passport $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
$pluginBadge = $passport->getBadge(PluginBadge::class);
|
||||
\assert($pluginBadge instanceof PluginBadge);
|
||||
$userBadge = $passport->getBadge(UserBadge::class);
|
||||
\assert($userBadge instanceof UserBadge);
|
||||
|
||||
// A custom token has not been set by the plugin, so create a new one. Mainly used for oAuth.
|
||||
if (null === $token = $pluginBadge->getPreAuthenticatedToken()) {
|
||||
$user = $userBadge->getUser();
|
||||
$token = new PluginToken(
|
||||
$this->firewallName,
|
||||
$pluginBadge->getAuthenticatingService(),
|
||||
$user,
|
||||
($user instanceof User) ? $user->getPassword() : '',
|
||||
($user instanceof User) ? $user->getRoles() : [],
|
||||
$pluginBadge->getPluginResponse()
|
||||
);
|
||||
}
|
||||
|
||||
$this->tokenPermissions->setActivePermissionsOnAuthToken($token);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
if (!$token instanceof PluginToken) {
|
||||
// Maybe this need to be replaced with assert, but as no tests cover this, an exception will be noticed earlier.
|
||||
throw new \RuntimeException('Token is not a PluginToken');
|
||||
}
|
||||
|
||||
if ('api' === $this->firewallName) {
|
||||
return $token->getResponse();
|
||||
}
|
||||
|
||||
$this->logger->info(sprintf('User "%s" has been authenticated successfully', $token->getUserIdentifier()));
|
||||
|
||||
$session = $request->getSession();
|
||||
$session->remove(SecurityRequestAttributes::AUTHENTICATION_ERROR);
|
||||
|
||||
$loginEvent = new InteractiveLoginEvent($request, $token);
|
||||
$this->dispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
|
||||
|
||||
$response = null;
|
||||
if (null === $token->getResponse()) {
|
||||
$response = $this->authenticationHandler->onAuthenticationSuccess($request, $token);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$this->logger->info(sprintf('Authentication request failed: %s', $exception->getMessage()));
|
||||
|
||||
// Gets app/bundles/UserBundle/Security/Firewall/AuthenticationListener.php:74 and till the end of the method referenced.
|
||||
if (in_array($this->firewallName, ['api', 'v2api'])) {
|
||||
// Continue with another authentication.
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->authenticationHandler->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator;
|
||||
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Event\AuthenticationEvent;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
/**
|
||||
* This is a modified copy of the \Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator
|
||||
* Replaces \Mautic\UserBundle\Security\Authenticator\FormAuthenticator.
|
||||
*/
|
||||
final class SsoAuthenticator extends AbstractAuthenticator implements InteractiveAuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $options;
|
||||
|
||||
/**
|
||||
* @param array<mixed> $options
|
||||
*/
|
||||
public function __construct(array $options, private HttpUtils $httpUtils, private UserProviderInterface $userProvider, private AuthenticationSuccessHandlerInterface $successHandler, private AuthenticationFailureHandlerInterface $failureHandler, private IntegrationHelper $integrationHelper, private EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
if ([] === $options) {
|
||||
throw new \RuntimeException('$options parameter is empty. Did you forgot to configure?');
|
||||
}
|
||||
|
||||
$this->options = array_merge([
|
||||
'username_parameter' => '_username',
|
||||
'password_parameter' => '_password',
|
||||
'integration_parameter' => 'integration',
|
||||
'post_only' => true,
|
||||
'enable_csrf' => true,
|
||||
'csrf_parameter' => '_csrf_token',
|
||||
'csrf_token_id' => 'authenticate',
|
||||
], $options);
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
if (true === $this->options['post_only'] && !$request->isMethod(Request::METHOD_POST)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $this->options['form_only'] && 'form' !== $request->getContentTypeFormat()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $this->options['post_only']) {
|
||||
return $request->request->has($this->options['integration_parameter']);
|
||||
}
|
||||
|
||||
return $request->query->has($this->options['integration_parameter'])
|
||||
|| $request->request->has($this->options['integration_parameter']);
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$credentials = $this->getCredentials($request);
|
||||
$authenticatingService = $credentials['integration'] ?? null;
|
||||
|
||||
$passport = new Passport(
|
||||
new UserBadge($credentials['username'], function (string $userIdentifier) use ($request, $authenticatingService, $credentials): ?User {
|
||||
/** @var User|null $user */
|
||||
$user = null;
|
||||
|
||||
try {
|
||||
$user = $this->userProvider->loadUserByIdentifier($userIdentifier);
|
||||
} catch (UserNotFoundException) {
|
||||
// Do nothing. Will try to authenticate by username.
|
||||
}
|
||||
|
||||
// Try authenticating with a plugin
|
||||
$integrations = $this->integrationHelper->getIntegrationObjects($authenticatingService, ['sso_form'], false, null, true);
|
||||
|
||||
$token = new PluginToken(
|
||||
null,
|
||||
$authenticatingService,
|
||||
$userIdentifier,
|
||||
null !== $user ? ($credentials['password'] ?? null) : '',
|
||||
null !== $user ? $user->getRoles() : [],
|
||||
);
|
||||
|
||||
$authEvent = new AuthenticationEvent(
|
||||
$user ?? $userIdentifier,
|
||||
$token,
|
||||
$this->userProvider,
|
||||
$request,
|
||||
false,
|
||||
$authenticatingService,
|
||||
$integrations
|
||||
);
|
||||
|
||||
if ($this->dispatcher->hasListeners(UserEvents::USER_FORM_AUTHENTICATION)) {
|
||||
$authEvent = $this->dispatcher->dispatch($authEvent, UserEvents::USER_FORM_AUTHENTICATION);
|
||||
}
|
||||
|
||||
if ($authEvent->isAuthenticated()) {
|
||||
$user = $authEvent->getUser();
|
||||
|
||||
// This line is most likely will never happen. Keep it until this is thoroughly tested manually.
|
||||
if (null !== $user && !$user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
if ($authEvent->isFailed()) {
|
||||
throw new AuthenticationException($authEvent->getFailedAuthenticationMessage());
|
||||
}
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}),
|
||||
new PasswordCredentials($credentials['password'] ?? null),
|
||||
[new RememberMeBadge()]
|
||||
);
|
||||
|
||||
if ($this->options['enable_csrf']) {
|
||||
$passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token']));
|
||||
}
|
||||
|
||||
return $passport;
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return $this->successHandler->onAuthenticationSuccess($request, $token);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
return $this->failureHandler->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getCredentials(Request $request): array
|
||||
{
|
||||
$credentials = [];
|
||||
$credentials['csrf_token'] = $request->get($this->options['csrf_parameter']);
|
||||
|
||||
if ($this->options['post_only']) {
|
||||
$credentials['username'] = $request->request->get($this->options['username_parameter']);
|
||||
$credentials['password'] = $request->request->get($this->options['password_parameter']) ?? '';
|
||||
$credentials['integration'] = $request->request->get($this->options['integration_parameter']);
|
||||
} else {
|
||||
$credentials['username'] = $request->get($this->options['username_parameter']);
|
||||
$credentials['password'] = $request->get($this->options['password_parameter']) ?? '';
|
||||
$credentials['integration'] = $request->get($this->options['integration_parameter']);
|
||||
}
|
||||
|
||||
if (!\is_string($credentials['username'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username'])));
|
||||
}
|
||||
|
||||
$credentials['username'] = trim($credentials['username']);
|
||||
|
||||
if (\strlen($credentials['username']) > UserBadge::MAX_USERNAME_LENGTH) {
|
||||
throw new BadCredentialsException('Invalid username.');
|
||||
}
|
||||
|
||||
if (null !== $credentials['integration'] && !\is_string($credentials['integration'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string or null, "%s" given.', $this->options['integration_parameter'], \gettype($credentials['integration'])));
|
||||
}
|
||||
|
||||
$request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $credentials['username']);
|
||||
|
||||
if (!\is_string($credentials['password'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['password_parameter'], \gettype($credentials['password'])));
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||
|
||||
final class DummyToken extends AbstractToken
|
||||
{
|
||||
public function getCredentials(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\EntryPoint;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
|
||||
class MainEntryPoint implements AuthenticationEntryPointInterface
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $urlGenerator, private bool $samlEnabled)
|
||||
{
|
||||
}
|
||||
|
||||
public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||
{
|
||||
// as per https://docs.mautic.org/en/5.x/authentication/authentication.html#logging-in
|
||||
// log in always as SAML for all requests.
|
||||
// todo: task for testers: enable saml, and check if regular login page is available
|
||||
$route = (string) $request->attributes->get('_route');
|
||||
if ($this->samlEnabled && 'login' !== $route && 'mautic_user_logincheck' !== $route) {
|
||||
// As the system doesn't know the IDP of the service, we can spare one redirect,
|
||||
// and redirect the user straight to discovery.
|
||||
return new RedirectResponse($this->urlGenerator->generate('lightsaml_sp.discovery'));
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('login'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Mautic\UserBundle\Form\Type\PermissionListType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class UserPermissions extends AbstractPermissions
|
||||
{
|
||||
public function __construct($params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
$this->permissions = [
|
||||
'profile' => [
|
||||
'editusername' => 1,
|
||||
'editemail' => 2,
|
||||
'editposition' => 4,
|
||||
'editname' => 8,
|
||||
'full' => 1024,
|
||||
],
|
||||
];
|
||||
$this->addStandardPermissions('users', false);
|
||||
$this->addStandardPermissions('roles', false);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields('user', 'users', $builder, $data, false);
|
||||
$this->addStandardFormFields('user', 'roles', $builder, $data, false);
|
||||
|
||||
$builder->add(
|
||||
'user:profile',
|
||||
PermissionListType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.user.account.permissions.editname' => 'editname',
|
||||
'mautic.user.account.permissions.editusername' => 'editusername',
|
||||
'mautic.user.account.permissions.editemail' => 'editemail',
|
||||
'mautic.user.account.permissions.editposition' => 'editposition',
|
||||
'mautic.user.account.permissions.editall' => 'full',
|
||||
],
|
||||
'label' => 'mautic.user.permissions.profile',
|
||||
'data' => (!empty($data['profile']) ? $data['profile'] : []),
|
||||
'bundle' => 'user',
|
||||
'level' => 'profile',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\Provider;
|
||||
|
||||
use Mautic\CoreBundle\Cache\ResultCacheHelper;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
||||
use Mautic\CoreBundle\Helper\EncryptionHelper;
|
||||
use Mautic\UserBundle\Entity\PermissionRepository;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use Mautic\UserBundle\Event\UserEvent;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
class UserProvider implements UserProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepository $userRepository,
|
||||
protected PermissionRepository $permissionRepository,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected UserPasswordHasher $encoder,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
*/
|
||||
public function loadUserByUsername($username): User
|
||||
{
|
||||
return $this->loadUserByIdentifier($username);
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $identifier): User
|
||||
{
|
||||
$qb = $this->userRepository
|
||||
->createQueryBuilder('u')
|
||||
->select('u, r')
|
||||
->leftJoin('u.role', 'r')
|
||||
->where('u.username = :username OR u.email = :username')
|
||||
->andWhere('u.isPublished = :true')
|
||||
->setParameter('true', true, 'boolean')
|
||||
->setParameter('username', $identifier);
|
||||
$query = $qb->getQuery();
|
||||
ResultCacheHelper::enableOrmQueryCache($query, new ResultCacheOptions(User::CACHE_NAMESPACE, 5 * 60));
|
||||
$user = $query->getOneOrNullResult();
|
||||
|
||||
if (empty($user)) {
|
||||
$message = sprintf(
|
||||
'Unable to find an active admin MauticUserBundle:User object identified by "%s".',
|
||||
$identifier
|
||||
);
|
||||
throw new UserNotFoundException($message, 0);
|
||||
}
|
||||
|
||||
// load permissions
|
||||
if ($user->getId()) {
|
||||
$permissions = $this->permissionRepository->getPermissionsByRole($user->getRole());
|
||||
$user->setActivePermissions($permissions);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
$class = $user::class;
|
||||
if (!$this->supportsClass($class)) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $class));
|
||||
}
|
||||
|
||||
return $this->loadUserByIdentifier($user->getUserIdentifier());
|
||||
}
|
||||
|
||||
public function supportsClass(string $class): bool
|
||||
{
|
||||
return User::class === $class || is_subclass_of($class, User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/update user from authentication plugins.
|
||||
*
|
||||
* @param bool|true $createIfNotExists
|
||||
*
|
||||
* @return User
|
||||
*
|
||||
* @throws BadCredentialsException
|
||||
*/
|
||||
public function saveUser(User $user, $createIfNotExists = true)
|
||||
{
|
||||
$isNew = !$user->getId();
|
||||
|
||||
if ($isNew) {
|
||||
$user = $this->findUser($user);
|
||||
if (!$user->getId() && !$createIfNotExists) {
|
||||
throw new BadCredentialsException();
|
||||
}
|
||||
}
|
||||
|
||||
// Validation for User objects returned by a plugin
|
||||
if (!$user->getRole()) {
|
||||
throw new AuthenticationException('mautic.integration.sso.error.no_role');
|
||||
}
|
||||
|
||||
if (!$user->getUserIdentifier()) {
|
||||
throw new AuthenticationException('mautic.integration.sso.error.no_username');
|
||||
}
|
||||
|
||||
if (!$user->getEmail()) {
|
||||
throw new AuthenticationException('mautic.integration.sso.error.no_email');
|
||||
}
|
||||
|
||||
if (!$user->getFirstName() || !$user->getLastName()) {
|
||||
throw new AuthenticationException('mautic.integration.sso.error.no_name');
|
||||
}
|
||||
|
||||
// Check for plain password
|
||||
$plainPassword = $user->getPlainPassword();
|
||||
if ($plainPassword) {
|
||||
// Encode plain text
|
||||
$user->setPassword(
|
||||
$this->encoder->hashPassword($user, $plainPassword)
|
||||
);
|
||||
} elseif (!$password = $user->getPassword()) {
|
||||
// Generate and encode a random password
|
||||
$user->setPassword(
|
||||
$this->encoder->hashPassword($user, EncryptionHelper::generateKey())
|
||||
);
|
||||
}
|
||||
|
||||
$event = new UserEvent($user, $isNew);
|
||||
|
||||
if ($this->dispatcher->hasListeners(UserEvents::USER_PRE_SAVE)) {
|
||||
$event = $this->dispatcher->dispatch($event, UserEvents::USER_PRE_SAVE);
|
||||
}
|
||||
|
||||
$this->userRepository->saveEntity($user);
|
||||
|
||||
if ($this->dispatcher->hasListeners(UserEvents::USER_POST_SAVE)) {
|
||||
$this->dispatcher->dispatch($event, UserEvents::USER_POST_SAVE);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function findUser(User $user)
|
||||
{
|
||||
try {
|
||||
// Try by username
|
||||
$user = $this->loadUserByIdentifier($user->getUserIdentifier());
|
||||
|
||||
return $user;
|
||||
} catch (UserNotFoundException) {
|
||||
// Try by email
|
||||
try {
|
||||
return $this->loadUserByIdentifier($user->getEmail());
|
||||
} catch (UserNotFoundException) {
|
||||
}
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML;
|
||||
|
||||
use LightSaml\Builder\EntityDescriptor\SimpleEntityDescriptorBuilder;
|
||||
use LightSaml\Credential\X509Credential;
|
||||
use LightSaml\Store\Credential\CredentialStoreInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class EntityDescriptorProviderFactory
|
||||
{
|
||||
public static function build(
|
||||
string $ownEntityId,
|
||||
RouterInterface $router,
|
||||
?string $routeName,
|
||||
CredentialStoreInterface $ownCredentialStore,
|
||||
): SimpleEntityDescriptorBuilder {
|
||||
/** @var X509Credential[] $arrOwnCredentials */
|
||||
$arrOwnCredentials = $ownCredentialStore->getByEntityId($ownEntityId);
|
||||
$route = $routeName ? $router->generate($routeName, [], RouterInterface::ABSOLUTE_PATH) : '';
|
||||
|
||||
return new SimpleEntityDescriptorBuilder(
|
||||
$ownEntityId,
|
||||
$route ? sprintf('%s%s', $ownEntityId, $route) : '',
|
||||
'',
|
||||
$arrOwnCredentials[0]->getCertificate()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class Helper
|
||||
{
|
||||
public function __construct(private CoreParametersHelper $coreParametersHelper, private RequestStack $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function isSamlSession(): bool
|
||||
{
|
||||
return $this->isSamlEnabled() && $this->request->getSession()->has('samlsso');
|
||||
}
|
||||
|
||||
public function isSamlEnabled(): bool
|
||||
{
|
||||
return (bool) $this->coreParametersHelper->get('saml_idp_metadata');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML\Store;
|
||||
|
||||
use LightSaml\Credential\KeyHelper;
|
||||
use LightSaml\Credential\X509Certificate;
|
||||
use LightSaml\Credential\X509Credential;
|
||||
use LightSaml\Store\Credential\CredentialStoreInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use RobRichards\XMLSecLibs\XMLSecurityKey;
|
||||
|
||||
class CredentialsStore implements CredentialStoreInterface
|
||||
{
|
||||
private ?X509Credential $credentials = null;
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private string $entityId,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getByEntityId($entityId): array
|
||||
{
|
||||
// EntityIds do not match
|
||||
if ($entityId !== $this->entityId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$this->credentials) {
|
||||
$this->delegateAndCreateCredentials();
|
||||
}
|
||||
|
||||
return [$this->credentials];
|
||||
}
|
||||
|
||||
private function delegateAndCreateCredentials(): void
|
||||
{
|
||||
// Credentials are required or SP will cause a never ending login loop as it throws an exception
|
||||
$samlEnabled = (bool) $this->coreParametersHelper->get('saml_idp_metadata');
|
||||
|
||||
if (!$samlEnabled || !$certificateContent = $this->coreParametersHelper->get('saml_idp_own_certificate')) {
|
||||
$this->credentials = $this->createDefaultCredentials();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->credentials = $this->createOwnCredentials();
|
||||
}
|
||||
|
||||
private function createOwnCredentials(): X509Credential
|
||||
{
|
||||
$certificateContent = base64_decode($this->coreParametersHelper->get('saml_idp_own_certificate'));
|
||||
$privateKeyContent = base64_decode($this->coreParametersHelper->get('saml_idp_own_private_key'));
|
||||
$keyPassword = (string) $this->coreParametersHelper->get('saml_idp_own_password');
|
||||
|
||||
return $this->createCredentials($certificateContent, $privateKeyContent, $keyPassword);
|
||||
}
|
||||
|
||||
private function createDefaultCredentials(): X509Credential
|
||||
{
|
||||
$cache_dir = $this->coreParametersHelper->get('cache_path');
|
||||
$keyPassword = '';
|
||||
|
||||
if (!file_exists($cache_dir.'/saml_default.key') || !file_exists($cache_dir.'/saml_default.crt')) {
|
||||
$dn = ['commonName' => 'Mautic dummy cert'];
|
||||
|
||||
// Generate a new private (and public) key pair
|
||||
$privkey = openssl_pkey_new([
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
]);
|
||||
|
||||
// Generate a certificate signing request
|
||||
$csr = openssl_csr_new($dn, $privkey, ['digest_alg' => 'sha256']);
|
||||
|
||||
// Generate a self-signed cert, valid for 365 days
|
||||
$x509 = openssl_csr_sign($csr, null, $privkey, $days=365, ['digest_alg' => 'sha256']);
|
||||
|
||||
openssl_x509_export_to_file($x509, $cache_dir.'/saml_default.crt');
|
||||
openssl_pkey_export_to_file($privkey, $cache_dir.'/saml_default.key', $keyPassword);
|
||||
}
|
||||
|
||||
$cert = file_get_contents($cache_dir.'/saml_default.crt');
|
||||
$privateKey = file_get_contents($cache_dir.'/saml_default.key');
|
||||
|
||||
return $this->createCredentials($cert, $privateKey, $keyPassword);
|
||||
}
|
||||
|
||||
private function createCertificate(string $certificateContent): X509Certificate
|
||||
{
|
||||
$certificate = new X509Certificate();
|
||||
$certificate->loadPem($certificateContent);
|
||||
|
||||
return $certificate;
|
||||
}
|
||||
|
||||
private function createPrivateKey(string $privateKeyContent, string $keyPassword, X509Certificate $certificate): XMLSecurityKey
|
||||
{
|
||||
return KeyHelper::createPrivateKey($privateKeyContent, $keyPassword, false, $certificate->getSignatureAlgorithm());
|
||||
}
|
||||
|
||||
private function createCredentials(string $certificateContent, string $privateKeyContent, string $keyPassword): X509Credential
|
||||
{
|
||||
$certificate = $this->createCertificate($certificateContent);
|
||||
$privateKey = $this->createPrivateKey($privateKeyContent, $keyPassword, $certificate);
|
||||
|
||||
$credentials = new X509Credential($certificate, $privateKey);
|
||||
$credentials->setEntityId($this->entityId);
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML\Store;
|
||||
|
||||
use LightSaml\Model\Metadata\EntityDescriptor;
|
||||
use LightSaml\Store\EntityDescriptor\EntityDescriptorStoreInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
class EntityDescriptorStore implements EntityDescriptorStoreInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityDescriptor
|
||||
*/
|
||||
private $entityDescriptor;
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function get($entityId): ?EntityDescriptor
|
||||
{
|
||||
if ($this->entityDescriptor) {
|
||||
return $this->entityDescriptor;
|
||||
}
|
||||
|
||||
$this->createEntityDescriptor();
|
||||
|
||||
if ($entityId !== $this->entityDescriptor->getEntityID()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityDescriptor;
|
||||
}
|
||||
|
||||
public function has($entityId): bool
|
||||
{
|
||||
// SAML is not enabled
|
||||
if (!$this->coreParametersHelper->get('saml_idp_metadata')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entityDescriptor = $this->get($entityId);
|
||||
|
||||
// EntityIds do not match
|
||||
if (!$entityDescriptor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|EntityDescriptor[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
if (!$this->entityDescriptor) {
|
||||
$this->createEntityDescriptor();
|
||||
}
|
||||
|
||||
return [$this->entityDescriptor];
|
||||
}
|
||||
|
||||
private function createEntityDescriptor(): void
|
||||
{
|
||||
$xml = base64_decode($this->coreParametersHelper->get('saml_idp_metadata'));
|
||||
|
||||
$this->entityDescriptor = EntityDescriptor::loadXml($xml);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML\Store;
|
||||
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use LightSaml\Provider\TimeProvider\TimeProviderInterface;
|
||||
use LightSaml\Store\Id\IdStoreInterface;
|
||||
use Mautic\UserBundle\Entity\IdEntry;
|
||||
|
||||
class IdStore implements IdStoreInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ObjectManager $manager,
|
||||
private TimeProviderInterface $timeProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityId
|
||||
* @param string $id
|
||||
*/
|
||||
public function set($entityId, $id, \DateTime $expiryTime): void
|
||||
{
|
||||
$idEntry = $this->manager->find(IdEntry::class, ['entityId' => $entityId, 'id' => $id]);
|
||||
if (null == $idEntry) {
|
||||
$idEntry = new IdEntry();
|
||||
}
|
||||
$idEntry->setEntityId($entityId)
|
||||
->setId($id)
|
||||
->setExpiryTime($expiryTime);
|
||||
$this->manager->persist($idEntry);
|
||||
$this->manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityId
|
||||
* @param string $id
|
||||
*/
|
||||
public function has($entityId, $id): bool
|
||||
{
|
||||
/** @var IdEntry $idEntry */
|
||||
$idEntry = $this->manager->find(IdEntry::class, ['entityId' => $entityId, 'id' => $id]);
|
||||
if (null == $idEntry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($idEntry->getExpiryTime()->getTimestamp() < $this->timeProvider->getTimestamp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML\Store\Request;
|
||||
|
||||
use LightSaml\State\Request\RequestState;
|
||||
use LightSaml\Store\Request\AbstractRequestStateArrayStore;
|
||||
use Mautic\CacheBundle\Cache\CacheProviderInterface;
|
||||
|
||||
class RequestStateStore extends AbstractRequestStateArrayStore
|
||||
{
|
||||
private string $prefix;
|
||||
|
||||
public function __construct(private CacheProviderInterface $cacheProvider, string $prefix, string $suffix)
|
||||
{
|
||||
$this->prefix = $prefix.$suffix;
|
||||
}
|
||||
|
||||
public function set(RequestState $state): self
|
||||
{
|
||||
$id = $state->getId();
|
||||
|
||||
$item = $this->cacheProvider->getItem($this->prefix.$id);
|
||||
$item->expiresAfter(2 * 60); // The login is valid for 2 minutes.
|
||||
|
||||
$item->set($state);
|
||||
$this->cacheProvider->save($item);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get($id): ?RequestState
|
||||
{
|
||||
$item = $this->cacheProvider->getItem($this->prefix.$id);
|
||||
|
||||
if (!$item->isHit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$state = $item->get();
|
||||
if (!$state instanceof RequestState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
public function remove($id): bool
|
||||
{
|
||||
return $this->cacheProvider->deleteItem($this->prefix.$id);
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->cacheProvider->clear($this->prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
protected function getArray(): array
|
||||
{
|
||||
throw new \LogicException('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $arr
|
||||
*/
|
||||
protected function setArray(array $arr): AbstractRequestStateArrayStore
|
||||
{
|
||||
throw new \LogicException('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML\Store;
|
||||
|
||||
use LightSaml\Meta\TrustOptions\TrustOptions;
|
||||
use LightSaml\Store\TrustOptions\TrustOptionsStoreInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
class TrustOptionsStore implements TrustOptionsStoreInterface
|
||||
{
|
||||
private ?TrustOptions $trustOptions = null;
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private string $entityId,
|
||||
) {
|
||||
}
|
||||
|
||||
public function get($entityId): TrustOptions
|
||||
{
|
||||
if ($this->trustOptions) {
|
||||
return $this->trustOptions;
|
||||
}
|
||||
|
||||
return $this->createTrustOptions();
|
||||
}
|
||||
|
||||
public function has($entityId): bool
|
||||
{
|
||||
// SAML is not enabled
|
||||
if (!$this->coreParametersHelper->get('saml_idp_metadata')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// EntityIds do not match
|
||||
if ($entityId !== $this->entityId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTrustOptions(): TrustOptions
|
||||
{
|
||||
$this->trustOptions = $trustOptions = new TrustOptions();
|
||||
|
||||
if (!$this->coreParametersHelper->get('saml_idp_own_certificate')) {
|
||||
return $trustOptions;
|
||||
}
|
||||
|
||||
$trustOptions->setSignAuthnRequest(true);
|
||||
$trustOptions->setEncryptAssertions(true);
|
||||
$trustOptions->setEncryptAuthnRequest(true);
|
||||
$trustOptions->setSignAssertions(true);
|
||||
$trustOptions->setSignResponse(true);
|
||||
|
||||
return $trustOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML\User;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use LightSaml\Model\Protocol\Response;
|
||||
use LightSaml\SpBundle\Security\User\UserCreatorInterface;
|
||||
use Mautic\CoreBundle\Helper\EncryptionHelper;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class UserCreator implements UserCreatorInterface
|
||||
{
|
||||
private int $defaultRole;
|
||||
|
||||
private array $requiredFields = [
|
||||
'username',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'email',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UserMapper $userMapper,
|
||||
private UserModel $userModel,
|
||||
private UserPasswordHasher $hasher,
|
||||
$defaultRole,
|
||||
) {
|
||||
$this->defaultRole = (int) $defaultRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserInterface|null
|
||||
*/
|
||||
public function createUser(Response $response): User
|
||||
{
|
||||
if (empty($this->defaultRole)) {
|
||||
throw new BadCredentialsException('User does not exist.');
|
||||
}
|
||||
|
||||
/** @var Role $defaultRole */
|
||||
$defaultRole = $this->entityManager->getReference(Role::class, $this->defaultRole);
|
||||
|
||||
$user = $this->userMapper->getUser($response);
|
||||
$user->setPassword($this->userModel->checkNewPassword($user, $this->hasher, EncryptionHelper::generateKey()));
|
||||
$user->setRole($defaultRole);
|
||||
|
||||
$this->validateUser($user);
|
||||
|
||||
$this->userModel->saveEntity($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadCredentialsException
|
||||
*/
|
||||
private function validateUser(User $user): void
|
||||
{
|
||||
// Validate that the user has all that's required
|
||||
foreach ($this->requiredFields as $field) {
|
||||
$getter = 'get'.ucfirst($field);
|
||||
|
||||
if (!$user->$getter()) {
|
||||
throw new BadCredentialsException('User does not include required fields.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\SAML\User;
|
||||
|
||||
use LightSaml\Model\Assertion\Assertion;
|
||||
use LightSaml\Model\Protocol\Response;
|
||||
use LightSaml\SpBundle\Security\User\UsernameMapperInterface;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
|
||||
class UserMapper implements UsernameMapperInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function __construct(
|
||||
private array $attributes,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUser(Response $response): User
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
foreach ($response->getAllAssertions() as $assertion) {
|
||||
$this->setValuesFromAssertion($assertion, $user);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getUsername(Response $response): ?string
|
||||
{
|
||||
$user = $this->getUser($response);
|
||||
|
||||
return $user->getUserIdentifier();
|
||||
}
|
||||
|
||||
private function setValuesFromAssertion(Assertion $assertion, User $user): void
|
||||
{
|
||||
$attributes = $this->extractAttributes($assertion);
|
||||
|
||||
// use email as the user by default
|
||||
if (isset($attributes['email'])) {
|
||||
$user->setEmail($attributes['email']);
|
||||
$user->setUsername($attributes['email']);
|
||||
}
|
||||
|
||||
if (isset($attributes['username']) && !empty($attributes['username'])) {
|
||||
$user->setUsername($attributes['username']);
|
||||
}
|
||||
|
||||
if (isset($attributes['firstname'])) {
|
||||
$user->setFirstname($attributes['firstname']);
|
||||
}
|
||||
|
||||
if (isset($attributes['lastname'])) {
|
||||
$user->setLastName($attributes['lastname']);
|
||||
}
|
||||
}
|
||||
|
||||
private function extractAttributes(Assertion $assertion): array
|
||||
{
|
||||
$attributes = [];
|
||||
|
||||
foreach ($this->attributes as $key => $attributeName) {
|
||||
if (!$attributeName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($assertion->getAllAttributeStatements() as $attributeStatement) {
|
||||
$attribute = $attributeStatement->getFirstAttributeByName($attributeName);
|
||||
if ($attribute && $attribute->getFirstAttributeValue()) {
|
||||
$attributes[$key] = $attribute->getFirstAttributeValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security;
|
||||
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
class TimingSafeFormLoginAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $options;
|
||||
|
||||
/**
|
||||
* @param array<mixed> $options
|
||||
*/
|
||||
public function __construct(private FormLoginAuthenticator $authenticator, private UserProviderInterface $userProvider, private PasswordHasherFactoryInterface $passwordHasherFactory, array $options)
|
||||
{
|
||||
$this->authenticator = $authenticator;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->passwordHasherFactory = $passwordHasherFactory;
|
||||
$this->options = array_merge([
|
||||
'username_parameter' => '_username',
|
||||
'password_parameter' => '_password',
|
||||
'check_path' => '/login_check',
|
||||
'post_only' => true,
|
||||
'form_only' => false,
|
||||
'enable_csrf' => false,
|
||||
'csrf_parameter' => '_csrf_token',
|
||||
'csrf_token_id' => 'authenticate',
|
||||
], $options);
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $this->authenticator->supports($request);
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$credentials = $this->getCredentials($request);
|
||||
$passwordHasherFactory = $this->passwordHasherFactory;
|
||||
$userLoader = function (string $identifier) use ($passwordHasherFactory, $credentials): UserInterface {
|
||||
try {
|
||||
// Attempt to load the real user.
|
||||
return $this->userProvider->loadUserByIdentifier($identifier);
|
||||
} catch (UserNotFoundException $e) {
|
||||
// If real user is not found, provide a dummy user and still 'check' the credentials to prevent
|
||||
// user enumeration via response timing comparison.
|
||||
// We check it against a pre-calculated hash so the verify functions take roughly
|
||||
// the same amount of time, and we pass the actual entered password so the response
|
||||
// timing varies with the given password the same way it does for existing users.
|
||||
$user = new User();
|
||||
$passwordHasherFactory->getPasswordHasher($user)->verify('$2y$13$aAwXNyqA87lcXQQuk8Cp6eo2amRywLct29oG2uWZ8lYBeamFZ8UhK', $credentials['password']);
|
||||
// Rethrow exception
|
||||
throw $e;
|
||||
}
|
||||
};
|
||||
|
||||
$userBadge = new UserBadge($credentials['username'], $userLoader);
|
||||
$passport = new Passport($userBadge, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]);
|
||||
|
||||
if ($this->options['enable_csrf']) {
|
||||
$passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token']));
|
||||
}
|
||||
|
||||
if ($this->userProvider instanceof PasswordUpgraderInterface) {
|
||||
$passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
|
||||
}
|
||||
|
||||
return $passport;
|
||||
}
|
||||
|
||||
public function createToken(Passport $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
return $this->authenticator->createToken($passport, $firewallName);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
return $this->authenticator->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
|
||||
public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||
{
|
||||
return $this->authenticator->start($request, $authException);
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return $this->authenticator->isInteractive();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function getCredentials(Request $request): array
|
||||
{
|
||||
$credentials = [];
|
||||
$credentials['csrf_token'] = $request->get($this->options['csrf_parameter']);
|
||||
|
||||
if ($this->options['post_only']) {
|
||||
$credentials['username'] = $request->request->get($this->options['username_parameter']);
|
||||
$credentials['password'] = $request->request->get($this->options['password_parameter'], '');
|
||||
} else {
|
||||
$credentials['username'] = $request->get($this->options['username_parameter']);
|
||||
$credentials['password'] = $request->get($this->options['password_parameter'], '');
|
||||
}
|
||||
|
||||
if (!\is_string($credentials['username']) && !$credentials['username'] instanceof \Stringable) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username'])));
|
||||
}
|
||||
|
||||
$credentials['username'] = trim($credentials['username']);
|
||||
|
||||
$request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $credentials['username']);
|
||||
|
||||
if (!\is_string($credentials['password']) && (!\is_object($credentials['password']) || !method_exists($credentials['password'], '__toString'))) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['password_parameter'], \gettype($credentials['password'])));
|
||||
}
|
||||
|
||||
if (!\is_string($credentials['csrf_token'] ?? '') && (!\is_object($credentials['csrf_token']) || !method_exists($credentials['csrf_token'], '__toString'))) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['csrf_parameter'], \gettype($credentials['csrf_token'])));
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security;
|
||||
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
final class UserTokenSetter implements UserTokenSetterInterface
|
||||
{
|
||||
public function __construct(private UserModel $userModel, private TokenStorageInterface $tokenStorage)
|
||||
{
|
||||
}
|
||||
|
||||
public function setUser(int $userId): void
|
||||
{
|
||||
$user = $this->userModel->getEntity($userId);
|
||||
$token = $this->tokenStorage->getToken() ?? new DummyToken();
|
||||
|
||||
$token->setUser($user);
|
||||
$this->tokenStorage->setToken($token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security;
|
||||
|
||||
interface UserTokenSetterInterface
|
||||
{
|
||||
public function setUser(int $userId): void;
|
||||
}
|
||||
Reference in New Issue
Block a user