Skip to content
Open
15 changes: 8 additions & 7 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
use OCA\UserOIDC\Listener\InternalTokenRequestedListener;
use OCA\UserOIDC\Listener\TimezoneHandlingListener;
use OCA\UserOIDC\Listener\TokenInvalidatedListener;
use OCA\UserOIDC\MagentaBearer\MBackend;
use OCA\UserOIDC\Service\ID4MeService;
use OCA\UserOIDC\Service\RequestClassificationService;
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;
Expand All @@ -42,7 +42,6 @@ class Application extends App implements IBootstrap {
public const APP_ID = 'user_oidc';
public const OIDC_API_REQ_HEADER = 'Authorization';

private $backend;
private $cachedProviders;

public function __construct(array $urlParams = []) {
Expand All @@ -54,14 +53,14 @@ public function register(IRegistrationContext $context): void {
$userManager = $this->getContainer()->get(IUserManager::class);

/* Register our own user backend */
$this->backend = $this->getContainer()->get(Backend::class);
$backend = $this->getContainer()->get(MBackend::class);

$config = $this->getContainer()->get(IConfig::class);
if (version_compare($config->getSystemValueString('version', '0.0.0'), '32.0.0', '>=')) {
// see https://docs.nextcloud.com/server/latest/developer_manual/app_publishing_maintenance/app_upgrade_guide/upgrade_to_32.html#id3
$userManager->registerBackend($this->backend);
$userManager->registerBackend($backend);
} else {
\OC_User::useBackend($this->backend);
\OC_User::useBackend($backend);
}

$context->registerEventListener(LoadAdditionalScriptsEvent::class, TimezoneHandlingListener::class);
Expand All @@ -83,8 +82,10 @@ public function register(IRegistrationContext $context): void {
}

public function boot(IBootContext $context): void {
$context->injectFn(\Closure::fromCallable([$this->backend, 'injectSession']));
$context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken']));
/** @var MBackend $backend */
$backend = $this->getContainer()->get(MBackend::class);
$context->injectFn(\Closure::fromCallable([$backend, 'injectSession']));
// $context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken']));
/** @var IUserSession $userSession */
$userSession = $this->getContainer()->get(IUserSession::class);
if ($userSession->isLoggedIn()) {
Expand Down
148 changes: 148 additions & 0 deletions lib/MagentaBearer/MBackend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 T-Systems International
* SPDX-License-Identifier: AGPL-3.0-only
*/

namespace OCA\UserOIDC\MagentaBearer;

use OCA\UserOIDC\AppInfo\Application;
use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Db\UserMapper;
use OCA\UserOIDC\Service\DiscoveryService;
use OCA\UserOIDC\Service\ProviderService;
use OCA\UserOIDC\Service\ProvisioningDeniedException;
use OCA\UserOIDC\Service\ProvisioningEventService;
use OCA\UserOIDC\User\AbstractOidcBackend;
use OCP\Authentication\IApacheBackend;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Security\ICrypto;
use Psr\Log\LoggerInterface;

class MBackend extends AbstractOidcBackend implements IApacheBackend {

public function __construct(
IConfig $config,
UserMapper $userMapper,
LoggerInterface $logger,
IRequest $request,
ISession $session,
IURLGenerator $urlGenerator,
IEventDispatcher $eventDispatcher,
DiscoveryService $discoveryService,
ProviderMapper $providerMapper,
ProviderService $providerService,
IUserManager $userManager,
protected ICrypto $crypto,
protected TokenService $mtokenService,
protected ProvisioningEventService $provisioningService,
) {
parent::__construct(
$config,
$userMapper,
$logger,
$request,
$session,
$urlGenerator,
$eventDispatcher,
$discoveryService,
$providerMapper,
$providerService,
$userManager,
);
}

public function getBackendName(): string {
return Application::APP_ID . '\\MagentaBearer';
}

/**
* Backend is activated if a bearer token header is detected.
*/
public function isSessionActive(): bool {
$headerToken = $this->request->getHeader(Application::OIDC_API_REQ_HEADER);

return preg_match('/^\s*bearer\s+/i', $headerToken) === 1;
}

public function getCurrentUserId(): string {
$headerToken = $this->request->getHeader(Application::OIDC_API_REQ_HEADER);

if (preg_match('/^\s*bearer\s+/i', $headerToken) !== 1) {
$this->logger->debug('No Bearer token');
return '';
}

$headerToken = preg_replace('/^\s*bearer\s+/i', '', $headerToken);
if (!is_string($headerToken) || $headerToken === '') {
$this->logger->debug('No Bearer token');
return '';
}

$providers = $this->providerMapper->getProviders();
if (count($providers) === 0) {
$this->logger->debug('No OIDC providers');
return '';
}

foreach ($providers as $provider) {
if ($this->providerService->getSetting($provider->getId(), ProviderService::SETTING_CHECK_BEARER, '0') !== '1') {
continue;
}

try {
$sharedSecret = $this->crypto->decrypt($provider->getBearerSecret());
$bearerToken = $this->mtokenService->decryptToken($headerToken, $sharedSecret);
$this->mtokenService->verifySignature($bearerToken, $sharedSecret);

$payload = $this->mtokenService->decode($bearerToken);
$this->mtokenService->verifyClaims($payload, ['http://auth.magentacloud.de']);
} catch (InvalidTokenException $e) {
$this->logger->debug('Invalid token: ' . $e->getMessage() . '. Trying another provider.');
continue;
} catch (SignatureException $e) {
$this->logger->debug($e->getMessage() . '. Trying another provider.');
continue;
} catch (\Throwable $e) {
$this->logger->debug('General non-matching provider problem: ' . $e->getMessage());
continue;
}

$uidAttribute = $this->providerService->getSetting($provider->getId(), ProviderService::SETTING_MAPPING_UID, 'sub');
$userId = is_object($payload) ? ($payload->{$uidAttribute} ?? null) : null;

if (!$this->isAcceptableUserId($userId)) {
$this->logger->debug('No extractable user id, check mapping!');
return '';
}

try {
$provisioningResult = $this->provisioningService->provisionUser($userId, $provider->getId(), $payload);
$provisionedUser = $provisioningResult['user'] ?? null;

if ($provisionedUser instanceof IUser) {
$userId = $provisionedUser->getUID();
}

$this->checkFirstLogin($userId);

return $userId;
} catch (ProvisioningDeniedException $e) {
$this->logger->error('Bearer token access denied: ' . $e->getMessage());
return '';
}
}

$this->logger->debug('Could not find provider for token');

return '';
}
}
192 changes: 192 additions & 0 deletions lib/User/AbstractOidcBackend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 T-Systems International
* SPDX-License-Identifier: AGPL-3.0-only
*/

namespace OCA\UserOIDC\User;

use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Db\UserMapper;
use OCA\UserOIDC\Service\DiscoveryService;
use OCA\UserOIDC\Service\ProviderService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\DB\Exception;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\Files\ISetupManager;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Server;
use OCP\User\Backend\ABackend;
use OCP\User\Backend\ICustomLogout;
use OCP\User\Backend\IGetDisplayNameBackend;
use OCP\User\Backend\ILimitAwareCountUsersBackend;
use OCP\User\Backend\IPasswordConfirmationBackend;
use OCP\User\Events\UserFirstTimeLoggedInEvent;
use Psr\Log\LoggerInterface;

abstract class AbstractOidcBackend extends ABackend implements IPasswordConfirmationBackend, IGetDisplayNameBackend, ICustomLogout, ILimitAwareCountUsersBackend {

public function __construct(
protected IConfig $config,
protected UserMapper $userMapper,
protected LoggerInterface $logger,
protected IRequest $request,
protected ISession $session,
protected IURLGenerator $urlGenerator,
protected IEventDispatcher $eventDispatcher,
protected DiscoveryService $discoveryService,
protected ProviderMapper $providerMapper,
protected ProviderService $providerService,
protected IUserManager $userManager,
) {
}

public function countUsers(int $limit = 0): int|false {
try {
$count = $this->userMapper->countUsers();

if ($limit > 0 && $count > $limit) {
return $limit;
}

return $count;
} catch (\Throwable $e) {
$this->logger->error('Failed to count OIDC users', [
'exception' => $e,
]);

return false;
}
}

public function deleteUser($uid): bool {
if (!is_string($uid) || $uid === '') {
return false;
}

try {
$user = $this->userMapper->getUser($uid);
$this->userMapper->delete($user);
return true;
} catch (DoesNotExistException $e) {
$this->logger->info('Tried to delete non-existent user', [
'uid' => $uid,
'exception' => $e,
]);
return false;
} catch (Exception $e) {
$this->logger->error('Failed to delete user', [
'uid' => $uid,
'exception' => $e,
]);
return false;
}
}

public function getUsers($search = '', $limit = null, $offset = null): array {
if (!is_string($search)
|| ($limit !== null && !is_int($limit))
|| ($offset !== null && !is_int($offset))
) {
return [];
}

return array_map(
static fn ($user) => $user->getUserId(),
$this->userMapper->find($search, $limit, $offset)
);
}

public function userExists($uid): bool {
return is_string($uid) && $uid !== '' && $this->userMapper->userExists($uid);
}

public function getDisplayName($uid): string {
if (!is_string($uid) || $uid === '') {
return (string)$uid;
}

try {
$user = $this->userMapper->getUser($uid);
return $user->getDisplayName();
} catch (DoesNotExistException) {
return $uid;
}
}

public function getDisplayNames($search = '', $limit = null, $offset = null): array {
if (!is_string($search)
|| ($limit !== null && !is_int($limit))
|| ($offset !== null && !is_int($offset))
) {
return [];
}

return $this->userMapper->findDisplayNames($search, $limit, $offset);
}

public function hasUserListings(): bool {
return true;
}

public function canConfirmPassword(string $uid): bool {
return false;
}

public function injectSession(ISession $session): void {
$this->session = $session;
}

public function getLogoutUrl(): string {
return $this->urlGenerator->linkToRouteAbsolute('user_oidc.login.singleLogoutService');
}

protected function isAcceptableUserId(mixed $userId): bool {
return is_string($userId) && trim($userId) !== '';
}

protected function checkFirstLogin(string $userId): bool {
$user = $this->userManager->get($userId);
if ($user === null) {
return false;
}

$firstLogin = $user->getLastLogin() === 0;

if ($firstLogin) {
try {
if (version_compare($this->config->getSystemValueString('version', '0.0.0'), '34.0.0', '>=')
&& interface_exists(ISetupManager::class)
) {
Server::get(ISetupManager::class)->setupForUser($user);
} else {
\OC_Util::setupFS($userId);
}

$userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
\OC_Util::copySkeleton($userId, $userFolder);
} catch (\Throwable $e) {
$this->logger->warning('Could not fully set up user filesystem on first login', [
'userId' => $userId,
'exception' => $e,
]);
}

if (class_exists(UserFirstTimeLoggedInEvent::class)) {
$this->eventDispatcher->dispatchTyped(new UserFirstTimeLoggedInEvent($user));
}
}

$user->updateLastLoginTimestamp();

return $firstLogin;
}
}
Loading