diff --git a/src/Bridges/SspBridge.php b/src/Bridges/SspBridge.php
index 1d0a6173..6b5b7ffc 100644
--- a/src/Bridges/SspBridge.php
+++ b/src/Bridges/SspBridge.php
@@ -4,6 +4,7 @@
namespace SimpleSAML\Module\oidc\Bridges;
+use SimpleSAML\Module\oidc\Bridges\SspBridge\Auth;
use SimpleSAML\Module\oidc\Bridges\SspBridge\Module;
use SimpleSAML\Module\oidc\Bridges\SspBridge\Utils;
@@ -13,6 +14,7 @@
*/
class SspBridge
{
+ protected static ?Auth $auth = null;
protected static ?Utils $utils = null;
protected static ?Module $module = null;
@@ -25,4 +27,9 @@ public function module(): Module
{
return self::$module ??= new Module();
}
+
+ public function auth(): Auth
+ {
+ return self::$auth ??= new Auth();
+ }
}
diff --git a/src/Bridges/SspBridge/Auth.php b/src/Bridges/SspBridge/Auth.php
new file mode 100644
index 00000000..d3be17d2
--- /dev/null
+++ b/src/Bridges/SspBridge/Auth.php
@@ -0,0 +1,17 @@
+buildForm();
@@ -315,6 +318,14 @@ public function setDefaults(object|array $data, bool $erase = false): static
$data['jwks'] = is_array($data['jwks']) ? json_encode($data['jwks']) : null;
+ if (
+ $data['auth_source'] !== null &&
+ (!in_array($data['auth_source'], $this->sspBridge->auth()->source()->getSources()))
+ ) {
+ // Possible auth source name change without prior update in clients, resetting.
+ $data['auth_source'] = null;
+ }
+
parent::setDefaults($data, $erase);
return $this;
@@ -355,10 +366,9 @@ protected function buildForm(): void
$this->addCheckbox('is_confidential', '{oidc:client:is_confidential}');
- // TODO mivanci Source::getSource() move to SSP Bridge.
$this->addSelect('auth_source', '{oidc:client:auth_source}:')
->setHtmlAttribute('class', 'full-width')
- ->setItems(Source::getSources(), false)
+ ->setItems($this->sspBridge->auth()->source()->getSources(), false)
->setPrompt(Translate::noop('-'));
$scopes = $this->getScopes();
diff --git a/tests/config/authsources.php b/tests/config/authsources.php
new file mode 100644
index 00000000..6d5b7481
--- /dev/null
+++ b/tests/config/authsources.php
@@ -0,0 +1,348 @@
+ [
+ // The default is to use core:AdminPassword, but it can be replaced with
+ // any authentication source.
+
+ 'core:AdminPassword',
+ ],
+
+
+ // An authentication source which can authenticate against SAML 2.0 IdPs.
+ 'default-sp' => [
+ 'saml:SP',
+
+ // The entity ID of this SP.
+ 'entityID' => 'https://myapp.example.org/',
+
+ // The entity ID of the IdP this SP should contact.
+ // Can be NULL/unset, in which case the user will be shown a list of available IdPs.
+ 'idp' => null,
+
+ // The URL to the discovery service.
+ // Can be NULL/unset, in which case a builtin discovery service will be used.
+ 'discoURL' => null,
+
+ /*
+ * If SP behind the SimpleSAMLphp in IdP/SP proxy mode requests
+ * AuthnContextClassRef, decide whether the AuthnContextClassRef will be
+ * processed by the IdP/SP proxy or if it will be passed to the original
+ * IdP in front of the IdP/SP proxy.
+ */
+ 'proxymode.passAuthnContextClassRef' => false,
+
+ /*
+ * The attributes parameter must contain an array of desired attributes by the SP.
+ * The attributes can be expressed as an array of names or as an associative array
+ * in the form of 'friendlyName' => 'name'. This feature requires 'name' to be set.
+ * The metadata will then be created as follows:
+ *
+ */
+ /*
+ 'name' => [
+ 'en' => 'A service',
+ 'no' => 'En tjeneste',
+ ],
+
+ 'attributes' => [
+ 'attrname' => 'urn:oid:x.x.x.x',
+ ],
+ 'attributes.required' => [
+ 'urn:oid:x.x.x.x',
+ ],
+ */
+ ],
+
+
+ /*
+ 'example-sql' => [
+ 'sqlauth:SQL',
+ 'dsn' => 'pgsql:host=sql.example.org;port=5432;dbname=simplesaml',
+ 'username' => 'simplesaml',
+ 'password' => 'secretpassword',
+ 'query' => 'SELECT uid, givenName, email, eduPersonPrincipalName FROM users WHERE uid = :username ' .
+ 'AND password = SHA2(CONCAT((SELECT salt FROM users WHERE uid = :username), :password), 256);',
+ ],
+ */
+
+ /*
+ 'example-static' => [
+ 'exampleauth:StaticSource',
+ 'uid' => ['testuser'],
+ 'eduPersonAffiliation' => ['member', 'employee'],
+ 'cn' => ['Test User'],
+ ],
+ */
+
+ /*
+ 'example-userpass' => [
+ 'exampleauth:UserPass',
+
+ // Give the user an option to save their username for future login attempts
+ // And when enabled, what should the default be, to save the username or not
+ //'remember.username.enabled' => false,
+ //'remember.username.checked' => false,
+
+ 'users' => [
+ 'student:studentpass' => [
+ 'uid' => ['test'],
+ 'eduPersonAffiliation' => ['member', 'student'],
+ ],
+ 'employee:employeepass' => [
+ 'uid' => ['employee'],
+ 'eduPersonAffiliation' => ['member', 'employee'],
+ ],
+ ],
+ ],
+ */
+
+ /*
+ 'crypto-hash' => [
+ 'authcrypt:Hash',
+ // hashed version of 'verysecret', made with bin/pwgen.php
+ 'professor:{SSHA256}P6FDTEEIY2EnER9a6P2GwHhI5JDrwBgjQ913oVQjBngmCtrNBUMowA==' => [
+ 'uid' => ['prof_a'],
+ 'eduPersonAffiliation' => ['member', 'employee', 'board'],
+ ],
+ ],
+ */
+
+ /*
+ 'htpasswd' => [
+ 'authcrypt:Htpasswd',
+ 'htpasswd_file' => '/var/www/foo.edu/legacy_app/.htpasswd',
+ 'static_attributes' => [
+ 'eduPersonAffiliation' => ['member', 'employee'],
+ 'Organization' => ['University of Foo'],
+ ],
+ ],
+ */
+
+ /*
+ // This authentication source serves as an example of integration with an
+ // external authentication engine. Take a look at the comment in the beginning
+ // of modules/exampleauth/lib/Auth/Source/External.php for a description of
+ // how to adjust it to your own site.
+ 'example-external' => [
+ 'exampleauth:External',
+ ],
+ */
+
+ /*
+ 'yubikey' => [
+ 'authYubiKey:YubiKey',
+ 'id' => '000',
+ // 'key' => '012345678',
+ ],
+ */
+
+ /*
+ 'facebook' => [
+ 'authfacebook:Facebook',
+ // Register your Facebook application on http://www.facebook.com/developers
+ // App ID or API key (requests with App ID should be faster; https://github.com/facebook/php-sdk/issues/214)
+ 'api_key' => 'xxxxxxxxxxxxxxxx',
+ // App Secret
+ 'secret' => 'xxxxxxxxxxxxxxxx',
+ // which additional data permissions to request from user
+ // see http://developers.facebook.com/docs/authentication/permissions/ for the full list
+ // 'req_perms' => 'email,user_birthday',
+ // Which additional user profile fields to request.
+ // When empty, only the app-specific user id and name will be returned
+ // See https://developers.facebook.com/docs/graph-api/reference/v2.6/user for the full list
+ // 'user_fields' => 'email,birthday,third_party_id,name,first_name,last_name',
+ ],
+ */
+
+ /*
+ // Twitter OAuth Authentication API.
+ // Register your application to get an API key here:
+ // http://twitter.com/oauth_clients
+ 'twitter' => [
+ 'authtwitter:Twitter',
+ 'key' => 'xxxxxxxxxxxxxxxx',
+ 'secret' => 'xxxxxxxxxxxxxxxx',
+ // Forces the user to enter their credentials to ensure the correct users account is authorized.
+ // Details: https://dev.twitter.com/docs/api/1/get/oauth/authenticate
+ 'force_login' => false,
+ ],
+ */
+
+ /*
+ // Microsoft Account (Windows Live ID) Authentication API.
+ // Register your application to get an API key here:
+ // https://apps.dev.microsoft.com/
+ 'windowslive' => [
+ 'authwindowslive:LiveID',
+ 'key' => 'xxxxxxxxxxxxxxxx',
+ 'secret' => 'xxxxxxxxxxxxxxxx',
+ ],
+ */
+
+ /*
+ // Example of a LDAP authentication source.
+ 'example-ldap' => [
+ 'ldap:Ldap',
+
+ // The connection string for the LDAP-server.
+ // You can add multiple by separating them with a space.
+ 'connection_string' => 'ldap.example.org',
+
+ // Whether SSL/TLS should be used when contacting the LDAP server.
+ // Possible values are 'ssl', 'tls' or 'none'
+ 'encryption' => 'ssl',
+
+ // The LDAP version to use when interfacing the LDAP-server.
+ // Defaults to 3
+ 'version' => 3,
+
+ // Set to TRUE to enable LDAP debug level. Passed to the LDAP connector class.
+ //
+ // Default: FALSE
+ // Required: No
+ 'ldap.debug' => false,
+
+ // The LDAP-options to pass when setting up a connection
+ // See [Symfony documentation][1]
+ 'options' => [
+
+ // Set whether to follow referrals.
+ // AD Controllers may require 0x00 to function.
+ // Possible values are 0x00 (NEVER), 0x01 (SEARCHING),
+ // 0x02 (FINDING) or 0x03 (ALWAYS).
+ 'referrals' => 0x00,
+
+ 'network_timeout' => 3,
+ ],
+
+ // The connector to use.
+ // Defaults to '\SimpleSAML\Module\ldap\Connector\Ldap', but can be set
+ // to '\SimpleSAML\Module\ldap\Connector\ActiveDirectory' when
+ // authenticating against Microsoft Active Directory. This will
+ // provide you with more specific error messages.
+ 'connector' => '\SimpleSAML\Module\ldap\Connector\Ldap',
+
+ // Which attributes should be retrieved from the LDAP server.
+ // This can be an array of attribute names, or NULL, in which case
+ // all attributes are fetched.
+ 'attributes' => null,
+
+ // Which attributes should be base64 encoded after retrieval from
+ // the LDAP server.
+ 'attributes.binary' => [
+ 'jpegPhoto',
+ 'objectGUID',
+ 'objectSid',
+ 'mS-DS-ConsistencyGuid'
+ ],
+
+ // The pattern which should be used to create the user's DN given
+ // the username. %username% in this pattern will be replaced with
+ // the user's username.
+ //
+ // This option is not used if the search.enable option is set to TRUE.
+ 'dnpattern' => 'uid=%username%,ou=people,dc=example,dc=org',
+
+ // As an alternative to specifying a pattern for the users DN, it is
+ // possible to search for the username in a set of attributes. This is
+ // enabled by this option.
+ 'search.enable' => false,
+
+ // An array on DNs which will be used as a base for the search. In
+ // case of multiple strings, they will be searched in the order given.
+ 'search.base' => [
+ 'ou=people,dc=example,dc=org',
+ ],
+
+ // The scope of the search. Valid values are 'sub' and 'one' and
+ // 'base', first one being the default if no value is set.
+ 'search.scope' => 'sub',
+
+ // The attribute(s) the username should match against.
+ //
+ // This is an array with one or more attribute names. Any of the
+ // attributes in the array may match the value the username.
+ 'search.attributes' => ['uid', 'mail'],
+
+ // Additional filters that must match for the entire LDAP search to
+ // be true.
+ //
+ // This should be a single string conforming to [RFC 1960][2]
+ // and [RFC 2544][3]. The string is appended to the search attributes
+ 'search.filter' => '(&(objectClass=Person)(|(sn=Doe)(cn=John *)))',
+
+ // The username & password where SimpleSAMLphp should bind to before
+ // searching. If this is left NULL, no bind will be performed before
+ // searching.
+ 'search.username' => null,
+ 'search.password' => null,
+ ],
+ */
+
+ /*
+ // Example of an LDAPMulti authentication source.
+ 'example-ldapmulti' => [
+ 'ldap:LdapMulti',
+
+ // The way the organization as part of the username should be handled.
+ // Three possible values:
+ // - 'none': No handling of the organization. Allows '@' to be part
+ // of the username.
+ // - 'allow': Will allow users to type 'username@organization'.
+ // - 'force': Force users to type 'username@organization'. The dropdown
+ // list will be hidden.
+ //
+ // The default is 'none'.
+ 'username_organization_method' => 'none',
+
+ // Whether the organization should be included as part of the username
+ // when authenticating. If this is set to TRUE, the username will be on
+ // the form @. If this is FALSE, the
+ // username will be used as the user enters it.
+ //
+ // The default is FALSE.
+ 'include_organization_in_username' => false,
+
+ // A list of available LDAP servers.
+ //
+ // The index is an identifier for the organization/group. When
+ // 'username_organization_method' is set to something other than 'none',
+ // the organization-part of the username is matched against the index.
+ //
+ // The value of each element is an array in the same format as an LDAP
+ // authentication source.
+ 'mapping' => [
+ 'employees' => [
+ // A short name/description for this group. Will be shown in a
+ // dropdown list when the user logs on.
+ //
+ // This option can be a string or an array with
+ // language => text mappings.
+ 'description' => 'Employees',
+ 'authsource' => 'example-ldap',
+ ],
+
+ 'students' => [
+ 'description' => 'Students',
+ 'authsource' => 'example-ldap-2',
+ ],
+ ],
+ ],
+ */
+];
diff --git a/tests/unit/src/Bridges/SspBridge/Auth/SourceTest.php b/tests/unit/src/Bridges/SspBridge/Auth/SourceTest.php
new file mode 100644
index 00000000..63c0989d
--- /dev/null
+++ b/tests/unit/src/Bridges/SspBridge/Auth/SourceTest.php
@@ -0,0 +1,23 @@
+assertTrue(in_array('admin', $this->sut()->getSources()));
+ }
+}
diff --git a/tests/unit/src/Bridges/SspBridge/AuthTest.php b/tests/unit/src/Bridges/SspBridge/AuthTest.php
new file mode 100644
index 00000000..d573f64b
--- /dev/null
+++ b/tests/unit/src/Bridges/SspBridge/AuthTest.php
@@ -0,0 +1,28 @@
+assertInstanceOf(Auth::class, $this->sut());
+ }
+
+ public function testCanBuildSourceInstance(): void
+ {
+ $this->assertInstanceOf(Auth\Source::class, $this->sut()->source());
+ }
+}
diff --git a/tests/unit/src/Bridges/SspBridgeTest.php b/tests/unit/src/Bridges/SspBridgeTest.php
index bd4da0aa..12ab86a5 100644
--- a/tests/unit/src/Bridges/SspBridgeTest.php
+++ b/tests/unit/src/Bridges/SspBridgeTest.php
@@ -30,4 +30,9 @@ public function testCanBuildModuleInstance(): void
{
$this->assertInstanceOf(SspBridge\Module::class, $this->sut()->module());
}
+
+ public function testCanBuildAuthInstance(): void
+ {
+ $this->assertInstanceOf(SspBridge\Auth::class, $this->sut()->auth());
+ }
}
diff --git a/tests/unit/src/Forms/ClientFormTest.php b/tests/unit/src/Forms/ClientFormTest.php
index 59700f2e..fbe200dc 100644
--- a/tests/unit/src/Forms/ClientFormTest.php
+++ b/tests/unit/src/Forms/ClientFormTest.php
@@ -4,12 +4,14 @@
namespace SimpleSAML\Test\Module\oidc\unit\Forms;
+use DateTimeImmutable;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
-use SimpleSAML\Configuration;
+use SimpleSAML\Module\oidc\Bridges\SspBridge;
+use SimpleSAML\Module\oidc\Codebooks\RegistrationTypeEnum;
use SimpleSAML\Module\oidc\Forms\ClientForm;
use SimpleSAML\Module\oidc\Forms\Controls\CsrfProtection;
use SimpleSAML\Module\oidc\ModuleConfig;
@@ -19,14 +21,16 @@
*/
class ClientFormTest extends TestCase
{
- /** @var \PHPUnit\Framework\MockObject\MockObject */
- protected MockObject $csrfProtection;
+ protected MockObject $csrfProtectionMock;
- /** @var \PHPUnit\Framework\MockObject\MockObject */
- protected MockObject $moduleConfig;
+ protected MockObject $moduleConfigMock;
- /** @var \PHPUnit\Framework\MockObject\MockObject */
protected MockObject $serverRequestMock;
+ protected MockObject $sspBridgeMock;
+ protected MockObject $sspBridgeAuthMock;
+ protected MockObject $sspBridgeAuthSourceMock;
+
+ protected array $clientDataSample;
/**
* @throws \Exception
@@ -34,10 +38,63 @@ class ClientFormTest extends TestCase
public function setUp(): void
{
parent::setUp();
- Configuration::clearInternalState();
- $this->csrfProtection = $this->createMock(CsrfProtection::class);
- $this->moduleConfig = $this->createMock(ModuleConfig::class);
+ $this->csrfProtectionMock = $this->createMock(CsrfProtection::class);
+ $this->moduleConfigMock = $this->createMock(ModuleConfig::class);
$this->serverRequestMock = $this->createMock(ServerRequest::class);
+ $this->sspBridgeMock = $this->createMock(SspBridge::class);
+
+ $this->sspBridgeAuthMock = $this->createMock(SspBridge\Auth::class);
+ $this->sspBridgeMock->method('auth')->willReturn($this->sspBridgeAuthMock);
+
+ $this->sspBridgeAuthSourceMock = $this->createMock(SspBridge\Auth\Source::class);
+ $this->sspBridgeAuthMock->method('source')->willReturn($this->sspBridgeAuthSourceMock);
+
+ $this->clientDataSample = [
+ 'id' => 'clientId',
+ 'secret' => 'clientSecret',
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'auth_source' => 'default-sp',
+ 'redirect_uri' => [0 => 'https://example.com/redirect',],
+ 'scopes' => [0 => 'openid', 1 => 'offline_access', 2 => 'profile',],
+ 'is_enabled' => false,
+ 'is_confidential' => true,
+ 'owner' => null,
+ 'post_logout_redirect_uri' => [0 => 'https://example.com/',],
+ 'backchannel_logout_uri' => 'https://example.com/logout',
+ 'entity_identifier' => 'https://example.com/',
+ 'client_registration_types' => [0 => 'automatic',],
+ 'federation_jwks' => ['keys' => [0 => [],],],
+ 'jwks' => ['keys' => [0 => [],],],
+ 'jwks_uri' => 'https://example.com/jwks',
+ 'signed_jwks_uri' => 'https://example.com/signed-jwks',
+ 'registration_type' => RegistrationTypeEnum::Manual,
+ 'updated_at' => DateTimeImmutable::__set_state(
+ ['date' => '2025-02-05 15:05:27.000000', 'timezone_type' => 3, 'timezone' => 'UTC',],
+ ),
+ 'created_at' => DateTimeImmutable::__set_state(
+ ['date' => '2024-12-01 11:54:12.000000', 'timezone_type' => 3, 'timezone' => 'UTC',],
+ ),
+ 'expires_at' => null,
+ 'is_federated' => false,
+ 'allowed_origin' => [],
+ ];
+ }
+
+ protected function sut(
+ ?ModuleConfig $moduleConfig = null,
+ ?CsrfProtection $csrfProtection = null,
+ ?SspBridge $sspBridge = null,
+ ): ClientForm {
+ $moduleConfig ??= $this->moduleConfigMock;
+ $csrfProtection ??= $this->csrfProtectionMock;
+ $sspBridge ??= $this->sspBridgeMock;
+
+ return new ClientForm(
+ $moduleConfig,
+ $csrfProtection,
+ $sspBridge,
+ );
}
public static function validateOriginProvider(): array
@@ -74,7 +131,6 @@ public static function validateOriginProvider(): array
];
}
-
/**
* @param string $url
* @param bool $isValid
@@ -86,19 +142,26 @@ public static function validateOriginProvider(): array
#[TestDox('Allowed Origin URL: $url is expected to be $isValid')]
public function testValidateOrigin(string $url, bool $isValid): void
{
- $clientForm = $this->prepareMockedInstance();
+ $clientForm = $this->sut();
$clientForm->setValues(['allowed_origin' => $url]);
$clientForm->validateAllowedOrigin($clientForm);
$this->assertEquals(!$isValid, $clientForm->hasErrors(), $url);
}
- /**
- * @return \SimpleSAML\Module\oidc\Forms\ClientForm
- * @throws \Exception
- */
- protected function prepareMockedInstance(): ClientForm
+ public function testSetDefaultsLeavesValidAuthSourceValue(): void
{
- return new ClientForm($this->moduleConfig, $this->csrfProtection);
+ $this->sspBridgeAuthSourceMock->method('getSources')->willReturn(['default-sp']);
+
+ $sut = $this->sut()->setDefaults($this->clientDataSample);
+
+ $this->assertSame('default-sp', $sut->getValues()['auth_source']);
+ }
+
+ public function testSetDefaultsUnsetsAuthSourceIfNotValid(): void
+ {
+ $sut = $this->sut()->setDefaults($this->clientDataSample);
+
+ $this->assertNull($sut->getValues()['auth_source']);
}
}