diff --git a/appinfo/app.php b/appinfo/app.php deleted file mode 100644 index 507897707..000000000 --- a/appinfo/app.php +++ /dev/null @@ -1,176 +0,0 @@ -getURLGenerator(); - $l = \OC::$server->getL10N('user_saml'); - $config = \OC::$server->getConfig(); - $request = \OC::$server->getRequest(); - $userSession = \OC::$server->getUserSession(); - $session = \OC::$server->getSession(); -} catch (Throwable $e) { - $logger = \OCP\Server::get(LoggerInterface::class); - $logger->critical($e->getMessage(), ['exception' => $e, 'app' => 'user_saml']); - return; -} - -$groupBackend = \OC::$server->get(GroupBackend::class); -\OC::$server->get(IGroupManager::class)->addBackend($groupBackend); - -$samlSettings = \OC::$server->get(SAMLSettings::class); - -$userBackend = \OCP\Server::get(UserBackend::class); -$userBackend->registerBackends(\OC::$server->getUserManager()->getBackends()); -OC_User::useBackend($userBackend); - -$params = []; - -// Setting up the one login config may fail, if so, do not catch the requests later. -$returnScript = false; -$type = ''; -switch ($config->getAppValue('user_saml', 'type')) { - case 'saml': - $type = 'saml'; - break; - case 'environment-variable': - $type = 'environment-variable'; - break; - default: - return; -} - -if ($type === 'environment-variable') { - // We should ignore oauth2 token endpoint (oauth can send the credentials as basic auth which will fail with apache auth) - $uri = $request->getRequestUri(); - if (substr($uri, -24) === '/apps/oauth/api/v1/token') { - return; - } - - try { - OC_User::handleApacheAuth(); - } catch (LoginException $e) { - if ($request->getPathInfo() === '/apps/user_saml/saml/error') { - return; - } - $targetUrl = $urlGenerator->linkToRouteAbsolute( - 'user_saml.SAML.genericError', - [ - 'message' => $e->getMessage() - ] - ); - header('Location: ' . $targetUrl); - exit(); - } -} - -if ($returnScript === true) { - return; -} - -$app = \OC::$server->query(\OCA\User_SAML\AppInfo\Application::class); -$app->registerDavAuth(); - -$redirectSituation = false; - -$user = $userSession->getUser(); -if ($user !== null) { - $enabled = $user->isEnabled(); - if ($enabled === false) { - if ($request->getPathInfo() === '/apps/user_saml/saml/error') { - return; - } - $targetUrl = $urlGenerator->linkToRouteAbsolute( - 'user_saml.SAML.genericError', - [ - 'message' => $l->t('This user account is disabled, please contact your administrator.') - ] - ); - header('Location: ' . $targetUrl); - exit(); - } -} - -// All requests that are not authenticated and match against the "/login" route are -// redirected to the SAML login endpoint -if (!$cli && - !$userSession->isLoggedIn() && - \OC::$server->getRequest()->getPathInfo() === '/login' && - $type !== '') { - try { - $params = $request->getParams(); - } catch (\LogicException $e) { - // ignore exception when PUT is called since getParams cannot parse parameters in that case - } - if (isset($params['direct']) && ($params['direct'] === 1 || $params['direct'] === '1')) { - return; - } - $redirectSituation = true; -} - -$multipleUserBackEnds = $samlSettings->allowMultipleUserBackEnds(); -$configuredIdps = $samlSettings->getListOfIdps(); -$showLoginOptions = ($multipleUserBackEnds || count($configuredIdps) > 1) && $type === 'saml'; - -if ($redirectSituation === true && $showLoginOptions) { - try { - $params = $request->getParams(); - } catch (\LogicException $e) { - // ignore exception when PUT is called since getParams cannot parse parameters in that case - } - $redirectUrl = ''; - if (isset($params['redirect_url'])) { - $redirectUrl = $params['redirect_url']; - } - - $targetUrl = $urlGenerator->linkToRouteAbsolute( - 'user_saml.SAML.selectUserBackEnd', - [ - 'redirectUrl' => $redirectUrl - ] - ); - header('Location: ' . $targetUrl); - exit(); -} - -if ($redirectSituation === true) { - try { - $params = $request->getParams(); - } catch (\LogicException $e) { - // ignore exception when PUT is called since getParams cannot parse parameters in that case - } - $originalUrl = ''; - if (isset($params['redirect_url'])) { - $originalUrl = $urlGenerator->getAbsoluteURL($params['redirect_url']); - } - - $csrfToken = \OC::$server->getCsrfTokenManager()->getToken(); - $targetUrl = $urlGenerator->linkToRouteAbsolute( - 'user_saml.SAML.login', - [ - 'requesttoken' => $csrfToken->getEncryptedValue(), - 'originalUrl' => $originalUrl, - 'idp' => array_keys($configuredIdps)[0] ?? '', - ] - ); - header('Location: ' . $targetUrl); - exit(); -} diff --git a/appinfo/info.xml b/appinfo/info.xml index 0dd691cfd..2225101ec 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -26,6 +26,7 @@ While theoretically any other authentication provider implementing either one of User_SAML + https://portal.nextcloud.com/article/configuring-single-sign-on-10.html @@ -60,4 +61,9 @@ While theoretically any other authentication provider implementing either one of OCA\User_SAML\Settings\Admin OCA\User_SAML\Settings\Section + + + OCA\User_SAML\DavPlugin + + diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index c9e517e91..150f395a2 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -1,5 +1,7 @@ getContainer(); - - /** - * Middleware - */ - $container->registerService('OnlyLoggedInMiddleware', function (ContainerInterface $c) { - return new OnlyLoggedInMiddleware( - $c->get(IControllerMethodReflector::class), - $c->get(IUserSession::class), - $c->get(IURLGenerator::class) - ); - }); + } - $container->registerService(DavPlugin::class, function (ContainerInterface $c) { + public function register(IRegistrationContext $context): void { + $context->registerMiddleware(OnlyLoggedInMiddleware::class); + $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadAdditionalScriptsListener::class); + $context->registerService(DavPlugin::class, function (ContainerInterface $c) { return new DavPlugin( $c->get(ISession::class), $c->get(IConfig::class), @@ -47,36 +58,164 @@ public function __construct(array $urlParams = []) { $c->get(SAMLSettings::class) ); }); - - $container->registerMiddleWare('OnlyLoggedInMiddleware'); - $this->timezoneHandling(); } - public function registerDavAuth(): void { - $dispatcher = Server::get(IEventDispatcher::class); - $dispatcher->addListener('OCA\DAV\Connector\Sabre::addPlugin', function (SabrePluginEvent $event) { - $event->getServer()->addPlugin(Server::get(DavPlugin::class)); - }); - } + public function boot(IBootContext $context): void { + try { + $context->injectFn(function ( + IL10N $l10n, + IURLGenerator $urlGenerator, + IConfig $config, + IRequest $request, + IUserSession $userSession, + ISession $session, + IFactory $factory, + SAMLSettings $samlSettings, + IUserManager $userManager, + IDBConnection $connection, + LoggerInterface $logger, + GroupManager $groupManager, + IEventDispatcher $dispatcher, + CsrfTokenManager $csrfTokenManager, + bool $isCLI, + ) { + $groupBackend = Server::get(GroupBackend::class); + Server::get(IGroupManager::class)->addBackend($groupBackend); - private function timezoneHandling(): void { - $userSession = Server::get(IUserSession::class); - $session = Server::get(ISession::class); - $config = Server::get(IConfig::class); + $samlSettings = Server::get(SAMLSettings::class); - $dispatcher = Server::get(IEventDispatcher::class); - $dispatcher->addListener(LoadAdditionalScriptsEvent::class, function () use ($session, $config, $userSession) { - if (!$userSession->isLoggedIn()) { - return; - } + $userBackend = Server::get(UserBackend::class); - $user = $userSession->getUser(); - $timezoneDB = $config->getUserValue($user->getUID(), 'core', 'timezone', ''); + $userBackend->registerBackends($userManager->getBackends()); + OC_User::useBackend($userBackend); - if ($timezoneDB === '' || !$session->exists('timezone')) { - Util::addScript('user_saml', 'vendor/jstz.min'); - Util::addScript('user_saml', 'timezone'); - } - }); + $params = []; + + // Setting up the one login config may fail, if so, do not catch the requests later. + switch ($config->getAppValue('user_saml', 'type')) { + case 'saml': + $type = 'saml'; + break; + case 'environment-variable': + $type = 'environment-variable'; + break; + default: + return; + } + + if ($type === 'environment-variable') { + // We should ignore oauth2 token endpoint (oauth can send the credentials as basic auth which will fail with apache auth) + $uri = $request->getRequestUri(); + if (str_ends_with($uri, '/apps/oauth/api/v1/token')) { + return; + } + + try { + OC_User::handleApacheAuth(); + } catch (LoginException $e) { + if ($request->getPathInfo() === '/apps/user_saml/saml/error') { + return; + } + $targetUrl = $urlGenerator->linkToRouteAbsolute( + 'user_saml.SAML.genericError', + [ + 'message' => $e->getMessage() + ] + ); + header('Location: ' . $targetUrl); + exit(); + } + } + + $redirectSituation = false; + + $user = $userSession->getUser(); + if ($user !== null) { + $enabled = $user->isEnabled(); + if ($enabled === false) { + if ($request->getPathInfo() === '/apps/user_saml/saml/error') { + return; + } + $targetUrl = $urlGenerator->linkToRouteAbsolute( + 'user_saml.SAML.genericError', + [ + 'message' => $l10n->t('This user account is disabled, please contact your administrator.') + ] + ); + header('Location: ' . $targetUrl); + exit(); + } + } + + // All requests that are not authenticated and match against the "/login" route are + // redirected to the SAML login endpoint + if (!$isCLI && + !$userSession->isLoggedIn() && + ($request->getPathInfo() === '/login')) { + try { + $params = $request->getParams(); + } catch (\LogicException $e) { + // ignore exception when PUT is called since getParams cannot parse parameters in that case + } + if (isset($params['direct']) && ($params['direct'] === 1 || $params['direct'] === '1')) { + return; + } + $redirectSituation = true; + } + + $multipleUserBackEnds = $samlSettings->allowMultipleUserBackEnds(); + $configuredIdps = $samlSettings->getListOfIdps(); + $showLoginOptions = $multipleUserBackEnds || count($configuredIdps) > 1; + + if ($redirectSituation === true && $showLoginOptions) { + try { + $params = $request->getParams(); + } catch (\LogicException $e) { + // ignore exception when PUT is called since getParams cannot parse parameters in that case + } + $redirectUrl = ''; + if (isset($params['redirect_url'])) { + $redirectUrl = $params['redirect_url']; + } + + $targetUrl = $urlGenerator->linkToRouteAbsolute( + 'user_saml.SAML.selectUserBackEnd', + [ + 'redirectUrl' => $redirectUrl + ] + ); + header('Location: ' . $targetUrl); + exit(); + } + + if ($redirectSituation === true) { + try { + $params = $request->getParams(); + } catch (\LogicException $e) { + // ignore exception when PUT is called since getParams cannot parse parameters in that case + } + $originalUrl = ''; + if (isset($params['redirect_url'])) { + $originalUrl = $urlGenerator->getAbsoluteURL($params['redirect_url']); + } + + $csrfToken = $csrfTokenManager->getToken(); + $targetUrl = $urlGenerator->linkToRouteAbsolute( + 'user_saml.SAML.login', + [ + 'requesttoken' => $csrfToken->getEncryptedValue(), + 'originalUrl' => $originalUrl, + 'idp' => array_keys($configuredIdps)[0] ?? '', + ] + ); + header('Location: ' . $targetUrl); + exit(); + } + }); + } catch (Throwable $e) { + Server::get(LoggerInterface::class)->critical('Error when loading user_saml app', [ + 'exception' => $e, + ]); + } } } diff --git a/lib/Listener/LoadAdditionalScriptsListener.php b/lib/Listener/LoadAdditionalScriptsListener.php new file mode 100644 index 000000000..dd5b29331 --- /dev/null +++ b/lib/Listener/LoadAdditionalScriptsListener.php @@ -0,0 +1,46 @@ + */ +class LoadAdditionalScriptsListener implements IEventListener { + public function __construct( + private ISession $session, + private IUserSession $userSession, + private IConfig $config, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof BeforeTemplateRenderedEvent) { + return; + } + + if (!$event->isLoggedIn()) { + return; + } + + $user = $this->userSession->getUser(); + $timezoneDB = $this->config->getUserValue($user->getUID(), 'core', 'timezone', ''); + + if ($timezoneDB === '' || !$this->session->exists('timezone')) { + Util::addScript('user_saml', 'vendor/jstz.min'); + Util::addScript('user_saml', 'timezone'); + } + } +} diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 9772c2300..dd0a1ef0d 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -5,19 +5,10 @@ --> - - - - - - - - getServer()]]> - @@ -36,12 +27,7 @@ - - - - - @@ -91,7 +77,6 @@ - diff --git a/tests/stub.phpstub b/tests/stub.phpstub index 8cfa75fd9..04451c801 100644 --- a/tests/stub.phpstub +++ b/tests/stub.phpstub @@ -52,3 +52,24 @@ namespace OC\DB\Exceptions { namespace OCP\Log { public function logger(?string $appId = null): \Psr\Log\LoggerInterface; } + +namespace OC\Security\CSRF { + class CsrfToken { + public function getEncryptedValue(): string { + return 'token'; + } + } + class CsrfTokenManager { + abstract public function getToken(): CsrfToken; + } +} + +namespace OC\User { + class LoginException extends \Exception { + } +} + +class OC_User { + public static function useBackend($userBackend): void; + public static function handleApacheAuth(): void; +} diff --git a/tests/unit/AppInfo/ApplicationTest.php b/tests/unit/AppInfo/ApplicationTest.php index 2398354ac..db394e555 100644 --- a/tests/unit/AppInfo/ApplicationTest.php +++ b/tests/unit/AppInfo/ApplicationTest.php @@ -28,7 +28,7 @@ public function testContainerAppName() { public function queryData() { return [ - ['OnlyLoggedInMiddleware', OnlyLoggedInMiddleware::class], + [OnlyLoggedInMiddleware::class], ]; } @@ -37,10 +37,7 @@ public function queryData() { * @param string $service * @param string $expected */ - public function testContainerQuery($service, $expected = null) { - if ($expected === null) { - $expected = $service; - } - $this->assertTrue($this->container->query($service) instanceof $expected); + public function testContainerQuery($serviceClass) { + $this->assertTrue($this->container->query($serviceClass) instanceof $serviceClass); } }