Skip to content
Snippets Groups Projects
Unverified Commit 837dab94 authored by Tim van Dijen's avatar Tim van Dijen Committed by GitHub
Browse files

Feature: Add auth source selector (#1641)

* Add AuthSource Selector - dynamically call an authsource based on policy decision
parent 6298d32e
No related branches found
No related tags found
No related merge requests found
# Authentication source selector
The Authentication source selector is a special kind of Authentication Source
that delegates the actual authentication to a secondary Authentication Source
based on some form of policy decision.
## AbstractSourceSelector
The AbstractSourceSelector extends from `\SimpleSAML\Auth\Source` and as such
act as an Authentication Source. Any derivative classes must implement the
abstract `selectAuthSource` method. This method must return the name of the
Authentication Source to use, based on whatever logic is necessary.
## IPSourceSelector
The IPSourceSelector is an implementation of the `AbstractSourceSelector` and
uses the client IP to decide what Authentication Source is called.
It works by defining zones with corresponding IP-ranges and Authentication
Sources. The 'default' zone is required and acts as a fallback when none
of the zones match a client's IP-address.
An example configuration would look like this:
```php
'selector' => [
'core:IPSourceSelector',
'zones' => [
'internal' => [
'source' => 'ldap',
'subnet' => [
'10.0.0.0/8',
'2001:0DB8::/108',
],
],
'other' => [
'source' => 'radius',
'subnet' => [
'172.16.0.0/12',
'2002:1234::/108',
],
],
'default' => 'yubikey',
],
],
```
## YourCustomSourceSelector
If you have a use-case for a custom Authentication source selector, all you
have to do is to create your own class, make it extend `AbstractSourceSelector`
and make it implement the abstract `selectAuthSource` method containing
your own logic. The method should return the name of the Authentication
source to use.
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\core\Auth\Source;
use Exception;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Auth;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\Session;
/**
* Authentication source which delegates authentication to secondary
* authentication sources based on policy decision
*
* @package SimpleSAMLphp
*/
abstract class AbstractSourceSelector extends Auth\Source
{
/**
* @var array The names of all the configured auth sources
*/
protected array $validSources;
/**
* Constructor for this authentication source.
*
* @param array $info Information about this authentication source.
* @param array $config Configuration.
*/
public function __construct(array $info, array $config)
{
// Call the parent constructor first, as required by the interface
parent::__construct($info, $config);
$authsources = Configuration::getConfig('authsources.php');
$this->validSources = array_keys($authsources->toArray());
}
/**
* Process a request.
*
* If an authentication source returns from this function, it is assumed to have
* authenticated the user, and should have set elements in $state with the attributes
* of the user.
*
* If the authentication process requires additional steps which make it impossible to
* complete before returning from this function, the authentication source should
* save the state, and at a later stage, load the state, update it with the authentication
* information about the user, and call completeAuth with the state array.
*
* @param array &$state Information about the current authentication.
*/
public function authenticate(array &$state): void
{
$source = $this->selectAuthSource();
$as = Auth\Source::getById($source);
if ($as === null || !in_array($source, $this->validSources, true)) {
throw new Exception('Invalid authentication source: ' . $source);
}
static::doAuthentication($as, $state);
}
/**
* @param \SimpleSAML\Auth\Source $as
* @param array $state
* @return void
*/
public static function doAuthentication(Auth\Source $as, array $state): void
{
try {
$as->authenticate($state);
} catch (Error\Exception $e) {
Auth\State::throwException($state, $e);
} catch (Exception $e) {
$e = new Error\UnserializableException($e);
Auth\State::throwException($state, $e);
}
Auth\Source::completeAuth($state);
}
/**
* Decide what authsource to use.
*
* @param array &$state Information about the current authentication.
* @return string
*/
abstract protected function selectAuthSource(): string;
}
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\core\Auth\Source;
use Exception;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Auth;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\Logger;
use SimpleSAML\Session;
use SimpleSAML\Utils;
/**
* Authentication source which delegates authentication to secondary
* authentication sources based on the client's source IP
*
* @package simplesamlphp/simplesamlphp
*/
class IPSourceSelector extends AbstractSourceSelector
{
/**
* The key of the AuthId field in the state.
*/
public const AUTHID = '\SimpleSAML\Module\core\Auth\Source\IPSourceSelector.AuthId';
/**
* The string used to identify our states.
*/
public const STAGEID = '\SimpleSAML\Module\core\Auth\Source\IPSourceSelector.StageId';
/**
* The key where the sources is saved in the state.
*/
public const SOURCESID = '\SimpleSAML\Module\core\Auth\Source\IPSourceSelector.SourceId';
/**
* @param string The default authentication source to use when none of the zones match
*/
protected string $defaultSource;
/**
* @param array An array of zones. Each zone requires two keys;
* 'source' containing the authsource for the zone,
* 'subnet' containing an array of IP-ranges (CIDR notation).
*/
protected array $zones = [];
/**
* Constructor for this authentication source.
*
* @param array $info Information about this authentication source.
* @param array $config Configuration.
*/
public function __construct(array $info, array $config)
{
// Call the parent constructor first, as required by the interface
parent::__construct($info, $config);
Assert::keyExists($config, 'zones');
Assert::keyExists($config['zones'], 'default');
Assert::stringNotEmpty($config['zones']['default']);
$this->defaultSource = $config['zones']['default'];
unset($config['zones']['default']);
$zones = $config['zones'];
foreach ($zones as $key => $zone) {
if (!array_key_exists('source', $zone)) {
Logger::warning(sprintf('Discarding zone %s due to missing `source` key.', $key));
} elseif (!array_key_exists('subnet', $zone)) {
Logger::warning(sprintf('Discarding zone %s due to missing `subnet` key.', $key));
} else {
$this->zones[$key] = $zone;
}
}
}
/**
* Decide what authsource to use.
*
* @param array &$state Information about the current authentication.
* @return string
*/
protected function selectAuthSource(): string
{
$netUtils = new Utils\Net();
$ip = $_SERVER['REMOTE_ADDR'];
$source = $this->defaultSource;
foreach ($this->zones as $name => $zone) {
foreach ($zone['subnet'] as $subnet) {
if ($netUtils->ipCIDRcheck($subnet, $ip)) {
// Client's IP is in one of the ranges for the secondary auth source
Logger::info(sprintf("core:IPSourceSelector: Selecting zone `%s` based on client IP %s", $name, $ip));
$source = $zone['source'];
break;
}
}
}
if ($source === $this->defaultSource) {
Logger::info("core:IPSourceSelector: no match on client IP; selecting default zone");
}
return $source;
}
}
<?php
declare(strict_types=1);
namespace SimpleSAML\Test\Module\core\Auth\Source;
use Error;
use Exception;
use PHPUnit\Framework\TestCase;
use SimpleSAML\Assert\AssertionFailedException;
use SimpleSAML\Auth;
use SimpleSAML\Configuration;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\Module\core\Auth\Source\IPSourceSelector;
/**
* @covers \SimpleSAML\Module\core\Auth\Source\AbstractSourceSelector
* @covers \SimpleSAML\Module\core\Auth\Source\IPSourceSelector
*/
class IPSourceSelectorTest extends TestCase
{
/** @var \SimpleSAML\Configuration */
private Configuration $config;
/** @var \SimpleSAML\Configuration */
private Configuration $sourceConfig;
/**
*/
public function setUp(): void
{
$this->config = Configuration::loadFromArray(
['module.enable' => ['core' => true]],
'[ARRAY]',
'simplesaml'
);
Configuration::setPreLoadedConfig($this->config, 'config.php');
$this->sourceConfig = Configuration::loadFromArray([
'selector' => [
'core:IPSourceSelector',
'zones' => [
'internal' => [
'source' => 'internal',
'subnet' => [
'10.0.0.0/8',
'2001:0DB8::/108',
],
],
'other' => [
'source' => 'other',
'subnet' => [
'172.16.0.0/12',
'2002:1234::/108',
],
],
'default' => 'external',
],
],
'other' => [
'core:AdminPassword',
],
'internal' => [
'core:AdminPassword',
],
'external' => [
'core:AdminPassword',
],
]);
Configuration::setPreLoadedConfig($this->sourceConfig, 'authsources.php');
}
/**
*/
public function testDefaultZoneIsRequired(): void
{
$this->expectException(AssertionFailedException::class);
$this->expectExceptionMessage('Expected the key "default" to exist.');
$sourceConfig = Configuration::loadFromArray([
'selector' => [
'core:IPSourceSelector',
'zones' => [
'internal' => [],
],
],
]);
Configuration::setPreLoadedConfig($sourceConfig, 'authsources.php');
new IPSourceSelector(['AuthId' => 'selector'], $sourceConfig->getArray('selector'));
}
/**
*/
public function testAuthentication(): void
{
$info = ['AuthId' => 'selector'];
$config = $this->sourceConfig->getArray('selector');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['REQUEST_URI'] = '/';
$selector = new class ($info, $config) extends IPSourceSelector {
/**
* @param \SimpleSAML\Auth\Source $as
* @param array $state
* @return void
*/
public static function doAuthentication(Auth\Source $as, array $state): void
{
// Dummy
}
};
$state = [];
$result = $selector->authenticate($state);
$this->assertNull($result);
}
/**
* @dataProvider provideClientIP
* @param string $ip The client IP
* @param string $expected The expected authsource
*/
public function testSelectAuthSource(string $ip, string $expected): void
{
$info = ['AuthId' => 'selector'];
$config = $this->sourceConfig->getArray('selector');
$_SERVER['REMOTE_ADDR'] = $ip;
$selector = new class ($info, $config) extends IPSourceSelector {
/**
* @return string
*/
public function selectAuthSource(): string
{
return parent::selectAuthSource();
}
};
$source = $selector->selectAuthSource();
$this->assertEquals($expected, $source);
}
/**
* @return string
*/
public function provideClientIP(): array
{
return [
['127.0.0.2', 'external'],
['10.4.13.2', 'internal'],
['2001:0DB8:0000:0000:0000:0000:0000:0000', 'internal'],
['145.21.93.97', 'external'],
['172.16.1.2', 'other'],
['2002:1234:0000:0000:0000:0000:0000:0000', 'other'],
];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment