Skip to content

Commit 17d5580

Browse files
authored
Merge pull request #1022 from nextcloud/fix/noid/restore-session-state-after-cookie-login
fix(Session): restore session data after cookie login
2 parents 3448746 + cc0d7a6 commit 17d5580

File tree

17 files changed

+526
-54
lines changed

17 files changed

+526
-54
lines changed

appinfo/info.xml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
44
- SPDX-License-Identifier: AGPL-3.0-or-later
55
-->
6-
<info>
6+
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
77
<id>user_saml</id>
88
<name>SSO &amp; SAML authentication</name>
99
<summary>Authenticate using single sign-on</summary>
@@ -20,7 +20,7 @@ The following providers are supported and tested at the moment:
2020
* Any other provider that authenticates using the environment variable
2121
2222
While theoretically any other authentication provider implementing either one of those standards is compatible, we like to note that they are not part of any internal test matrix.]]></description>
23-
<version>7.1.1</version>
23+
<version>7.1.2-beta.1</version>
2424
<licence>agpl</licence>
2525
<author>Lukas Reschke</author>
2626
<namespace>User_SAML</namespace>
@@ -39,6 +39,9 @@ While theoretically any other authentication provider implementing either one of
3939
<dependencies>
4040
<nextcloud min-version="30" max-version="33" />
4141
</dependencies>
42+
<background-jobs>
43+
<job>OCA\User_SAML\Jobs\CleanSessionData</job>
44+
</background-jobs>
4245
<repair-steps>
4346
<post-migration>
4447
<step>OCA\User_SAML\Migration\RememberLocalGroupsForPotentialMigrations</step>

composer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
"post-update-cmd": [
2424
"[ $COMPOSER_DEV_MODE -eq 0 ] || composer bin all update --ansi"
2525
],
26-
"cs:fix": "php-cs-fixer fix",
27-
"cs:check": "php-cs-fixer fix --dry-run --diff",
28-
"psalm": "psalm",
29-
"psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType",
30-
"psalm:update-baseline": "psalm --threads=1 --update-baseline",
26+
"cs:fix": "@php php-cs-fixer fix",
27+
"cs:check": "@php php-cs-fixer fix --dry-run --diff",
28+
"psalm": "@php psalm",
29+
"psalm:fix": "@php psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType",
30+
"psalm:update-baseline": "@php psalm --threads=1 --update-baseline",
3131
"lint": "find . -name \\*.php -not -path '*/vendor/*' -print0 | xargs -0 -n1 php -l",
3232
"rector:check": "rector --dry-run",
3333
"rector:fix": "rector",

lib/AppInfo/Application.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
use OCA\User_SAML\DavPlugin;
1717
use OCA\User_SAML\GroupBackend;
1818
use OCA\User_SAML\GroupManager;
19+
use OCA\User_SAML\Listener\CookieLoginEventListener;
1920
use OCA\User_SAML\Listener\LoadAdditionalScriptsListener;
21+
use OCA\User_SAML\Listener\LoginEventListener;
2022
use OCA\User_SAML\Listener\SabrePluginEventListener;
2123
use OCA\User_SAML\Middleware\OnlyLoggedInMiddleware;
2224
use OCA\User_SAML\SAMLSettings;
25+
use OCA\User_SAML\Service\SessionService;
2326
use OCA\User_SAML\UserBackend;
2427
use OCP\AppFramework\App;
2528
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -38,6 +41,9 @@
3841
use OCP\IUserSession;
3942
use OCP\L10N\IFactory;
4043
use OCP\Server;
44+
use OCP\User\Events\BeforeUserLoggedInWithCookieEvent;
45+
use OCP\User\Events\UserLoggedInEvent;
46+
use OCP\User\Events\UserLoggedInWithCookieEvent;
4147
use Psr\Container\ContainerInterface;
4248
use Psr\Log\LoggerInterface;
4349
use Throwable;
@@ -53,11 +59,15 @@ public function register(IRegistrationContext $context): void {
5359
$context->registerMiddleware(OnlyLoggedInMiddleware::class);
5460
$context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadAdditionalScriptsListener::class);
5561
$context->registerEventListener(SabrePluginAddEvent::class, SabrePluginEventListener::class);
62+
$context->registerEventListener(BeforeUserLoggedInWithCookieEvent::class, CookieLoginEventListener::class);
63+
$context->registerEventListener(UserLoggedInWithCookieEvent::class, CookieLoginEventListener::class);
64+
$context->registerEventListener(UserLoggedInEvent::class, LoginEventListener::class);
5665
$context->registerService(DavPlugin::class, fn (ContainerInterface $c) => new DavPlugin(
5766
$c->get(ISession::class),
5867
$c->get(IConfig::class),
5968
$_SERVER,
60-
$c->get(SAMLSettings::class)
69+
$c->get(SAMLSettings::class),
70+
$c->get(SessionService::class),
6171
));
6272
}
6373

