diff --git a/modules/core/docs/authsource_selector.md b/modules/core/docs/authsource_selector.md
new file mode 100644
index 0000000000000000000000000000000000000000..b4afa124ae6e5093e8f5de059ca438be72ef3735
--- /dev/null
+++ b/modules/core/docs/authsource_selector.md
@@ -0,0 +1,56 @@
+# 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.
diff --git a/modules/core/lib/Auth/Source/AbstractSourceSelector.php b/modules/core/lib/Auth/Source/AbstractSourceSelector.php
new file mode 100644
index 0000000000000000000000000000000000000000..48a061be8ab52772059eccb13c8ba6d3f682e13c
--- /dev/null
+++ b/modules/core/lib/Auth/Source/AbstractSourceSelector.php
@@ -0,0 +1,99 @@
+<?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;
+}
diff --git a/modules/core/lib/Auth/Source/IPSourceSelector.php b/modules/core/lib/Auth/Source/IPSourceSelector.php
new file mode 100644
index 0000000000000000000000000000000000000000..3264513dee005f5614ca5cafeed1f9cef3a74c78
--- /dev/null
+++ b/modules/core/lib/Auth/Source/IPSourceSelector.php
@@ -0,0 +1,113 @@
+<?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;
+    }
+}
diff --git a/tests/modules/core/lib/Auth/Source/IPSourceSelectorTest.php b/tests/modules/core/lib/Auth/Source/IPSourceSelectorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6addcf29991953dcca4f7f595efe9dd9de246e3e
--- /dev/null
+++ b/tests/modules/core/lib/Auth/Source/IPSourceSelectorTest.php
@@ -0,0 +1,172 @@
+<?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'],
+        ];
+    }
+}