From 9ebabab8d75453000ee1497642b06a15b96c55fc Mon Sep 17 00:00:00 2001 From: Mauro Mura Date: Mon, 11 Nov 2024 17:51:03 +0100 Subject: [PATCH 1/2] Central customization setup changes --- .github/README.md | 56 ++++++++++++++++++++++++ COPYING.DTAG | 5 +++ appinfo/routes.php | 8 +++- lib/AppInfo/Application.php | 85 +++++++++++++++++++++++++++++++++++-- tests/bootstrap.php | 1 + 5 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 .github/README.md create mode 100644 COPYING.DTAG diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 00000000..768b7049 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,56 @@ +# MagentaCLOUD user_oidc + +Customisation of the Nextcloud delivered OpenID connect app for MagentaCLOUD. + +The app extends the standard `user_oidc` Nextcloud app, +see [upstream configuration hints for basic setup](https://github.com/nextcloud/user_oidc/blob/main/README.md) + + +## Feature: Event-based provisioning (upstream contribution candidate) +The mechanism allows to implement custom puser provisioning logic in a separate Nextcloud app by +registering and handling a attribute change and provisioning event: + +``` +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; + +class Application extends App implements IBootstrap { +... + public function register(IRegistrationContext $context): void { + $context->registerEventListener(AttributeMappedEvent::class, MyUserAttributeListener::class); + $context->registerEventListener(UserAccountChangeEvent::class, MyUserAccountChangeListener::class); + } +... +} +``` +The provisioning handler should return a `OCA\UserOIDC\Event\UserAccountChangeResult` object + +## Feature: Telekom-specific bearer token + +Due to historic reason, Telekom bearer tokens have a close to standard structure, but +require special security implementation in detail. The customisation overrides te standard + + +### Requiring web-token libraries +The central configuration branch `nmc/2372-central-setup` automatic merge will frequently fail if composer +upstream + +The fast and easy way to bring it back to sync with upstream is: +``` +git checkout nmc/2372-central-setup +git rebase --onto main nmc/2372-central-setup +# manually take over everything from upstream for composer.lock (TODO: automate that) + +# ALWAYS update web-token dependencies in composer.lock +# to avoid upstream conflicts. The lock file diff should only contain adds to upstream state! +composer update "web-token/jwt-*" +``` + + +### Configuring an additional Bearer preshared secret with provider +TODO + +### Testing Bearer secrets +TODO \ No newline at end of file diff --git a/COPYING.DTAG b/COPYING.DTAG new file mode 100644 index 00000000..958cd262 --- /dev/null +++ b/COPYING.DTAG @@ -0,0 +1,5 @@ +Although this Nextcloud app code is free and available under the AGPL3 license, Deutsche Telekom +(including T-Systems) fully reserves all rights to the Telekom brand. To prevent users from getting confused about +the source of a digital product or experience, there are stringent restrictions on using the Telekom brand and design, +even when built into code that we provide. For any customization other than explicitly for Telekom or T-Systems, you must +replace the Deutsche Telekom and T-Systems brand elements contained in the provided sources. \ No newline at end of file diff --git a/appinfo/routes.php b/appinfo/routes.php index 15dcd72a..3707fbaa 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -17,8 +17,12 @@ ['name' => 'login#singleLogoutService', 'url' => '/sls', 'verb' => 'GET'], ['name' => 'login#backChannelLogout', 'url' => '/backchannel-logout/{providerIdentifier}', 'verb' => 'POST'], - ['name' => 'api#createUser', 'url' => '/user', 'verb' => 'POST'], - ['name' => 'api#deleteUser', 'url' => '/user/{userId}', 'verb' => 'DELETE'], + // compatibility with NMC V24 until reconfig on SAM + ['name' => 'login#telekomBackChannelLogout', 'url' => '/logout', 'verb' => 'POST'], + + // this is a security problem combined with Telekom provisioning, so we have to disable the endpoint + // ['name' => 'api#createUser', 'url' => '/user', 'verb' => 'POST'], + // ['name' => 'api#deleteUser', 'url' => '/user/{userId}', 'verb' => 'DELETE'], ['name' => 'id4me#showLogin', 'url' => '/id4me', 'verb' => 'GET'], ['name' => 'id4me#login', 'url' => '/id4me', 'verb' => 'POST'], diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d3be011c..77abe3ae 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -14,9 +14,11 @@ use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\UserOIDC\Db\ProviderMapper; use OCA\UserOIDC\Listener\TimezoneHandlingListener; +use OCA\UserOIDC\MagentaBearer\MBackend; use OCA\UserOIDC\Service\ID4MeService; +use OCA\UserOIDC\Service\ProvisioningEventService; +use OCA\UserOIDC\Service\ProvisioningService; use OCA\UserOIDC\Service\SettingsService; -use OCA\UserOIDC\User\Backend; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -27,6 +29,11 @@ use OCP\IUserManager; use OCP\IUserSession; use Throwable; +use Psr\Container\ContainerInterface; + +// this is needed only for the special, shortened client login flow +use OCP\Security\ISecureRandom; +use OCP\ISession; class Application extends App implements IBootstrap { public const APP_ID = 'user_oidc'; @@ -40,14 +47,23 @@ public function __construct(array $urlParams = []) { } public function register(IRegistrationContext $context): void { + // Register the composer autoloader required for the added jwt-token libs + include_once __DIR__ . '/../../vendor/autoload.php'; + + // override registration of provisioning srevice to use event-based solution + $this->getContainer()->registerService(ProvisioningService::class, function (ContainerInterface $c): ProvisioningService { + return $c->get(ProvisioningEventService::class); + }); + /** @var IUserManager $userManager */ $userManager = $this->getContainer()->get(IUserManager::class); /* Register our own user backend */ - $this->backend = $this->getContainer()->get(Backend::class); + // $this->backend = $this->getContainer()->get(Backend::class); + $this->backend = $this->getContainer()->get(MBackend::class); // this was done before but OC_User::useBackend calls OC::$server->getUserManager()->registerBackend anyway // so the backend was registered twice, leading to wrong user count (double) - // $userManager->registerBackend($this->backend); + $userManager->registerBackend($this->backend); // TODO check if it can be replaced by $userManager->registerBackend($this->backend); in our case OC_User::useBackend($this->backend); @@ -64,11 +80,72 @@ public function boot(IBootContext $context): void { try { $context->injectFn(\Closure::fromCallable([$this, 'registerRedirect'])); - $context->injectFn(\Closure::fromCallable([$this, 'registerLogin'])); + $context->injectFn(\Closure::fromCallable([$this, 'registerLogin'])); + // this is the custom auto-redirect for MagentaCLOUD client access + $context->injectFn(\Closure::fromCallable([$this, 'registerNmcClientFlow'])); } catch (Throwable $e) { } } + /** + * This is the automatic redirect exclusively for Nextcloud/Magentacloud clients completely skipping consent layer + */ + private function registerNmcClientFlow(IRequest $request, + IURLGenerator $urlGenerator, + ProviderMapper $providerMapper, + ISession $session, + ISecureRandom $random): void { + $providers = $this->getCachedProviders($providerMapper); + + // Handle immediate redirect on client first-time login + $isClientLoginFlow = false; + + try { + $isClientLoginFlow = $request->getPathInfo() === '/login/flow'; + } catch (Exception $e) { + // in case any errors happen when checking for the path do not apply redirect logic as it is only needed for the login + } + + if ($isClientLoginFlow) { + // only redirect if Telekom provider registered + $tproviders = array_values(array_filter($providers, function ($p) { + return strtolower($p->getIdentifier()) === "telekom"; + })); + + if (count($tproviders) == 0) { + // always show normal login flow as error fallback + return; + } + + $stateToken = $random->generate(64, ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS); + $session->set('client.flow.state.token', $stateToken); + + // call the service to get the params, but suppress the template + // compute grant redirect Url to go directly to Telekom login + $redirectUrl = $urlGenerator->linkToRoute('core.ClientFlowLogin.grantPage', [ + 'stateToken' => $stateToken, + // grantPage service operation is deriving oauth2 client name (again), + // so we simply pass on clientIdentifier or empty string + 'clientIdentifier' => $request->getParam('clientIdentifier', ''), + 'direct' => $request->getParam('direct', '0') + ]); + + if ($redirectUrl === null) { + // always show normal login flow as error fallback + return; + } + + // direct login, consent layer later + $targetUrl = $urlGenerator->linkToRoute(self::APP_ID . '.login.login', [ + 'providerId' => $tproviders[0]->getId(), + 'redirectUrl' => $redirectUrl + ]); + + header('Location: ' . $targetUrl); + exit(); + } + } + private function registerRedirect(IRequest $request, IURLGenerator $urlGenerator, SettingsService $settings, ProviderMapper $providerMapper): void { $providers = $this->getCachedProviders($providerMapper); $redirectUrl = $request->getParam('redirect_url'); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d2d92753..7adc52f8 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -11,6 +11,7 @@ require_once __DIR__ . '/../vendor/autoload.php'; \OC::$loader->addValidRoot(OC::$SERVERROOT . '/tests'); +\OC::$composerAutoloader->addPsr4('OCA\\UserOIDC\\BaseTest\\', dirname(__FILE__) . '/unit/MagentaCloud/', true); \OC_App::loadApp('user_oidc'); OC_Hook::clear(); From 70fd265f5fdf27c1055e70d50500d5a7030aa482 Mon Sep 17 00:00:00 2001 From: Mauro Mura Date: Thu, 14 Nov 2024 10:29:42 +0100 Subject: [PATCH 2/2] fixed code style --- lib/AppInfo/Application.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 77abe3ae..e244efb9 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -25,15 +25,15 @@ use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\IL10N; use OCP\IRequest; +use OCP\ISession; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\IUserSession; -use Throwable; -use Psr\Container\ContainerInterface; +use OCP\Security\ISecureRandom; // this is needed only for the special, shortened client login flow -use OCP\Security\ISecureRandom; -use OCP\ISession; +use Psr\Container\ContainerInterface; +use Throwable; class Application extends App implements IBootstrap { public const APP_ID = 'user_oidc'; @@ -80,7 +80,7 @@ public function boot(IBootContext $context): void { try { $context->injectFn(\Closure::fromCallable([$this, 'registerRedirect'])); - $context->injectFn(\Closure::fromCallable([$this, 'registerLogin'])); + $context->injectFn(\Closure::fromCallable([$this, 'registerLogin'])); // this is the custom auto-redirect for MagentaCLOUD client access $context->injectFn(\Closure::fromCallable([$this, 'registerNmcClientFlow'])); } catch (Throwable $e) { @@ -91,10 +91,10 @@ public function boot(IBootContext $context): void { * This is the automatic redirect exclusively for Nextcloud/Magentacloud clients completely skipping consent layer */ private function registerNmcClientFlow(IRequest $request, - IURLGenerator $urlGenerator, - ProviderMapper $providerMapper, - ISession $session, - ISecureRandom $random): void { + IURLGenerator $urlGenerator, + ProviderMapper $providerMapper, + ISession $session, + ISecureRandom $random): void { $providers = $this->getCachedProviders($providerMapper); // Handle immediate redirect on client first-time login @@ -109,7 +109,7 @@ private function registerNmcClientFlow(IRequest $request, if ($isClientLoginFlow) { // only redirect if Telekom provider registered $tproviders = array_values(array_filter($providers, function ($p) { - return strtolower($p->getIdentifier()) === "telekom"; + return strtolower($p->getIdentifier()) === 'telekom'; })); if (count($tproviders) == 0) { @@ -117,7 +117,7 @@ private function registerNmcClientFlow(IRequest $request, return; } - $stateToken = $random->generate(64, ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS); + $stateToken = $random->generate(64, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); $session->set('client.flow.state.token', $stateToken); // call the service to get the params, but suppress the template