Skip to content

Commit 3760db1

Browse files
committed
OPNsense/Auth - add minimalistic interface for SSO support.
In order to support other authentication methods for the business edition in the future, we need some glue to offer room for a different authentication flow. When using an SSO flow, the service binding is usually determined by the endpoint which acts as a trampoline. This commits mildly refactors our AuthenticationFactory and adds some handles to be able to choose SSO providers when available (using our standard listServers() call) ISSOContainer describes the minimal interface a container of providers should support, the Provider class is a "struct" type container offering relevant information for the provider in question.
1 parent f64287f commit 3760db1

File tree

9 files changed

+217
-28
lines changed

9 files changed

+217
-28
lines changed

plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,8 @@
503503
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/Local.php
504504
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php
505505
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/Radius.php
506+
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/SSOProviders/ISSOContainer.php
507+
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/SSOProviders/Provider.php
506508
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/Services/IPsec.php
507509
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/Services/System.php
508510
/usr/local/opnsense/mvc/app/library/OPNsense/Auth/Services/WebGui.php

src/etc/inc/auth.inc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,8 @@ function auth_get_authserver($name)
538538
return false;
539539
}
540540

541-
function auth_get_authserver_list()
541+
function auth_get_authserver_list($service='')
542542
{
543-
return (new \OPNsense\Auth\AuthenticationFactory())->listServers();
543+
return (new \OPNsense\Auth\AuthenticationFactory())->listServers($service);
544544
}
545545

src/opnsense/mvc/app/library/OPNsense/Auth/AuthenticationFactory.php

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,32 +48,51 @@ class AuthenticationFactory
4848
private function listConnectors()
4949
{
5050
$connectors = [];
51+
$interface = 'OPNsense\\Auth\\IAuthConnector';
5152
foreach (glob(__DIR__ . "/*.php") as $filename) {
52-
$pathParts = explode('/', $filename);
53-
$vendor = $pathParts[count($pathParts) - 3];
54-
$module = $pathParts[count($pathParts) - 2];
55-
$classname = explode('.php', $pathParts[count($pathParts) - 1])[0];
56-
$reflClass = new \ReflectionClass("{$vendor}\\{$module}\\{$classname}");
57-
if ($reflClass->implementsInterface('OPNsense\\Auth\\IAuthConnector')) {
58-
if ($reflClass->hasMethod('getType')) {
59-
$connectorType = $reflClass->getMethod('getType')->invoke(null);
60-
$connector = [];
61-
$connector['class'] = "{$vendor}\\{$module}\\{$classname}";
62-
$connector['classHandle'] = $reflClass;
63-
$connector['type'] = $connectorType;
64-
$connectors[$connectorType] = $connector;
65-
}
53+
$classname = 'OPNsense\\Auth\\' . explode('.php', basename($filename))[0];
54+
$reflClass = new \ReflectionClass($classname);
55+
if (!$reflClass->isInterface() && $reflClass->implementsInterface($interface)) {
56+
$connectorType = $reflClass->getMethod('getType')->invoke(null);
57+
$connectors[$connectorType] = [
58+
'class' => $classname,
59+
'classHandle' => $reflClass,
60+
'type' => $connectorType
61+
];
6662
}
6763
}
6864
return $connectors;
6965
}
7066

67+
/**
68+
* @param string $service filter on service when offered
69+
* @return array list of SSO providers
70+
*/
71+
public function listSSOproviders(string $service=''): array
72+
{
73+
$result = [];
74+
$interface = 'OPNsense\Auth\SSOProviders\\ISSOContainer';
75+
foreach (glob(__DIR__ . "/SSOProviders/*.php") as $filename) {
76+
$classname = 'OPNsense\\Auth\\SSOProviders\\' . explode('.php', basename($filename))[0];
77+
$reflClass = new \ReflectionClass($classname);
78+
if (!$reflClass->isInterface() && $reflClass->implementsInterface($interface)) {
79+
foreach (($reflClass->newInstance())->listProviders() as $provider) {
80+
if (empty($service) || $provider->service == $service) {
81+
$result[] = $provider;
82+
}
83+
}
84+
}
85+
}
86+
return $result;
87+
}
88+
7189
/**
7290
* request list of configured servers, the factory needs to be aware of its options and settings to
7391
* be able to instantiate useful connectors.
92+
* @param string $service name of the service we request our authenticators for
7493
* @return array list of configured servers
7594
*/
76-
public function listServers()
95+
public function listServers(string $service='')
7796
{
7897
$servers = [];
7998
$servers['Local Database'] = array("name" => "Local Database", "type" => "local");
@@ -88,6 +107,16 @@ public function listServers()
88107
}
89108
}
90109

