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 736b605e..bd55b1a2 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -16,19 +16,25 @@ use OCA\UserOIDC\Event\ExchangedTokenRequestedEvent; use OCA\UserOIDC\Listener\ExchangedTokenRequestedListener; 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\Service\TokenService; -use OCA\UserOIDC\User\Backend; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\IL10N; use OCP\IRequest; +use OCP\ISession; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Security\ISecureRandom; + +// this is needed only for the special, shortened client login flow +use Psr\Container\ContainerInterface; use Throwable; class Application extends App implements IBootstrap { @@ -43,14 +49,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); @@ -70,12 +85,69 @@ public function boot(IBootContext $context): void { try { $context->injectFn(\Closure::fromCallable([$this, 'registerRedirect'])); $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) { } } - private function checkLoginToken(TokenService $tokenService): void { - $tokenService->checkLoginToken(); + /** + * 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 { 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();