added support for saml login (#1408)

This commit is contained in:
Kevin Papst
2020-01-31 19:47:34 +01:00
committed by GitHub
parent 3ff46e06c0
commit 6a533579b7
47 changed files with 2278 additions and 77 deletions

View File

@@ -0,0 +1,45 @@
<?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\Tests\Saml\Controller;
use App\Saml\Controller\SamlController;
use App\Tests\Mocks\Saml\SamlAuthFactory;
use PHPUnit\Framework\TestCase;
/**
* @group integration
*/
class SamlControllerTest extends TestCase
{
protected function getAuth()
{
return (new SamlAuthFactory($this))->create();
}
public function testAssertionConsumerServiceAction()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('You must configure the check path in your firewall.');
$oauth = $this->getAuth();
$sut = new SamlController($oauth);
$sut->assertionConsumerServiceAction();
}
public function testLogoutAction()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('You must configure the logout path in your firewall.');
$oauth = $this->getAuth();
$sut = new SamlController($oauth);
$sut->logoutAction();
}
}

View File

@@ -0,0 +1,62 @@
<?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\Tests\Saml\Logout;
use App\Entity\User;
use App\Saml\Logout\SamlLogoutHandler;
use App\Saml\SamlAuth;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
use OneLogin\Saml2\Error;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @covers \App\Saml\Logout\SamlLogoutHandler
*/
class SamlLogoutHandlerTest extends TestCase
{
public function testLogout()
{
$auth = $this->getMockBuilder(SamlAuth::class)->disableOriginalConstructor()->getMock();
$auth->expects($this->once())->method('processSLO')->willThrowException(new Error('blub'));
$auth->expects($this->once())->method('getSLOurl')->willReturn('');
$request = new Request();
$response = new Response();
$token = new SamlToken([]);
$sut = new SamlLogoutHandler($auth);
$sut->logout($request, $response, $token);
}
public function testLogoutWithLogoutUrl()
{
$auth = $this->getMockBuilder(SamlAuth::class)->disableOriginalConstructor()->getMock();
$auth->expects($this->once())->method('processSLO')->willThrowException(new Error('blub'));
$auth->expects($this->once())->method('getSLOurl')->willReturn('/logout');
$auth->expects($this->once())->method('logout')->willReturnCallback(function () {
$args = func_get_args();
self::assertEquals(null, $args[0]);
self::assertEquals([], $args[1]);
self::assertEquals('tony', $args[2]);
self::assertEquals('foo-bar', $args[3]);
});
$request = new Request();
$response = new Response();
$token = new SamlToken([]);
$token->setUser((new User())->setUsername('tony'));
$token->setAttribute('sessionIndex', 'foo-bar');
$sut = new SamlLogoutHandler($auth);
$sut->logout($request, $response, $token);
}
}

View File

