Refactor authentication system (#2602)
Make auth configuration available via UI, remove FOSUserBundle and SAML-Bundle dependency
This commit is contained in:
75
src/Saml/Firewall/SamlListener.php
Normal file
75
src/Saml/Firewall/SamlListener.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Saml/SamlAuthFactory.php
Normal file
39
src/Saml/SamlAuthFactory.php
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
20
src/Saml/Token/SamlToken.php
Normal file
20
src/Saml/Token/SamlToken.php
Normal 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;
|
||||
}
|
||||
}
|
||||
16
src/Saml/Token/SamlTokenInterface.php
Normal file
16
src/Saml/Token/SamlTokenInterface.php
Normal 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
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user