Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions COPYING.DTAG
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 5 additions & 2 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
['name' => 'login#code', 'url' => '/code', 'verb' => 'GET'],
['name' => 'login#singleLogoutService', 'url' => '/sls', 'verb' => 'GET'],
['name' => 'login#backChannelLogout', 'url' => '/backchannel-logout/{providerIdentifier}', 'verb' => 'POST'],
// compatibility with NMC V24 until reconfig on SAM
['name' => 'login#telekomBackChannelLogout', 'url' => '/logout', 'verb' => 'POST'],

['name' => 'api#createUser', 'url' => '/user', 'verb' => 'POST'],
['name' => 'api#deleteUser', 'url' => '/user/{userId}', 'verb' => 'DELETE'],
// this is a security problem combined with Telekom provisioning, so we habe 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'],
Expand Down
84 changes: 78 additions & 6 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
use OCA\UserOIDC\Listener\TimezoneHandlingListener;
use OCA\UserOIDC\Service\ID4MeService;
use OCA\UserOIDC\Service\SettingsService;
use OCA\UserOIDC\User\Backend;
use OCA\UserOIDC\Service\ProvisioningService;
use OCA\UserOIDC\Service\ProvisioningEventService;
use OCA\UserOIDC\MagentaBearer\MBackend;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
Expand All @@ -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';
Expand All @@ -40,15 +47,20 @@
}

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);

Check failure on line 55 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedClass

lib/AppInfo/Application.php:55:19: UndefinedClass: Class, interface or enum named OCA\UserOIDC\Service\ProvisioningEventService does not exist (see https://psalm.dev/019)
});

/** @var IUserManager $userManager */
$userManager = $this->getContainer()->get(IUserManager::class);

/* Register our own user backend */
$this->backend = $this->getContainer()->get(Backend::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);
// TODO check if it can be replaced by $userManager->registerBackend($this->backend); in our case
$this->backend = $this->getContainer()->get(MBackend::class);

Check failure on line 62 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedClass

lib/AppInfo/Application.php:62:47: UndefinedClass: Class, interface or enum named OCA\UserOIDC\MagentaBearer\MBackend does not exist (see https://psalm.dev/019)
$userManager->registerBackend($this->backend);
OC_User::useBackend($this->backend);

$context->registerEventListener(LoadAdditionalScriptsEvent::class, TimezoneHandlingListener::class);
Expand All @@ -65,10 +77,70 @@
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) {
}
}

/**
* 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) {

Check failure on line 129 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

TypeDoesNotContainNull

lib/AppInfo/Application.php:129:8: TypeDoesNotContainNull: string does not contain null (see https://psalm.dev/090)
// 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');
Expand Down
1 change: 1 addition & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Loading