Skip to content
Merged
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
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
'url' => '/autologout',
'verb' => 'GET'
],
[
'name' => 'Master#finalizeOAuthFlow',
'url' => '/oauth2/login/flow',
'verb' => 'POST'
],
[
'name' => 'Slave#findFile',
'url' => '/gf/{token}/{fileId}',
Expand Down
11 changes: 6 additions & 5 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
use OCP\Server;
use OCP\User\Events\BeforeUserDeletedEvent;
use OCP\User\Events\BeforeUserLoggedInEvent;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserCreatedEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\User\Events\UserLoggedInEvent;
use OCP\User\Events\UserLoggedOutEvent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
Expand Down Expand Up @@ -69,7 +69,7 @@ public function register(IRegistrationContext $context): void {
$context->registerCapability(PublicCapabilities::class);

// events on master
$context->registerEventListener(UserLoggedInEvent::class, UserLoggingIn::class);
$context->registerEventListener(BeforeUserLoggedInEvent::class, UserLoggingIn::class);
$context->registerEventListener(
AddContentSecurityPolicyEvent::class,
AddContentSecurityPolicyListener::class
Expand Down Expand Up @@ -210,7 +210,8 @@ private function redirectToSlave(IRequest $request, Master $master, IUserSession
|| str_starts_with($uri, '/apps/globalsiteselector/autologout')
|| str_starts_with($uri, '/apps/user_saml/saml/sls')
|| str_starts_with($uri, '/apps/user_oidc/sls')
|| str_starts_with($uri, '/login/flow')
// we keep hand on /login/flow/grant that will be emulated by the app
|| (str_starts_with($uri, '/login/flow') && !str_starts_with($uri, '/login/flow/grant'))
) {
return;
}
Expand All @@ -222,11 +223,11 @@ private function redirectToSlave(IRequest $request, Master $master, IUserSession

$this->logger->debug('new redirectToSlave');
$master->handleLoginRequest(
$user,
$user->getUID(),
'',
$user->getBackend(),
true,
);

$this->logger->debug('ending redirectToSlave');
}
}
2 changes: 2 additions & 0 deletions lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ConfigLexicon implements ILexicon {
public const GS_TOKENS = 'globalScaleTokens';
public const LOCAL_TOKEN = 'localToken';
public const REDIRECT_WEBDAV = 'redirectWebDAV';
public const MANAGE_OAUTH2 = 'manageOAuth2';
public const SSO_USER_DATA = 'ssoUserData';

#[\Override]
Expand All @@ -34,6 +35,7 @@ public function getAppConfigs(): array {
new Entry(key: self::GS_TOKENS, type: ValueType::ARRAY, defaultRaw: [], definition: 'list of token+host to navigate through GlobalScale', lazy: true),
new Entry(key: self::LOCAL_TOKEN, type: ValueType::STRING, defaultRaw: '', definition: 'local token to id instance within GlobalScale'),
new Entry(key: self::REDIRECT_WEBDAV, type: ValueType::BOOL, defaultRaw: false, definition: 'redirect WebDAV request on Master to Slaves', lazy: false),
new Entry(key: self::MANAGE_OAUTH2, type: ValueType::BOOL, defaultRaw: false, definition: 'manage OAuth2 requests from Master', lazy: false),
];
}

Expand Down
27 changes: 27 additions & 0 deletions lib/Controller/MasterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
use OCA\GlobalSiteSelector\AppInfo\Application;
use OCA\GlobalSiteSelector\GlobalSiteSelector;
use OCA\GlobalSiteSelector\Master;
use OCA\GlobalSiteSelector\Service\OAuth2Service;
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\JWT;
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\Key;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCP\IURLGenerator;
Expand All @@ -34,11 +38,34 @@ public function __construct(
IRequest $request,
private readonly IURLGenerator $urlGenerator,
private readonly GlobalSiteSelector $gss,
private readonly OAuth2Service $oauth2Service,
private readonly LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}

#[PublicPage]
#[FrontpageRoute(verb: 'POST', url: '/test')]
public function finalizeOAuthFlow(
string $stateToken,
string $clientIdentifier = '',
string $providedRedirectUri = '',
): Response {
try {
return $this->oauth2Service->finalizeOAuth2(
$stateToken,
$clientIdentifier,
$providedRedirectUri,
$this->request->getHeader('user-agent'),
);
} catch (\Exception $e) {
$this->logger->warning('fail to manage oauth2', ['exception' => $e]);
$response = new Response();
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
}

#[PublicPage]
#[NoCSRFRequired]
#[UseSession]
Expand Down
9 changes: 5 additions & 4 deletions lib/Listeners/UserLoggingIn.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IRequest;
use OCP\User\Events\UserLoggedInEvent;
use OCP\User\Events\BeforeUserLoggedInEvent;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<UserLoggedInEvent>
* @template-implements IEventListener<BeforeUserLoggedInEvent>
*/
class UserLoggingIn implements IEventListener {

Expand All @@ -32,7 +32,7 @@ public function __construct(

#[\Override]
public function handle(Event $event): void {
if (!$event instanceof UserLoggedInEvent) {
if (!$event instanceof BeforeUserLoggedInEvent) {
return;
}

Expand All @@ -48,8 +48,9 @@ public function handle(Event $event): void {

$this->logger->debug('new BeforeUserLoggedInEvent event');
$this->master->handleLoginRequest(
$event->getUser(),
$event->getUsername(),
$event->getPassword(),
$event->getBackend()
);

$this->logger->debug('ending BeforeUserLoggedInEvent event');
Expand Down
61 changes: 31 additions & 30 deletions lib/Master.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@
use OCA\GlobalSiteSelector\AppInfo\Application;
use OCA\GlobalSiteSelector\Exceptions\IsLocalAdminException;
use OCA\GlobalSiteSelector\Service\GlobalScaleService;
use OCA\GlobalSiteSelector\Service\OAuth2Service;
use OCA\GlobalSiteSelector\Service\ToolsService;
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\JWT;
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\Key;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\Authentication\IApacheBackend;
use OCP\HintException;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IInitialStateService;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\Security\ICrypto;
use OCP\ServerVersion;
use OCP\UserInterface;
use OCP\Util;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
Expand All @@ -44,6 +48,7 @@ public function __construct(
private readonly ISession $session,
private readonly GlobalSiteSelector $gss,
private readonly ICrypto $crypto,
private readonly IInitialStateService $initialStateService,
private readonly LoginFlowV2Service $loginFlowV2Service,
private readonly ServerVersion $serverVersion,
private readonly Lookup $lookup,
Expand All @@ -53,6 +58,8 @@ public function __construct(
private readonly IConfig $config,
private readonly LoggerInterface $logger,
private readonly GlobalScaleService $globalScaleService,
private readonly ToolsService $toolsService,
private readonly OAuth2Service $oauth2Service,
) {
}

Expand All @@ -65,23 +72,22 @@ public function __construct(
* @throws NotFoundExceptionInterface
*/
public function handleLoginRequest(
IUser $user,
string $uid,
?string $password,
null|IApacheBackend|UserInterface $backend = null,
bool $ignoreJwt = false,
): void {
$backend = $user->getBackend();
$this->logger->debug(
'start handle login request',
[
'uid' => $user->getUID(),
'uid' => $uid,
'backend' => ($backend === null) ? null : $backend::class
]
);

/** ignoring request from slave with valid jwt */
if (!$ignoreJwt && $this->isValidJwt($this->request->getParam('jwt', ''))) {
$this->logger->debug('ignore request with valid jwt');

return;
}

Expand All @@ -95,9 +101,17 @@ public function handleLoginRequest(
'params' => $this->request->getParams(),
];

if ($this->toolsService->isPath(['/apps/globalsiteselector/oauth2/login/flow'], $target)) {
return;
}

if ($this->oauth2Service->manageOauth2($uid, $target)) {
return;
}

$redirectUrl = $this->request->getParam('redirect_url', '');

$ssoUserData = $this->globalScaleService->getSsoUserData($user);
$ssoUserData = $this->globalScaleService->getSsoUserData($uid, $backend);
if ($ssoUserData !== null && $ssoUserData['backend'] === 'saml') {
$this->logger->debug('handleLoginRequest: backend is SAML');

Expand Down Expand Up @@ -145,25 +159,22 @@ public function handleLoginRequest(
$this->logger->debug('handleLoginRequest: backend is not SAML or OIDC');
}

if ($this->isPath(['/login/flow', '/login/v2/flow'], $redirectUrl ?? '')) {
if ($this->toolsService->isPath(['/login/flow', '/login/v2/flow'], $redirectUrl ?? '')) {
$options['target'] = $redirectUrl;
$this->logger->debug('handleLoginRequest: overriding target with slave flow path: ' . $options['target']);
}

try {
$location = $this->globalScaleService->getSecondaryRemoteLocation($user);
$location = $this->globalScaleService->getSecondaryRemoteLocation($uid, $backend);
} catch (IsLocalAdminException) {
return;
}
if ($location !== null) {
$this->logger->debug(
'handleLoginRequest: redirecting user: ' . $user->getUID() . ' to ' . $location
);

$this->redirectUser($user->getUID(), $password, $location, $options);
if ($location !== null) {
$this->logger->debug('handleLoginRequest: redirecting user: ' . $uid . ' to ' . $location);
$this->redirectUser($uid, $password, $location, $options);
} else {
$this->logger->debug('handleLoginRequest: Could not find location for account ' . $user->getUID());

$this->logger->debug('handleLoginRequest: Could not find location for account ' . $uid);
throw new HintException('Unknown Account');
}
}
Expand All @@ -187,7 +198,7 @@ protected function redirectUser($uid, $password, $location, array $options = [])
'/mirall|csyncoC/', // <-- Support also not compliant Desktop Clients
'/^.*\(Android\)$/'
]
) || $this->isPath(['/login/flow/grant', '/login/v2/grant'], $options['target'] ?? '');
) || $this->toolsService->isPath(['/login/flow/grant', '/login/v2/grant'], $options['target'] ?? '');

$requestUri = $this->request->getRequestUri();

Expand Down Expand Up @@ -382,25 +393,15 @@ private function forceRelativeUrl(string $url): string {
return $url;
}

private function isPath(array $search, string $path): bool {
if ($path === '') {
return false;
}

foreach ($search as $entry) {
if (str_starts_with($path, (string)$entry) || str_starts_with($path, '/index.php' . $entry)) {
return true;
}
}

return false;
}

private function handleFlowDone(bool $result): StandaloneTemplateResponse {
if ($result) {
// login flow v2 templates were moved in NC33
if ($this->serverVersion->getMajorVersion() >= 33) {
Util::addScript('core', 'common');
Util::addScript('core', 'main');
Util::addTranslations('core');
Util::addScript('core', 'login_flow');
$this->initialStateService->provideInitialState('core', 'loginFlowState', 'done');
return new StandaloneTemplateResponse('core', 'loginflow', renderAs: 'guest');
}

Expand Down
19 changes: 10 additions & 9 deletions lib/Service/GlobalScaleService.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\JWT;
use OCA\GlobalSiteSelector\Vendor\Firebase\JWT\Key;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\IApacheBackend;
use OCP\Config\IUserConfig;
use OCP\GlobalScale\IGlobalScaleService;
use OCP\Http\Client\IClientService;
Expand All @@ -30,6 +31,7 @@
use OCP\IUser;
use OCP\Security\ISecureRandom;
use OCP\Server;
use OCP\UserInterface;
use Psr\Log\LoggerInterface;

trait TGlobalScaleService {
Expand Down Expand Up @@ -194,17 +196,17 @@ public function requestGssOcs(string $address, string $route, array $data = [],
*
* @return array{backend: 'saml'|'oidc', formatted: array, raw: array}|null
*/
public function getSsoUserData(IUser $user): ?array {
$uid = $user->getUID();

public function getSsoUserData(string $uid, null|IApacheBackend|UserInterface $backend): ?array {
$cached = $this->userConfig->getValueArray($uid, Application::APP_ID, ConfigLexicon::SSO_USER_DATA, [], lazy: true);
if ($cached !== []) {
return $cached;
}

$backend = $user->getBackend();
$data = null;
if ($backend === null) {
return null;
}

$data = null;
try {
if (class_exists('\OCA\User_SAML\UserBackend')
&& $backend instanceof \OCA\User_SAML\UserBackend) {
Expand Down Expand Up @@ -235,12 +237,11 @@ public function getSsoUserData(IUser $user): ?array {
*
* @throws IsLocalAdminException If the user is one of the local admin and shouldn't be redirected
*/
public function getSecondaryRemoteLocation(IUser $user): ?string {
$uid = $user->getUID();
public function getSecondaryRemoteLocation(string $uid, ?UserInterface $backend): ?string {
$discoveryData = [];
$isSamlOrOidc = false;

$ssoUserData = $this->getSsoUserData($user);
$ssoUserData = $this->getSsoUserData($uid, $backend);
if ($ssoUserData !== null) {
$isSamlOrOidc = true;
$this->logger->debug('getSecondaryRemoteLocation: backend is ' . $ssoUserData['backend']);
Expand Down Expand Up @@ -315,7 +316,7 @@ protected function normalizeLocation(string $url): string {
}

public function sendToSecondary(IUser $user, string $path, array $payload): string {
$location = $this->getSecondaryRemoteLocation($user);
$location = $this->getSecondaryRemoteLocation($user->getUID(), $user->getBackend());
if ($location === null) {
throw new \Exception('Could not send message to secondary. No secondary location found for user with id: ' . $user->getUID());
}
Expand Down
Loading
Loading