Refactor authentication system (#2602)

Make auth configuration available via UI, remove FOSUserBundle and SAML-Bundle dependency
This commit is contained in:
Kevin Papst
2021-06-10 15:34:13 +02:00
committed by GitHub
parent 286b63e2c8
commit 7f20cb045c
155 changed files with 5590 additions and 1802 deletions

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Saml\Firewall;
use App\Saml\SamlAuthFactory;
use App\Saml\Token\SamlToken;
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\Http\Firewall\AbstractAuthenticationListener;
class SamlListener extends AbstractAuthenticationListener
{
/**
* @var SamlAuthFactory
*/
protected $authFactory;
public function setAuth(SamlAuthFactory $authFactory): void
{
$this->authFactory = $authFactory;
}
/**
* Performs authentication.
*
* @param Request $request A Request instance
* @return TokenInterface|Response|null The authenticated token, null if full authentication is not possible, or a Response
*
* @throws AuthenticationException if the authentication fails
* @throws \Exception if attribute set by "username_attribute" option not found
*/
protected function attemptAuthentication(Request $request)
{
$oneLoginAuth = $this->authFactory->create();
$oneLoginAuth->processResponse();
if ($oneLoginAuth->getErrors()) {
$this->logger->error($oneLoginAuth->getLastErrorReason());
throw new AuthenticationException($oneLoginAuth->getLastErrorReason());
}
$attributes = [];
if (isset($this->options['use_attribute_friendly_name']) && $this->options['use_attribute_friendly_name']) {
$attributes = $oneLoginAuth->getAttributesWithFriendlyName();
} else {
$attributes = $oneLoginAuth->getAttributes();
}
$attributes['sessionIndex'] = $oneLoginAuth->getSessionIndex();
$token = new SamlToken();
$token->setAttributes($attributes);
if (isset($this->options['username_attribute'])) {
if (!\array_key_exists($this->options['username_attribute'], $attributes)) {
$this->logger->error(sprintf('Found attributes: %s', print_r($attributes, true)));
throw new \Exception(sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute']));
}
$username = $attributes[$this->options['username_attribute']][0];
} else {
$username = $oneLoginAuth->getNameId();
}
$token->setUser($username);
return $this->authenticationManager->authenticate($token);
}
}

View File

@@ -9,8 +9,8 @@
namespace App\Saml\Logout;
use App\Saml\SamlAuth;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
use App\Saml\SamlAuthFactory;
use App\Saml\Token\SamlTokenInterface;
use OneLogin\Saml2\Error;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -20,11 +20,11 @@ use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
final class SamlLogoutHandler implements LogoutHandlerInterface
{
/**
* @var SamlAuth
* @var SamlAuthFactory
*/
private $samlAuth;
public function __construct(SamlAuth $samlAuth)
public function __construct(SamlAuthFactory $samlAuth)
{
$this->samlAuth = $samlAuth;
}
@@ -44,12 +44,14 @@ final class SamlLogoutHandler implements LogoutHandlerInterface
return;
}
$samlAuth = $this->samlAuth->create();
try {
$this->samlAuth->processSLO();
$samlAuth->processSLO();
} catch (Error $e) {
if (!empty($this->samlAuth->getSLOurl())) {
if (!empty($samlAuth->getSLOurl())) {
$sessionIndex = $token->hasAttribute('sessionIndex') ? $token->getAttribute('sessionIndex') : null;
$this->samlAuth->logout(null, [], $token->getUsername(), $sessionIndex);
$samlAuth->logout(null, [], $token->getUsername(), $sessionIndex);
}
}
}

View File

@@ -9,11 +9,12 @@
namespace App\Saml\Provider;
use App\Configuration\SystemConfiguration;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Saml\SamlTokenFactory;
use App\Saml\Token\SamlTokenInterface;
use App\Saml\User\SamlUserFactory;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
@@ -22,29 +23,19 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
final class SamlProvider implements AuthenticationProviderInterface
{
/**
* @var UserProviderInterface
*/
private $userProvider;
/**
* @var SamlUserFactory
*/
private $userFactory;
/**
* @var SamlTokenFactory
*/
private $tokenFactory;
/**
* @var UserRepository
*/
private $repository;
private $configuration;
public function __construct(UserRepository $repository, UserProviderInterface $userProvider, SamlTokenFactory $tokenFactory, SamlUserFactory $userFactory)
public function __construct(UserRepository $repository, UserProviderInterface $userProvider, SamlTokenFactory $tokenFactory, SamlUserFactory $userFactory, SystemConfiguration $configuration)
{
$this->repository = $repository;
$this->userProvider = $userProvider;
$this->tokenFactory = $tokenFactory;
$this->userFactory = $userFactory;
$this->configuration = $configuration;
}
/**
@@ -83,6 +74,10 @@ final class SamlProvider implements AuthenticationProviderInterface
public function supports(TokenInterface $token)
{
if (!$this->configuration->isSamlActive()) {
return false;
}
return $token instanceof SamlTokenInterface;
}
}

View File

@@ -1,26 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Saml;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Utils;
use Symfony\Component\HttpFoundation\RequestStack;
class SamlAuth extends Auth
{
public function __construct(RequestStack $request, array $settings = null)
{
parent::__construct($settings);
if (null !== $request->getMasterRequest() && $request->getMasterRequest()->isFromTrustedProxy()) {
Utils::setProxyVars(true);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Saml;
use App\Configuration\SamlConfiguration;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Utils;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @final
*/
class SamlAuthFactory
{
private $request;
private $configuration;
public function __construct(RequestStack $request, SamlConfiguration $configuration)
{
$this->request = $request;
$this->configuration = $configuration;
}
public function create(): Auth
{
if (null !== $this->request->getMasterRequest() && $this->request->getMasterRequest()->isFromTrustedProxy()) {
Utils::setProxyVars(true);
}
return new Auth($this->configuration->getConnection());
}
}

View File

@@ -9,15 +9,11 @@
namespace App\Saml;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenFactoryInterface;
use App\Saml\Token\SamlToken;
final class SamlTokenFactory implements SamlTokenFactoryInterface
final class SamlTokenFactory
{
/**
* {@inheritdoc}
*/
public function createToken($user, array $attributes, array $roles)
public function createToken($user, array $attributes, array $roles): SamlToken
{
$token = new SamlToken($roles);
$token->setUser($user);

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Saml\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class SamlToken extends AbstractToken implements SamlTokenInterface
{
public function getCredentials()
{
return null;
}
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Saml\Token;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
interface SamlTokenInterface extends TokenInterface
{
}

View File

@@ -9,38 +9,22 @@
namespace App\Saml\User;
use App\Configuration\SamlConfiguration;
use App\Entity\User;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface;
use App\Saml\Token\SamlTokenInterface;
final class SamlUserFactory implements SamlUserFactoryInterface
final class SamlUserFactory
{
/**
* @var array
*/
private $mapping;
/**
* @var string
*/
private $groupAttribute;
/**
* @var array
*/
private $groupMapping;
private $configuration;
public function __construct(array $attributes)
public function __construct(SamlConfiguration $configuration)
{
$this->mapping = $attributes['mapping'];
$this->groupAttribute = $attributes['roles']['attribute'];
$this->groupMapping = $attributes['roles']['mapping'];
$this->configuration = $configuration;
}
/**
* @param SamlTokenInterface $token
* @return User
*/
public function createUser(SamlTokenInterface $token)
public function createUser(SamlTokenInterface $token): User
{
// Not using UserService: user settings should be set via SAML attributes
$user = new User();
$user->setEnabled(true);
$user->setUsername($token->getUsername());
@@ -52,17 +36,20 @@ final class SamlUserFactory implements SamlUserFactoryInterface
public function hydrateUser(User $user, SamlTokenInterface $token): void
{
$groupAttribute = $this->configuration->getRolesAttribute();
$groupMapping = $this->configuration->getRolesMapping();
// extract user roles from a special saml attribute
if (!empty($this->groupAttribute) && $token->hasAttribute($this->groupAttribute)) {
if (!empty($groupAttribute) && $token->hasAttribute($groupAttribute)) {
$groupMap = [];
foreach ($this->groupMapping as $mapping) {
foreach ($groupMapping as $mapping) {
$field = $mapping['kimai'];
$attribute = $mapping['saml'];
$groupMap[$attribute] = $field;
}
$roles = [];
$samlGroups = $token->getAttribute($this->groupAttribute);
$samlGroups = $token->getAttribute($groupAttribute);
foreach ($samlGroups as $groupName) {
if (\array_key_exists($groupName, $groupMap)) {
$roles[] = $groupMap[$groupName];
@@ -71,7 +58,9 @@ final class SamlUserFactory implements SamlUserFactoryInterface
$user->setRoles($roles);
}
foreach ($this->mapping as $mapping) {
$mappingConfig = $this->configuration->getAttributeMapping();
foreach ($mappingConfig as $mapping) {
$field = $mapping['kimai'];
$attribute = $mapping['saml'];
$value = $this->getPropertyValue($token, $attribute);