Skip to content

Commit c5922e6

Browse files
Run session token renewals in a database transaction
The session token renewal does 1) Read the old token 2) Write a new token 3) Delete the old token If two processes succeed to read the old token there can be two new tokens because the queries were not run in a transaction. This is particularly problematic on clustered DBs where 1) would go to a read node and 2) and 3) go to a write node. Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
1 parent 495d49a commit c5922e6

File tree

2 files changed

+50
-25
lines changed

2 files changed

+50
-25
lines changed

lib/private/Authentication/Token/PublicKeyTokenProvider.php

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,18 @@
3434
use OC\Authentication\Exceptions\TokenPasswordExpiredException;
3535
use OC\Authentication\Exceptions\PasswordlessTokenException;
3636
use OC\Authentication\Exceptions\WipeTokenException;
37+
use OCP\AppFramework\Db\TTransactional;
3738
use OCP\Cache\CappedMemoryCache;
3839
use OCP\AppFramework\Db\DoesNotExistException;
3940
use OCP\AppFramework\Utility\ITimeFactory;
4041
use OCP\IConfig;
42+
use OCP\IDBConnection;
4143
use OCP\Security\ICrypto;
4244
use Psr\Log\LoggerInterface;
4345

4446
class PublicKeyTokenProvider implements IProvider {
47+
use TTransactional;
48+
4549
/** @var PublicKeyTokenMapper */
4650
private $mapper;
4751

@@ -51,6 +55,8 @@ class PublicKeyTokenProvider implements IProvider {
5155
/** @var IConfig */
5256
private $config;
5357

58+
private IDBConnection $db;
59+
5460
/** @var LoggerInterface */
5561
private $logger;
5662

@@ -63,11 +69,13 @@ class PublicKeyTokenProvider implements IProvider {
6369
public function __construct(PublicKeyTokenMapper $mapper,
6470
ICrypto $crypto,
6571
IConfig $config,
72+
IDBConnection $db,
6673
LoggerInterface $logger,
6774
ITimeFactory $time) {
6875
$this->mapper = $mapper;
6976
$this->crypto = $crypto;
7077
$this->config = $config;
78+
$this->db = $db;
7179
$this->logger = $logger;
7280
$this->time = $time;
7381

@@ -164,31 +172,32 @@ public function getTokenById(int $tokenId): IToken {
164172
public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
165173
$this->cache->clear();
166174

167-
$token = $this->getToken($oldSessionId);
168-
169-
if (!($token instanceof PublicKeyToken)) {
170-
throw new InvalidTokenException("Invalid token type");
171-
}
175+
return $this->atomic(function () use ($oldSessionId, $sessionId) {
176+
$token = $this->getToken($oldSessionId);
172177

173-
$password = null;
174-
if (!is_null($token->getPassword())) {
175-
$privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
176-
$password = $this->decryptPassword($token->getPassword(), $privateKey);
177-
}
178-
179-
$newToken = $this->generateToken(
180-
$sessionId,
181-
$token->getUID(),
182-
$token->getLoginName(),
183-
$password,
184-
$token->getName(),
185-
IToken::TEMPORARY_TOKEN,
186-
$token->getRemember()
187-
);
188-
189-
$this->mapper->delete($token);
178+
if (!($token instanceof PublicKeyToken)) {
179+
throw new InvalidTokenException("Invalid token type");
180+
}
190181

191-
return $newToken;
182+
$password = null;
183+
if (!is_null($token->getPassword())) {
184+
$privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
185+
$password = $this->decryptPassword($token->getPassword(), $privateKey);
186+
}
187+
$newToken = $this->generateToken(
188+
$sessionId,
189+
$token->getUID(),
190+
$token->getLoginName(),
191+
$password,
192+
$token->getName(),
193+
IToken::TEMPORARY_TOKEN,
194+
$token->getRemember()
195+
);
196+
197+
$this->mapper->delete($token);
198+
199+
return $newToken;
200+
}, $this->db);
192201
}
193202

194203
public function invalidateToken(string $token) {

tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<?php
2+
3+
declare(strict_types=1);
4+
25
/**
36
* @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl>
47
*
@@ -34,6 +37,7 @@
3437
use OCP\AppFramework\Utility\ITimeFactory;
3538
use OCP\IConfig;
3639
use OCP\Security\ICrypto;
40+
use PHPUnit\Framework\MockObject\MockObject;
3741
use Psr\Log\LoggerInterface;
3842
use Test\TestCase;
3943

@@ -46,6 +50,8 @@ class PublicKeyTokenProviderTest extends TestCase {
4650
private $crypto;
4751
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
4852
private $config;
53+
/** @var IDBConnection|IDBConnection|MockObject */
54+
private IDBConnection $db;
4955
/** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
5056
private $logger;
5157
/** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
@@ -66,14 +72,24 @@ protected function setUp(): void {
6672
['secret', '', '1f4h9s'],
6773
['openssl', [], []],
6874
]);
75+
$this->db = $this->createMock(IDBConnection::class);
76+
$this->db->method('atomic')->willReturnCallback(function ($cb) {
77+
return $cb();
78+
});
6979
$this->logger = $this->createMock(LoggerInterface::class);
7080
$this->timeFactory = $this->createMock(ITimeFactory::class);
7181
$this->time = 1313131;
7282
$this->timeFactory->method('getTime')
7383
->willReturn($this->time);
7484

75-
$this->tokenProvider = new PublicKeyTokenProvider($this->mapper, $this->crypto, $this->config, $this->logger,
76-
$this->timeFactory);
85+
$this->tokenProvider = new PublicKeyTokenProvider(
86+
$this->mapper,
87+
$this->crypto,
88+
$this->config,
89+
$this->db,
90+
$this->logger,
91+
$this->timeFactory,
92+
);
7793
}
7894

7995
public function testGenerateToken() {

0 commit comments

Comments
 (0)