lib/Controller/SAMLController.php

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use OCA\User_SAML\Exceptions\UserFilterViolationException;
1818
use OCA\User_SAML\Helper\TXmlHelper;
1919
use OCA\User_SAML\SAMLSettings;
20+
use OCA\User_SAML\Service\SessionService;
2021
use OCA\User_SAML\UserBackend;
2122
use OCA\User_SAML\UserData;
2223
use OCA\User_SAML\UserResolver;
@@ -57,6 +58,7 @@ public function __construct(
5758
private UserData $userData,
5859
private ICrypto $crypto,
5960
private ITrustedDomainHelper $trustedDomainHelper,
61+
private SessionService $sessionService,
6062
) {
6163
parent::__construct($appName, $request);
6264
}
@@ -232,7 +234,7 @@ public function login(int $idp = 1): Http\RedirectResponse|Http\TemplateResponse
232234
if (empty($ssoUrl)) {
233235
$ssoUrl = $this->urlGenerator->getAbsoluteURL('/');
234236
}
235-
$this->session->set('user_saml.samlUserData', $_SERVER);
237+
$this->sessionService->prepareEnvironmentBasedSession($_SERVER);
236238
try {
237239
$this->userData->setAttributes($this->session->get('user_saml.samlUserData'));
238240
$this->autoprovisionIfPossible();
@@ -335,8 +337,8 @@ public function assertionConsumerService(): Http\RedirectResponse {
335337
$AuthNRequestID = $data['AuthNRequestID'];
336338
$idp = $data['Idp'];
337339
// need to keep the IdP config ID during session lifetime (SAMLSettings::getPrefix)
338-
$this->session->set('user_saml.Idp', $idp);
339-
if (is_null($AuthNRequestID) || $AuthNRequestID === '' || is_null($idp)) {
340+
$this->sessionService->storeIdentityProviderInSession($idp);
341+
if (is_null($AuthNRequestID) || $AuthNRequestID === '') {
340342
$this->logger->debug('Invalid auth payload', ['app' => 'user_saml']);
341343
return new Http\RedirectResponse($this->urlGenerator->getAbsoluteURL('/'));
342344
}
@@ -383,14 +385,8 @@ public function assertionConsumerService(): Http\RedirectResponse {
383385
return $response;
384386
}
385387

386-
$this->session->set('user_saml.samlUserData', $auth->getAttributes());
387-
$this->session->set('user_saml.samlNameId', $auth->getNameId());
388-
$this->session->set('user_saml.samlNameIdFormat', $auth->getNameIdFormat());
389-
$this->session->set('user_saml.samlNameIdNameQualifier', $auth->getNameIdNameQualifier());
390-
$this->session->set('user_saml.samlNameIdSPNameQualifier', $auth->getNameIdSPNameQualifier());
391-
$this->session->set('user_saml.samlSessionIndex', $auth->getSessionIndex());
392-
$this->session->set('user_saml.samlSessionExpiration', $auth->getSessionExpiration());
393-
$this->logger->debug('Session values set', ['app' => 'user_saml']);
388+
$this->sessionService->storeAuthDataInSession($auth);
389+
394390
try {
395391
$user = $this->userResolver->findExistingUser($this->userBackend->getCurrentUserId());
396392
$firstLogin = $user->updateLastLoginTimestamp();
@@ -510,14 +506,14 @@ public function singleLogoutService(): Http\RedirectResponse {
510506
*/
511507
private function tryProcessSLOResponse(?int $idp): array {
512508
$idps = ($idp !== null) ? [$idp] : array_keys($this->samlSettings->getListOfIdps());
513-
foreach ($idps as $idp) {
509+
foreach ($idps as $identityProviderId) {
514510
try {
515-
$auth = new Auth($this->samlSettings->getOneLoginSettingsArray($idp));
511+
$auth = new Auth($this->samlSettings->getOneLoginSettingsArray($identityProviderId));
516512
// validator (called with processSLO()) needs an XML entity loader
517513
$targetUrl = $this->callWithXmlEntityLoader(fn (): string => $auth->processSLO(
518514
true, // do not let processSLO to delete the entire session. Let userSession->logout do the job
519515
null,
520-
$this->samlSettings->usesSloWebServerDecode($idp),
516+
$this->samlSettings->usesSloWebServerDecode($identityProviderId),
521517
null,
522518
true
523519
));

lib/DavPlugin.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,41 @@
88
namespace OCA\User_SAML;
99

1010
use OCA\DAV\Connector\Sabre\Auth;
11-
use OCP\IConfig;
11+
use OCA\User_SAML\Service\SessionService;
12+
use OCP\AppFramework\Services\IAppConfig;
1213
use OCP\ISession;
1314
use Sabre\DAV\Server;
1415
use Sabre\DAV\ServerPlugin;
1516
use Sabre\HTTP\RequestInterface;
1617
use Sabre\HTTP\ResponseInterface;
1718

1819
class DavPlugin extends ServerPlugin {
19-
/** @var Server */
20-
private $server;
20+
/** @noinspection PhpPropertyOnlyWrittenInspection */
21+
private Server $server;
2122

2223
public function __construct(
2324
private readonly ISession $session,
24-
private readonly IConfig $config,
25-
private array $auth,
25+
private readonly IAppConfig $config,
26+
private readonly array $auth,
2627
private readonly SAMLSettings $samlSettings,
28+
private readonly SessionService $sessionService,
2729
) {
2830
}
2931

30-
public function initialize(Server $server) {
31-
// before auth
32+
public function initialize(Server $server): void {
3233
$server->on('beforeMethod:*', $this->beforeMethod(...), 9);
3334
$this->server = $server;
3435
}
3536

36-
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
37+
public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
3738
if (
38-
$this->config->getAppValue('user_saml', 'type') === 'environment-variable'
39+
$this->config->getAppValueString('type', 'unset') === 'environment-variable'
3940
&& !$this->session->exists('user_saml.samlUserData')
4041
) {
4142
$uidMapping = $this->samlSettings->get(1)['general-uid_mapping'];
4243
if (isset($this->auth[$uidMapping])) {
4344
$this->session->set(Auth::DAV_AUTHENTICATED, $this->auth[$uidMapping]);
44-
$this->session->set('user_saml.samlUserData', $this->auth);
45+
$this->sessionService->prepareEnvironmentBasedSession($this->auth);
4546
}
4647
}
4748
}

lib/Db/SessionData.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OCA\User_SAML\Db;
9+
10+
use OCA\User_SAML\Model\SessionData as SessionDataModel;
11+
use OCP\AppFramework\Db\Entity;
12+
use OCP\DB\Types;
13+
14+
/**
15+
* @method setTokenId(int $tokenId): void
16+
* @method getTokenId(): int
17+
*/
18+
class SessionData extends Entity {
19+
/** @var string */
20+
public $id;
21+
public ?string $data = null;
22+
protected ?int $tokenId = null;
23+
24+
public function __construct() {
25+
$this->addType('id', Types::STRING);
26+
// technically tokenId is BIGINT, effectively no difference in the Entity context.
27+
// It can be set to BIGINT once dropping NC 30 support.
28+
$this->addType('tokenId', Types::INTEGER);
29+
$this->addType('data', Types::TEXT);
30+
}
31+
32+
public function setId(string $id): void {
33+
$this->id = $id;
34+
$this->markFieldUpdated('id');
35+
}
36+
37+
public function setData(SessionDataModel $input): void {
38+
$this->data = json_encode($input);
39+
$this->markFieldUpdated('data');
40+
}
41+
42+
public function getData(): SessionDataModel {
43+
$deserialized = json_decode($this->data, true);
44+
return SessionDataModel::fromInputArray($deserialized);
45+
}
46+
}

lib/Db/SessionDataMapper.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OCA\User_SAML\Db;
9+
10+
use OCP\AppFramework\Db\DoesNotExistException;
11+
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
12+
use OCP\AppFramework\Db\QBMapper;
13+
use OCP\DB\Exception;
14+
use OCP\IDBConnection;
15+
16+
/** @template-extends QBMapper<SessionData> */
17+
class SessionDataMapper extends QBMapper {
18+
public const SESSION_DATA_TABLE_NAME = 'user_saml_session_data';
19+
20+
public function __construct(IDBConnection $db) {
21+
parent::__construct($db, self::SESSION_DATA_TABLE_NAME, SessionData::class);
22+
}
23+
24+
/**
25+
* @throws DoesNotExistException
26+
* @throws MultipleObjectsReturnedException
27+
* @throws Exception
28+
*/
29+
public function retrieve(string $sessionId): SessionData {
30+
$qb = $this->db->getQueryBuilder();
31+
$qb->select('*')
32+
->from(self::SESSION_DATA_TABLE_NAME)
33+
->where($qb->expr()->eq('id', $qb->createNamedParameter($sessionId)));
34+
return $this->findEntity($qb);
35+
}
36+
}

lib/Jobs/CleanSessionData.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OCA\User_SAML\Jobs;
9+
10+
use OCA\User_SAML\Db\SessionDataMapper;
11+
use OCP\AppFramework\Utility\ITimeFactory;
12+
use OCP\BackgroundJob\IJob;
13+
use OCP\BackgroundJob\TimedJob;
14+
use OCP\DB\QueryBuilder\IQueryBuilder;
15+
use OCP\IDBConnection;
16+
17+
class CleanSessionData extends TimedJob {
18+
protected const NC_AUTH_TOKEN_TABLE = 'authtoken';
19+
20+
public function __construct(
21+
protected IDBConnection $dbc,
22+
ITimeFactory $timeFactory,
23+
) {
24+
parent::__construct($timeFactory);
25+
26+
$this->setInterval(86400);
27+
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
28+
$this->setAllowParallelRuns(false);
29+
}
30+
31+
protected function run(mixed $argument): void {
32+
$missingSessionIds = $this->findInvalidatedSessions();
33+
$this->deleteInvalidatedSessions($missingSessionIds);
34+
}
35+
36+
protected function findInvalidatedSessions(): array {
37+
$qb = $this->dbc->getQueryBuilder();
38+
$qb->select('s.id')
39+
->from(SessionDataMapper::SESSION_DATA_TABLE_NAME, 's')
40+
->leftJoin('s', self::NC_AUTH_TOKEN_TABLE, 'a',
41+
$qb->expr()->eq('s.token_id', 'a.id'),
42+
)
43+
->where($qb->expr()->isNull('a.id'))
44+
->setMaxResults(1000);
45+
46+
$invalidatedSessionsResult = $qb->executeQuery();
47+
$invalidatedSessionIds = $invalidatedSessionsResult->fetchAll(\PDO::FETCH_COLUMN);
48+
$invalidatedSessionsResult->closeCursor();
49+
50+
return $invalidatedSessionIds;
51+
}
52+
53+
protected function deleteInvalidatedSessions(array $invalidatedSessionIds): void {
54+
$qb = $this->dbc->getQueryBuilder();
55+
$qb->delete(SessionDataMapper::SESSION_DATA_TABLE_NAME)
56+
->where($qb->expr()->in(
57+
'id',
58+
$qb->createNamedParameter($invalidatedSessionIds, IQueryBuilder::PARAM_STR_ARRAY)
59+
));
60+
$qb->executeStatement();
61+
}
62+
}

0 commit comments

Comments
 (0)