added support for saml login (#1408)
This commit is contained in:
86
src/Saml/Controller/SamlController.php
Normal file
86
src/Saml/Controller/SamlController.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Saml\SamlAuth;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* @Route(path="/saml")
|
||||
*/
|
||||
final class SamlController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var SamlAuth
|
||||
*/
|
||||
private $oneLoginAuth;
|
||||
|
||||
public function __construct(SamlAuth $oneLoginAuth)
|
||||
{
|
||||
$this->oneLoginAuth = $oneLoginAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login", name="saml_login")
|
||||
*/
|
||||
public function loginAction(Request $request)
|
||||
{
|
||||
$session = $request->getSession();
|
||||
$authErrorKey = Security::AUTHENTICATION_ERROR;
|
||||
|
||||
if ($request->attributes->has($authErrorKey)) {
|
||||
$error = $request->attributes->get($authErrorKey);
|
||||
} elseif (null !== $session && $session->has($authErrorKey)) {
|
||||
$error = $session->get($authErrorKey);
|
||||
$session->remove($authErrorKey);
|
||||
} else {
|
||||
$error = null;
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
throw new \RuntimeException($error->getMessage());
|
||||
}
|
||||
|
||||
$this->oneLoginAuth->login($session->get('_security.main.target_path'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/metadata", name="saml_metadata")
|
||||
*/
|
||||
public function metadataAction()
|
||||
{
|
||||
$metadata = $this->oneLoginAuth->getSettings()->getSPMetadata();
|
||||
|
||||
$response = new Response($metadata);
|
||||
$response->headers->set('Content-Type', 'xml');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/acs", name="saml_acs")
|
||||
*/
|
||||
public function assertionConsumerServiceAction()
|
||||
{
|
||||
throw new \RuntimeException('You must configure the check path in your firewall.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/logout", name="saml_logout")
|
||||
*/
|
||||
public function logoutAction()
|
||||
{
|
||||
throw new \RuntimeException('You must configure the logout path in your firewall.');
|
||||
}
|
||||
}
|
||||
56
src/Saml/Logout/SamlLogoutHandler.php
Normal file
56
src/Saml/Logout/SamlLogoutHandler.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\Logout;
|
||||
|
||||
use App\Saml\SamlAuth;
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
|
||||
use OneLogin\Saml2\Error;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
|
||||
|
||||
final class SamlLogoutHandler implements LogoutHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var SamlAuth
|
||||
*/
|
||||
private $samlAuth;
|
||||
|
||||
public function __construct(SamlAuth $samlAuth)
|
||||
{
|
||||
$this->samlAuth = $samlAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the LogoutListener when a user has requested
|
||||
* to be logged out. Usually, you would unset session variables, or remove
|
||||
* cookies, etc.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param TokenInterface $token
|
||||
*/
|
||||
public function logout(Request $request, Response $response, TokenInterface $token)
|
||||
{
|
||||
if (!$token instanceof SamlTokenInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->samlAuth->processSLO();
|
||||
} catch (Error $e) {
|
||||
if (!empty($this->samlAuth->getSLOurl())) {
|
||||
$sessionIndex = $token->hasAttribute('sessionIndex') ? $token->getAttribute('sessionIndex') : null;
|
||||
$this->samlAuth->logout(null, [], $token->getUsername(), $sessionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Saml/Provider/SamlProvider.php
Normal file
90
src/Saml/Provider/SamlProvider.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Provider;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use App\Saml\SamlTokenFactory;
|
||||
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;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
||||
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;
|
||||
|
||||
public function __construct(UserRepository $repository, UserProviderInterface $userProvider, SamlTokenFactory $tokenFactory, SamlUserFactory $userFactory)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->tokenFactory = $tokenFactory;
|
||||
$this->userFactory = $userFactory;
|
||||
}
|
||||
|
||||
public function authenticate(TokenInterface $token)
|
||||
{
|
||||
$user = null;
|
||||
|
||||
/** @var ChainUserProvider $p */
|
||||
$p = $this->userProvider;
|
||||
|
||||
try {
|
||||
$user = $this->userProvider->loadUserByUsername($token->getUsername());
|
||||
} catch (UsernameNotFoundException $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
if (null === $user) {
|
||||
$user = $this->userFactory->createUser($token);
|
||||
} else {
|
||||
$this->userFactory->hydrateUser($user, $token);
|
||||
}
|
||||
|
||||
$this->repository->saveUser($user);
|
||||
} catch (\Exception $ex) {
|
||||
throw new AuthenticationException(
|
||||
sprintf('Failed creating or hydrating user "%s": %s', $token->getUsername(), $ex->getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$authenticatedToken = $this->tokenFactory->createToken($user, $token->getAttributes(), $user->getRoles());
|
||||
$authenticatedToken->setAuthenticated(true);
|
||||
|
||||
return $authenticatedToken;
|
||||
}
|
||||
|
||||
throw new AuthenticationException('The authentication failed.');
|
||||
}
|
||||
|
||||
public function supports(TokenInterface $token)
|
||||
{
|
||||
return $token instanceof SamlTokenInterface;
|
||||
}
|
||||
}
|
||||
26
src/Saml/SamlAuth.php
Normal file
26
src/Saml/SamlAuth.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Saml/SamlTokenFactory.php
Normal file
28
src/Saml/SamlTokenFactory.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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 Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenFactoryInterface;
|
||||
|
||||
final class SamlTokenFactory implements SamlTokenFactoryInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createToken($user, array $attributes, array $roles)
|
||||
{
|
||||
$token = new SamlToken($roles);
|
||||
$token->setUser($user);
|
||||
$token->setAttributes($attributes);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
30
src/Saml/Security/SamlAuthenticationSuccessHandler.php
Normal file
30
src/Saml/Security/SamlAuthenticationSuccessHandler.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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\Security;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
|
||||
|
||||
final class SamlAuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler
|
||||
{
|
||||
protected function determineTargetUrl(Request $request)
|
||||
{
|
||||
if ($this->options['always_use_default_target_path']) {
|
||||
return $this->options['default_target_path'];
|
||||
}
|
||||
|
||||
$relayState = $request->get('RelayState');
|
||||
if (null !== $relayState && $relayState !== $this->httpUtils->generateUri($request, $this->options['login_path'])) {
|
||||
return $relayState;
|
||||
}
|
||||
|
||||
return parent::determineTargetUrl($request);
|
||||
}
|
||||
}
|
||||
77
src/Saml/Security/SamlFactory.php
Normal file
77
src/Saml/Security/SamlFactory.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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\Security;
|
||||
|
||||
use App\Saml\Logout\SamlLogoutHandler;
|
||||
use App\Saml\Provider\SamlProvider;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
final class SamlFactory extends AbstractFactory
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->addOption('check_path', 'saml_acs');
|
||||
$this->addOption('failure_path', 'fos_user_security_login');
|
||||
$this->addOption('success_handler', SamlAuthenticationSuccessHandler::class);
|
||||
$this->defaultFailureHandlerOptions['login_path'] = 'saml_login';
|
||||
}
|
||||
|
||||
protected function isRememberMeAware($config)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'pre_auth';
|
||||
}
|
||||
|
||||
public function getKey()
|
||||
{
|
||||
return 'kimai_saml';
|
||||
}
|
||||
|
||||
protected function getListenerId()
|
||||
{
|
||||
return 'kimai.saml_listener';
|
||||
}
|
||||
|
||||
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
|
||||
{
|
||||
$providerId = 'security.authentication.provider.saml.' . $id;
|
||||
$definition = $container->setDefinition($providerId, new ChildDefinition(SamlProvider::class));
|
||||
$definition->replaceArgument(1, new Reference($userProviderId));
|
||||
|
||||
return $providerId;
|
||||
}
|
||||
|
||||
protected function createListener($container, $id, $config, $userProvider)
|
||||
{
|
||||
$listenerId = parent::createListener($container, $id, $config, $userProvider);
|
||||
$this->createLogoutHandler($container, $id, $config);
|
||||
|
||||
return $listenerId;
|
||||
}
|
||||
|
||||
private function createLogoutHandler(ContainerBuilder $container, $id, $config)
|
||||
{
|
||||
if ($container->hasDefinition('security.logout_listener.' . $id)) {
|
||||
$logoutListener = $container->getDefinition('security.logout_listener.' . $id);
|
||||
|
||||
$container
|
||||
->setDefinition(SamlLogoutHandler::class, new ChildDefinition('saml.security.http.logout'))
|
||||
->replaceArgument(2, array_intersect_key($config, $this->options));
|
||||
$logoutListener->addMethodCall('addHandler', [new Reference(SamlLogoutHandler::class)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/Saml/User/SamlUserFactory.php
Normal file
116
src/Saml/User/SamlUserFactory.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?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\User;
|
||||
|
||||
use App\Entity\User;
|
||||
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlTokenInterface;
|
||||
use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface;
|
||||
|
||||
final class SamlUserFactory implements SamlUserFactoryInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $mapping;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $groupAttribute;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $groupMapping;
|
||||
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
$this->mapping = $attributes['mapping'];
|
||||
$this->groupAttribute = $attributes['roles']['attribute'];
|
||||
$this->groupMapping = $attributes['roles']['mapping'];
|
||||
}
|
||||
|
||||
public function createUser(SamlTokenInterface $token)
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$user->setUsername($token->getUsername());
|
||||
|
||||
$this->hydrateUser($user, $token);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function hydrateUser(User $user, SamlTokenInterface $token): void
|
||||
{
|
||||
// extract user roles from a special saml attribute
|
||||
if (!empty($this->groupAttribute) && $token->hasAttribute($this->groupAttribute)) {
|
||||
$groupMap = [];
|
||||
foreach ($this->groupMapping as $mapping) {
|
||||
$field = $mapping['kimai'];
|
||||
$attribute = $mapping['saml'];
|
||||
$groupMap[$attribute] = $field;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
$samlGroups = $token->getAttribute($this->groupAttribute);
|
||||
foreach ($samlGroups as $groupName) {
|
||||
if (array_key_exists($groupName, $groupMap)) {
|
||||
$roles[] = $groupMap[$groupName];
|
||||
}
|
||||
}
|
||||
$user->setRoles($roles);
|
||||
}
|
||||
|
||||
foreach ($this->mapping as $mapping) {
|
||||
$field = $mapping['kimai'];
|
||||
$attribute = $mapping['saml'];
|
||||
$value = $this->getPropertyValue($token, $attribute);
|
||||
$setter = 'set' . ucfirst($field);
|
||||
if (method_exists($user, $setter)) {
|
||||
$user->$setter($value);
|
||||
} else {
|
||||
throw new \RuntimeException('Invalid mapping field given: ' . $field);
|
||||
}
|
||||
}
|
||||
|
||||
// fill them after hydrating account, so they can't be overwritten
|
||||
$user->setUsername($token->getUsername());
|
||||
$user->setPassword('');
|
||||
$user->setAuth(User::AUTH_SAML);
|
||||
}
|
||||
|
||||
private function getPropertyValue(SamlTokenInterface $token, $attribute)
|
||||
{
|
||||
$results = [];
|
||||
$attributes = $token->getAttributes();
|
||||
|
||||
$parts = explode(' ', $attribute);
|
||||
foreach ($parts as $part) {
|
||||
if (empty(trim($part))) {
|
||||
continue;
|
||||
}
|
||||
if ($part[0] === '$') {
|
||||
$key = substr($part, 1);
|
||||
if (!isset($attributes[$key])) {
|
||||
throw new \RuntimeException('Missing user attribute: ' . $key);
|
||||
}
|
||||
|
||||
$results[] = $attributes[$key][0];
|
||||
} else {
|
||||
$results[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($results)) {
|
||||
return implode(' ', $results);
|
||||
}
|
||||
|
||||
return $attribute;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user