@@ -0,0 +1,111 @@
<?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\Tests\Saml\Provider;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Saml\Provider\SamlProvider;
use App\Saml\SamlTokenFactory;
use App\Saml\User\SamlUserFactory;
use App\Security\DoctrineUserProvider;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\ChainUserProvider;
/**
* @covers \App\Saml\Provider\SamlProvider
*/
class SamlProviderTest extends TestCase
{
protected function getSamlProvider($mapping = null, $loadUser = false): SamlProvider
{
if (null === $mapping) {
$mapping = [
'mapping' => [
['saml' => '$Email', 'kimai' => 'email'],
['saml' => '$title', 'kimai' => 'title'],
],
'roles' => [
'attribute' => '',
'mapping' => []
]
];
}
$repository = $this->getMockBuilder(UserRepository::class)->disableOriginalConstructor()->getMock();
if ($loadUser !== false) {
$repository->expects($this->once())->method('loadUserByUsername')->willReturn($loadUser);
}
$userProvider = new ChainUserProvider([new DoctrineUserProvider($repository)]);
$provider = new SamlProvider($repository, $userProvider, new SamlTokenFactory(), new SamlUserFactory($mapping));
return $provider;
}
public function testSupportsToken()
{
$sut = $this->getSamlProvider();
self::assertFalse($sut->supports(new AnonymousToken('ads', 'ads')));
self::assertFalse($sut->supports(new UsernamePasswordToken('ads', 'ads', 'asd')));
self::assertTrue($sut->supports(new SamlToken([])));
}
public function testAuthenticateHydratesUser()
{
$user = new User();
$user->setAuth(User::AUTH_SAML);
$token = new SamlToken([]);
$token->setUser('foo1@example.com');
$token->setAttributes([
'Email' => ['foo@example.com'],
'title' => ['Tralalala'],
]);
self::assertFalse($token->isAuthenticated());
$sut = $this->getSamlProvider(null, $user);
$authToken = $sut->authenticate($token);
self::assertTrue($authToken->isAuthenticated());
/** @var User $tokenUser */
$tokenUser = $authToken->getUser();
self::assertSame($user, $tokenUser);
self::assertEquals('foo1@example.com', $tokenUser->getUsername());
self::assertEquals('Tralalala', $tokenUser->getTitle());
self::assertEquals('foo@example.com', $tokenUser->getEmail());
}
public function testAuthenticatCreatesNewUser()
{
$token = new SamlToken([]);
$token->setUser('foo1@example.com');
$token->setAttributes([
'Email' => ['foo@example.com'],
'title' => ['Tralalala'],
]);
self::assertFalse($token->isAuthenticated());
$sut = $this->getSamlProvider(null);
$authToken = $sut->authenticate($token);
self::assertTrue($authToken->isAuthenticated());
/** @var User $tokenUser */
$tokenUser = $authToken->getUser();
self::assertEquals('foo1@example.com', $tokenUser->getUsername());
self::assertEquals('Tralalala', $tokenUser->getTitle());
self::assertEquals('foo@example.com', $tokenUser->getEmail());
}
}

View File

@@ -0,0 +1,37 @@
<?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\Tests\Saml;
use App\Entity\User;
use App\Saml\SamlTokenFactory;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Saml\SamlTokenFactory
*/
class SamlTokenFactoryTest extends TestCase
{
public function testCreateToken()
{
$user = new User();
$user->setUsername('foobar');
$factory = new SamlTokenFactory();
$sut = $factory->createToken($user, ['foo' => 'bar', 'bar' => 'world'], ['ROLE_ADMIN', 'ROLE_TEST']);
self::assertInstanceOf(SamlToken::class, $sut);
self::assertEquals('bar', $sut->getAttribute('foo'));
self::assertEquals('world', $sut->getAttribute('bar'));
self::assertEquals(['ROLE_ADMIN', 'ROLE_TEST'], $sut->getRoleNames());
self::assertSame($user, $sut->getUser());
self::assertEquals('foobar', $sut->getUsername());
}
}

View File

