From 6a44d96d0eec1787930a46d61f3a557684d7f5df Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 16 Dec 2024 12:46:33 +0100 Subject: [PATCH] feat: add oidc support Signed-off-by: Julien Veyssier --- README.md | 15 ++++- lib/Controller/MasterController.php | 15 ++++- lib/Controller/SlaveController.php | 11 +++- lib/Master.php | 31 +++++++-- lib/Slave.php | 6 ++ .../UserDiscoveryOIDC.php | 64 +++++++++++++++++++ 6 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 lib/UserDiscoveryModules/UserDiscoveryOIDC.php diff --git a/README.md b/README.md index 69645a3..408a2b5 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Config.php parameters to operate the server in master mode: // The user disovery module might require additional config paramters you can find in // the documentation of the module 'gss.user.discovery.module' => '\OCA\GlobalSiteSelector\UserDiscoveryModules\UserDiscoverySAML', +// or 'gss.user.discovery.module' => '\OCA\GlobalSiteSelector\UserDiscoveryModules\UserDiscoveryOIDC' // define a allow list for automatic login to other instance to let browsers handle the redirect properly 'gss.master.csp-allow' => ['*.myorg.com', 'node3.otherorg.com'], @@ -80,13 +81,23 @@ specific use case: #### UserDiscoverySAML -This modules reads the location directly from a parameter of the IDP which contain -the exact URL to the server. The name of the parameter can be defined this way: +This modules reads the location directly from a parameter of the IDP which contains +the exact URL to the slave target server. The name of the parameter can be defined this way: ```` 'gss.discovery.saml.slave.mapping' => 'idp-parameter' ```` +#### UserDiscoveryOIDC + +This module is similar to UserDiscoverySAML. +It reads the location from an OIDC token attribute which contains +the exact URL to the slave target server. The attribute can be defined this way: + +```` +'gss.discovery.oidc.slave.mapping' => 'token-attribute' +```` + #### ManualUserMapping This allows you to maintain a custom json file which maps a specific key word diff --git a/lib/Controller/MasterController.php b/lib/Controller/MasterController.php index fddd2be..af1f5d9 100644 --- a/lib/Controller/MasterController.php +++ b/lib/Controller/MasterController.php @@ -67,13 +67,22 @@ public function autoLogout(?string $jwt) { if ($jwt !== null) { $key = $this->gss->getJwtKey(); $decoded = (array)JWT::decode($jwt, new Key($key, Application::JWT_ALGORITHM)); - $idp = $decoded['saml.idp'] ?? null; - $logoutUrl = $this->urlGenerator->linkToRoute('user_saml.SAML.singleLogoutService'); + // saml idp ID + $samlIdp = $decoded['saml.idp'] ?? null; + // oidc provider ID + $oidcProviderId = $decoded['oidc.providerId'] ?? ''; + + if (class_exists('\OCA\User_SAML\UserBackend')) { + $logoutUrl = $this->urlGenerator->linkToRoute('user_saml.SAML.singleLogoutService'); + } elseif (class_exists('\OCA\UserOIDC\User\Backend')) { + $logoutUrl = $this->urlGenerator->linkToRoute('user_oidc.login.singleLogoutService'); + } if (!empty($logoutUrl)) { $token = [ 'logout' => 'logout', - 'idp' => $idp, + 'idp' => $samlIdp, + 'oidcProviderId' => $oidcProviderId, 'exp' => time() + 300, // expires after 5 minutes ]; diff --git a/lib/Controller/SlaveController.php b/lib/Controller/SlaveController.php index 196417e..4fb5e6f 100644 --- a/lib/Controller/SlaveController.php +++ b/lib/Controller/SlaveController.php @@ -90,8 +90,9 @@ public function autoLogin(string $jwt): RedirectResponse { $this->logger->debug('uid: ' . $uid . ', options: ' . json_encode($options)); $target = $options['target']; - if (($options['backend'] ?? '') === 'saml') { - $this->logger->debug('saml enabled'); + $backend = $options['backend'] ?? ''; + if ($backend === 'saml' || $backend === 'oidc') { + $this->logger->debug('saml or oidc enabled: ' . $backend); $this->autoprovisionIfNeeded($uid, $options); $user = $this->userManager->get($uid); @@ -108,6 +109,12 @@ public function autoLogin(string $jwt): RedirectResponse { Slave::SAML_IDP, $options['saml']['idp'] ?? null ); + $this->config->setUserValue( + $user->getUID(), + Application::APP_ID, + Slave::OIDC_PROVIDER_ID, + $options['oidc']['providerId'] ?? '' + ); $result = true; } else { diff --git a/lib/Master.php b/lib/Master.php index 9d2d31c..006b8c6 100644 --- a/lib/Master.php +++ b/lib/Master.php @@ -104,10 +104,10 @@ public function handleLoginRequest( $userDiscoveryModule = $this->config->getSystemValueString('gss.user.discovery.module', ''); $this->logger->debug('handleLoginRequest: discovery module is: ' . $userDiscoveryModule); - $isSaml = false; + $isSamlOrOidc = false; if (class_exists('\OCA\User_SAML\UserBackend') && $backend instanceof \OCA\User_SAML\UserBackend) { - $isSaml = true; + $isSamlOrOidc = true; $this->logger->debug('handleLoginRequest: backend is SAML'); $options['backend'] = 'saml'; @@ -122,8 +122,29 @@ public function handleLoginRequest( ]; $this->logger->debug('handleLoginRequest: backend is SAML.', ['options' => $options]); + } elseif (class_exists('\OCA\UserOIDC\Controller\LoginController') + && class_exists('\OCA\UserOIDC\User\Backend') + && $backend instanceof \OCA\UserOIDC\User\Backend + && method_exists($backend, 'getUserData') + ) { + // TODO double check if we need to behave the same when saml or oidc is used + $isSamlOrOidc = true; + $this->logger->debug('handleLoginRequest: backend is OIDC'); + + $options['backend'] = 'oidc'; + $options['userData'] = $backend->getUserData(); + $uid = $options['userData']['formatted']['uid']; + $password = ''; + $discoveryData['oidc'] = $options['userData']['raw']; + // we only send the formatted user data to the slave + $options['userData'] = $options['userData']['formatted']; + $options['oidc'] = [ + 'providerId' => $this->session->get(\OCA\UserOIDC\Controller\LoginController::PROVIDERID) + ]; + + $this->logger->debug('handleLoginRequest: backend is OIDC.', ['options' => $options]); } else { - $this->logger->debug('handleLoginRequest: backend is not SAML'); + $this->logger->debug('handleLoginRequest: backend is not SAML or OIDC'); } $this->logger->debug('handleLoginRequest: uid is: ' . $uid); @@ -141,8 +162,8 @@ public function handleLoginRequest( } // first ask the lookup server if we already know the user - // is from SAML, only search on userId, ignore email. - $location = $this->queryLookupServer($uid, $isSaml); + // is from SAML or OIDC, only search on userId, ignore email. + $location = $this->queryLookupServer($uid, $isSamlOrOidc); $this->logger->debug('handleLoginRequest: location according to lookup server: ' . $location); // if not we fall-back to a initial user deployment method, if configured diff --git a/lib/Slave.php b/lib/Slave.php index fb777cc..fcefc63 100644 --- a/lib/Slave.php +++ b/lib/Slave.php @@ -19,6 +19,7 @@ class Slave { public const SAML_IDP = 'saml_idp'; + public const OIDC_PROVIDER_ID = 'oidc_provider_id'; private IUserManager $userManager; private IClientService $clientService; @@ -280,6 +281,11 @@ public function handleLogoutRequest(IUser $user) { Application::APP_ID, self::SAML_IDP, null), + 'oidc.providerId' => $this->config->getUserValue( + $user->getUID(), + Application::APP_ID, + self::OIDC_PROVIDER_ID, + null), 'exp' => time() + 300 // expires after 5 minute ]; diff --git a/lib/UserDiscoveryModules/UserDiscoveryOIDC.php b/lib/UserDiscoveryModules/UserDiscoveryOIDC.php new file mode 100644 index 0000000..10e0687 --- /dev/null +++ b/lib/UserDiscoveryModules/UserDiscoveryOIDC.php @@ -0,0 +1,64 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\GlobalSiteSelector\UserDiscoveryModules; + +use OCP\IConfig; + +/** + * Class UserDiscoveryOIDC + * + * Discover initial user location with a dedicated OIDC attribute + * + * Therefore you have to define two values in the config.php file: + * + * 'gss.discovery.oidc.slave.mapping' => 'token-attribute' + * 'gss.user.discovery.module' => '\OCA\GlobalSiteSelector\UserDiscoveryModules\UserDiscoveryOIDC' + * + * @package OCA\GlobalSiteSelector\UserDiscoveryModule + */ +class UserDiscoveryOIDC implements IUserDiscoveryModule { + private string $tokenLocationAttribute; + + public function __construct(IConfig $config) { + $this->tokenLocationAttribute = $config->getSystemValueString('gss.discovery.oidc.slave.mapping', ''); + } + + + /** + * read user location from OIDC token attribute + * + * @param array $data OIDC attributes to read the location from + * + * @return string + */ + public function getLocation(array $data): string { + $location = ''; + if (!empty($this->tokenLocationAttribute) && isset($data['oidc'][$this->tokenLocationAttribute])) { + $location = $data['oidc'][$this->tokenLocationAttribute]; + } + + return $location; + } +}