diff --git a/appinfo/info.xml b/appinfo/info.xml
index 8685f191..d6acbf76 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -8,7 +8,7 @@
OpenID Connect user backend
Use an OpenID Connect backend to login to your Nextcloud
Allows flexible configuration of an OIDC server as Nextcloud login user backend.
- 8.10.1
+ 8.1.1
agpl
Roeland Jago Douma
Julius Härtl
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 192220ca..9880be3f 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -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;
@@ -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 = []) {
@@ -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);
@@ -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()) {
diff --git a/lib/MagentaBearer/MBackend.php b/lib/MagentaBearer/MBackend.php
new file mode 100644
index 00000000..96010d96
--- /dev/null
+++ b/lib/MagentaBearer/MBackend.php
@@ -0,0 +1,148 @@
+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 '';
+ }
+}
diff --git a/lib/User/AbstractOidcBackend.php b/lib/User/AbstractOidcBackend.php
new file mode 100644
index 00000000..dad8e961
--- /dev/null
+++ b/lib/User/AbstractOidcBackend.php
@@ -0,0 +1,192 @@
+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;
+ }
+}