@@ -0,0 +1,104 @@
<?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\Tests\Saml\Security;
use App\Saml\Security\SamlAuthenticationSuccessHandler;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\HttpUtils;
/**
* @covers \App\Saml\Security\SamlAuthenticationSuccessHandler
*/
class SamlAuthenticationSuccessHandlerTest extends TestCase
{
private $handler;
public function testWithAlwaysUseDefaultTargetPath()
{
$httpUtils = new HttpUtils($this->getUrlGenerator());
$handler = new SamlAuthenticationSuccessHandler($httpUtils, ['always_use_default_target_path' => true]);
$defaultTargetPath = $httpUtils->generateUri($this->getRequest('/sso/login'), $this->getOption($handler, 'default_target_path', '/'));
$response = $handler->onAuthenticationSuccess($this->getRequest('/login', 'http://localhost/relayed'), $this->getSamlToken());
$this->assertTrue($response->isRedirect($defaultTargetPath));
}
public function testRelayState()
{
$handler = new SamlAuthenticationSuccessHandler(new HttpUtils($this->getUrlGenerator()), ['always_use_default_target_path' => false]);
$response = $handler->onAuthenticationSuccess($this->getRequest('/sso/login', 'http://localhost/relayed'), $this->getSamlToken());
$this->assertTrue($response->isRedirect('http://localhost/relayed'));
}
public function testWithoutRelayState()
{
$httpUtils = new HttpUtils($this->getUrlGenerator());
$handler = new SamlAuthenticationSuccessHandler($httpUtils, ['always_use_default_target_path' => false]);
$defaultTargetPath = $httpUtils->generateUri($this->getRequest('/sso/login'), $this->getOption($handler, 'default_target_path', '/'));
$response = $handler->onAuthenticationSuccess($this->getRequest(), $this->getSamlToken());
$this->assertTrue($response->isRedirect($defaultTargetPath));
}
public function testRelayStateLoop()
{
$httpUtils = new HttpUtils($this->getUrlGenerator());
$handler = new SamlAuthenticationSuccessHandler($httpUtils, ['always_use_default_target_path' => false]);
$loginPath = $httpUtils->generateUri($this->getRequest('/sso/login'), $this->getOption($handler, 'login_path', '/login'));
$response = $handler->onAuthenticationSuccess($this->getRequest($loginPath), $this->getSamlToken());
$this->assertTrue(!$response->isRedirect($loginPath));
}
private function getUrlGenerator()
{
$urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock();
$urlGenerator
->expects($this->any())
->method('generate')
->will($this->returnCallback(function ($name) {
return (string) $name;
}))
;
return $urlGenerator;
}
private function getRequest($path = '/', $relayState = null)
{
$params = [];
if (null !== $relayState) {
$params['RelayState'] = $relayState;
}
return Request::create($path, 'get', $params);
}
private function getSamlToken()
{
$token = new SamlToken([]);
$token->setAttributes(['foo' => 'bar']);
$token->setUser('admin');
return $token;
}
private function getOption($handler, $name, $default = null)
{
$reflection = new \ReflectionObject($handler);
$options = $reflection->getProperty('options');
$options->setAccessible(true);
$arr = $options->getValue($handler);
if (!is_array($arr) || !isset($arr[$name])) {
return $default;
}
return $arr[$name];
}
}

View File

@@ -0,0 +1,45 @@
<?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\Tests\Saml\Security;
use App\Saml\Security\SamlFactory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @covers \App\Saml\Security\SamlFactory
*/
class SamlFactoryTest extends TestCase
{
public function testStaticValues()
{
$sut = new SamlFactory();
self::assertEquals('kimai_saml', $sut->getKey());
self::assertEquals('pre_auth', $sut->getPosition());
}
public function testCreate()
{
$container = new ContainerBuilder();
$sut = new SamlFactory();
$result = $sut->create($container, 'test', ['foo' => 'bar', 'login_path' => null, 'use_forward' => null], 'fosuserbundle', 'secured_area');
self::assertEquals([
'security.authentication.provider.saml.test',
'kimai.saml_listener.test',
'secured_area'
], $result);
$definition = $container->getDefinition('security.authentication.provider.saml.test');
self::assertInstanceOf(ChildDefinition::class, $definition);
self::assertCount(1, $definition->getArguments());
}
}

View File