110+
if (!empty($service)) {
111+
/**
112+
* Single sign on providers are bound to their service, which is different than our usual
113+
* user/pass authentication flow in which case the authenticate() method passes the requested service
114+
*/
115+
foreach ($this->listSSOproviders($service) as $provider) {
116+
$servers[$provider->id] = $provider->asArray();
117+
}
118+
}
119+
91120
return $servers;
92121
}
93122

@@ -98,8 +127,8 @@ public function listServers()
98127
*/
99128
public function get($authserver)
100129
{
101-
$servers = $this->listServers();
102-
$servers['Local API'] = array("name" => "Local API Database", "type" => "api");
130+
$servers = $this->listServers(); /* only servers, no SSO providers */
131+
$servers['Local API'] = ["name" => "Local API Database", "type" => "api"];
103132
// create a new auth connector
104133
if (isset($servers[$authserver]['type'])) {
105134
$connectors = $this->listConnectors();

src/opnsense/mvc/app/library/OPNsense/Auth/IAuthConnector.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
*/
3737
interface IAuthConnector
3838
{
39+
/**
40+
* @return string type of this authenticator
41+
*/
42+
public static function getType();
43+
3944
/**
4045
* set connector properties
4146
* @param array $config set configuration for this connector to use
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/**
4+
* Copyright (C) 2025 Deciso B.V.
5+
*
6+
* All rights reserved.
7+
*
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice,
12+
* this list of conditions and the following disclaimer.
13+
*
14+
* 2. Redistributions in binary form must reproduce the above copyright
15+
* notice, this list of conditions and the following disclaimer in the
16+
* documentation and/or other materials provided with the distribution.
17+
*
18+
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
19+
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
20+
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
21+
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
22+
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
* POSSIBILITY OF SUCH DAMAGE.
28+
*
29+
*/
30+
31+
namespace OPNsense\Auth\SSOProviders;
32+
33+
/**
34+
* Interface ISSOContainer defines required methods to support SSO types, a container may yield multiple providers
35+
* which are usually defined in models.
36+
* @package OPNsense\Auth\SSOProviders
37+
*/
38+
interface ISSOContainer
39+
{
40+
41+
/**
42+
* yield provider objects (servers) offered by this container
43+
*/
44+
public function listProviders(): \Generator;
45+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/**
4+
* Copyright (C) 2025 Deciso B.V.
5+
*
6+
* All rights reserved.
7+
*
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice,
12+
* this list of conditions and the following disclaimer.
13+
*
14+
* 2. Redistributions in binary form must reproduce the above copyright
15+
* notice, this list of conditions and the following disclaimer in the
16+
* documentation and/or other materials provided with the distribution.
17+
*
18+
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
19+
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
20+
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
21+
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
22+
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
* POSSIBILITY OF SUCH DAMAGE.
28+
*
29+
*/
30+
31+
namespace OPNsense\Auth\SSOProviders;
32+
33+
class Provider
34+
{
35+
public readonly string $id; /* unique id for this provider */
36+
public readonly string $appcode; /* short app code */
37+
public readonly string $name; /* the name of this provider */
38+
public readonly string $login_uri; /* (default) link to start SSO procedure */
39+
public readonly string $service; /* service this provider belongs to */
40+
public readonly string $html_content; /* optional html content to render for "login using" phrase*/
41+
42+
/**
43+
* load properties of this object on construct
44+
*/
45+
public function __construct(array $props)
46+
{
47+
foreach (get_class_vars(get_class($this)) as $key => $value) {
48+
if (isset($props[$key])) {
49+
$this->$key = $props[$key];
50+
} else {
51+
$this->$key = '';
52+
}
53+
}
54+
}
55+
56+
/**
57+
* @return array this providers settings as array
58+
*/
59+
public function asArray(): array
60+
{
61+
$result = ['type' => 'sso'];
62+
foreach (get_class_vars(get_class($this)) as $key => $value) {
63+
$result[$key] = $this->$key;
64+
}
65+
return $result;
66+
}
67+
68+
/**
69+
* @return string html content to use to render login link
70+
*/
71+
public function renderLink()
72+
{
73+
if (!empty($this->html_content)) {
74+
return $this->html_content;
75+
} else {
76+
return sprintf(
77+
gettext("Login using <a href='%s'>%s</a>"),
78+
$this->login_uri,
79+
$this->name
80+
);
81+
}
82+
}
83+
}

src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/AuthenticationServerField.php

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright (C) 2015-2019 Deciso B.V.
4+
* Copyright (C) 2015-2025 Deciso B.V.
55
* All rights reserved.
66
*
77
* Redistribution and use in source and binary forms, with or without
@@ -39,12 +39,17 @@ class AuthenticationServerField extends BaseListField
3939
/**
4040
* @var array collected options
4141
*/
42-
private static $internalStaticOptionList = array();
42+
private static $internalStaticOptionList = [];
4343

4444
/**
4545
* @var array filters to use on the authservers list
4646
*/
47-
private $internalFilters = array();
47+
private array $internalFilters = [];
48+
49+
/**
50+
* @var string optionally pass service we're requesting our servers for
51+
*/
52+
private string $internalService = '';
4853

4954
/**
5055
* @var string key to use for option selections, to prevent excessive reloading
@@ -57,10 +62,10 @@ class AuthenticationServerField extends BaseListField
5762
protected function actionPostLoadingEvent()
5863
{
5964
if (!isset(self::$internalStaticOptionList[$this->internalCacheKey])) {
60-
self::$internalStaticOptionList[$this->internalCacheKey] = array();
65+
self::$internalStaticOptionList[$this->internalCacheKey] = [];
6166

6267
$authFactory = new \OPNsense\Auth\AuthenticationFactory();
63-
$allAuthServers = $authFactory->listServers();
68+
$allAuthServers = $authFactory->listServers($this->internalService);
6469

6570
foreach ($allAuthServers as $key => $value) {
6671
// use filters to determine relevance
@@ -78,7 +83,7 @@ protected function actionPostLoadingEvent()
7883
}
7984
}
8085
if ($isMatched) {
81-
self::$internalStaticOptionList[$this->internalCacheKey][$key] = $key;
86+
self::$internalStaticOptionList[$this->internalCacheKey][$key] = $value['name'];
8287
}
8388
}
8489
natcasesort(self::$internalStaticOptionList[$this->internalCacheKey]);
@@ -89,13 +94,25 @@ protected function actionPostLoadingEvent()
8994
/**
9095
* set filters to use (in regex) per field, all tags are combined
9196
* and cached for the next object using the same filters
92-
* @param $filters filters to use
97+
* @param array $filters filters to use
9398
*/
9499
public function setFilters($filters)
95100
{
96101
if (is_array($filters)) {
97102
$this->internalFilters = $filters;
98-
$this->internalCacheKey = md5(serialize($this->internalFilters));
103+
$this->internalCacheKey = md5(serialize($this->internalFilters) . $this->internalService);
104+
}
105+
}
106+
107+
/**
108+
* set service type to limit results too
109+
* @param string $service service name
110+
*/
111+
public function setService($service)
112+
{
113+
if (is_string($service)) {
114+
$this->internalService = $service;
115+
$this->internalCacheKey = md5(serialize($this->internalFilters) . $this->internalService);
99116
}
100117
}
101118
}

src/www/authgui.inc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,13 @@ function display_login_form($Login_Error)
414414

415415
</form>
416416

417+
418+
<?php foreach ((new \OPNsense\Auth\AuthenticationFactory())->listSSOproviders('WebGui') as $provider):?>
419+
<div class="login-sso-link-container">
420+
<?=$provider->renderLink();?>
421+
</div>
422+
<?php endforeach;?>
423+
417424
<?php if (!$have_cookies && isset($_POST['login'])) : ?>
418425
<br /><br />
419426
<span class="text-danger">

src/www/system_advanced_admin.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
require_once("system.inc");
3737

3838
$a_group = &config_read_array('system', 'group');
39-
$a_authmode = auth_get_authserver_list();
39+
/* XXX: both webgui and console(ssh) use the same config reference, but may not support the same options */
40+
$a_authmode = auth_get_authserver_list('WebGui');
4041
$ssh_rekeylimit_choices = [
4142
'' => gettext('System defaults'),
4243
'default 60s' => gettext('60 seconds'),

0 commit comments

Comments
 (0)