@@ -0,0 +1,184 @@
<?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\Tests\Saml\User;
use App\Entity\User;
use App\Saml\User\SamlUserFactory;
use Hslavich\OneloginSamlBundle\Security\Authentication\Token\SamlToken;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Saml\User\SamlUserFactory
*/
class SamlUserFactoryTest extends TestCase
{
public function testCreateUserThrowsExceptionOnMissingAttribute()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Missing user attribute: title');
$mapping = [
'mapping' => [
['saml' => '$Email', 'kimai' => 'email'],
['saml' => '$title', 'kimai' => 'title'],
],
'roles' => [
'attribute' => '',
'mapping' => []
]
];
$attributes = [
'Email' => ['test@example.com'],
];
$token = new SamlToken();
$token->setAttributes($attributes);
$sut = new SamlUserFactory($mapping);
$user = $sut->createUser($token);
}
public function testCreateUserThrowsExceptionOnMissingAttributeInMultiple()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Missing user attribute: test');
$mapping = [
'mapping' => [
['saml' => '$Email $test', 'kimai' => 'email'],
],
'roles' => [
'attribute' => '',
'mapping' => []
]
];
$attributes = [
'Email' => ['test@example.com'],
];
$token = new SamlToken();
$token->setAttributes($attributes);
$sut = new SamlUserFactory($mapping);
$user = $sut->createUser($token);
}
public function testCreateUserThrowsExceptionOnInvalidMapping()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Invalid mapping field given: foo');
$mapping = [
'mapping' => [
['saml' => '$Email', 'kimai' => 'email'],
['saml' => '$Email', 'kimai' => 'foo'],
],
'roles' => [
'attribute' => '',
'mapping' => []
]
];
$attributes = [
'Email' => ['test@example.com'],
];
$token = new SamlToken();
$token->setAttributes($attributes);
$sut = new SamlUserFactory($mapping);
$user = $sut->createUser($token);
}
public function testCreateUser()
{
$mapping = [
'mapping' => [
['saml' => '$avatar', 'kimai' => 'avatar'],
['saml' => '$Email', 'kimai' => 'email'],
['saml' => 'A static super title', 'kimai' => 'title'],
// double space between "$LastName $FOOO" on purpose!!!
['saml' => '$FirstName $LastName $FOOO me', 'kimai' => 'alias'],
],
'roles' => [
'attribute' => 'RoLeS',
'mapping' => [
['saml' => 'fooobar', 'kimai' => 'ROLE_ADMIN'],
['saml' => 'ROLE_1', 'kimai' => 'ROLE_TEAMLEAD'],
['saml' => 'ROLE_2', 'kimai' => 'ROLE_2'],
]
]
];
$attributes = [
'RoLeS' => ['ROLE_1', 'ROLE_2', 'ROLE_3'],
'Email' => ['test@example.com'],
'FOOO' => ['test', 'test2'],
'FirstName' => ['Kevin'],
'LastName' => ['Papst'],
'avatar' => ['http://www.example.com/test.jpg'],
];
$token = new SamlToken();
$token->setUser('foo@example.com');
$token->setAttributes($attributes);
$sut = new SamlUserFactory($mapping);
$user = $sut->createUser($token);
self::assertInstanceOf(User::class, $user);
self::assertTrue($user->isEnabled());
self::assertEquals('', $user->getPassword());
self::assertEquals('test@example.com', $user->getEmail());
self::assertEquals('foo@example.com', $user->getUsername());
self::assertEquals('A static super title', $user->getTitle());
self::assertEquals('Kevin Papst test me', $user->getAlias());
self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_2', 'ROLE_USER'], $user->getRoles());
}
public function testCreateUserDoesOverwriteUsername()
{
$mapping = [
'mapping' => [
['saml' => '$avatar', 'kimai' => 'avatar'],
['saml' => '$Email', 'kimai' => 'email'],
['saml' => 'A static super title', 'kimai' => 'title'],
['saml' => 'Mr. T', 'kimai' => 'username'],
],
'roles' => [
'attribute' => null,
'mapping' => []
]
];
$attributes = [
'Email' => ['test@example.com'],
'FOOO' => ['test', 'test2'],
'avatar' => ['http://www.example.com/test.jpg'],
];
$token = new SamlToken();
$token->setUser('foo@example.com');
$token->setAttributes($attributes);
$sut = new SamlUserFactory($mapping);
$user = $sut->createUser($token);
self::assertInstanceOf(User::class, $user);
self::assertTrue($user->isEnabled());
self::assertEquals('', $user->getPassword());
self::assertEquals('test@example.com', $user->getEmail());
self::assertEquals('foo@example.com', $user->getUsername());
self::assertEquals('A static super title', $user->getTitle());
self::assertEquals(['ROLE_USER'], $user->getRoles());
}
}