Skip to content

Commit 5b43335

Browse files
ChristophWurstrullzer
authored andcommitted
Handle token insert conflicts
Env-based SAML uses the "Apache auth" mechanism to log users in. In this code path, we first delete all existin auth tokens from the database, before a new one is inserted. This is problematic for concurrent requests as they might reach the same code at the same time, hence both trying to insert a new row wit the same token (the session ID). This also bubbles up and disables user_saml. As the token might still be OK (both request will insert the same data), we can actually just check if the UIDs of the conflict row is the same as the one we want to insert right now. In that case let's just use the existing entry and carry on. Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
1 parent daa6362 commit 5b43335

File tree

2 files changed

+68
-20
lines changed

2 files changed

+68
-20
lines changed

lib/private/Authentication/Token/Manager.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
namespace OC\Authentication\Token;
2525

26+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
2627
use OC\Authentication\Exceptions\ExpiredTokenException;
2728
use OC\Authentication\Exceptions\InvalidTokenException;
2829
use OC\Authentication\Exceptions\PasswordlessTokenException;
@@ -60,15 +61,29 @@ public function generateToken(string $token,
6061
string $name,
6162
int $type = IToken::TEMPORARY_TOKEN,
6263
int $remember = IToken::DO_NOT_REMEMBER): IToken {
63-
return $this->publicKeyTokenProvider->generateToken(
64-
$token,
65-
$uid,
66-
$loginName,
67-
$password,
68-
$name,
69-
$type,
70-
$remember
71-
);
64+
try {
65+
return $this->publicKeyTokenProvider->generateToken(
66+
$token,
67+
$uid,
68+
$loginName,
69+
$password,
70+
$name,
71+
$type,
72+
$remember
73+
);
74+
} catch (UniqueConstraintViolationException $e) {
75+
// It's rare, but if two requests of the same session (e.g. env-based SAML)
76+
// try to create the session token they might end up here at the same time
77+
// because we use the session ID as token and the db token is created anew
78+
// with every request.
79+
//
80+
// If the UIDs match, then this should be fine.
81+
$existing = $this->getToken($token);
82+
if ($existing->getUID() !== $uid) {
83+
throw new \Exception('Token conflict handled, but UIDs do not match. This should not happen', 0, $e);
84+
}
85+
return $existing;
86+
}
7287
}
7388

7489
/**

tests/lib/Authentication/Token/ManagerTest.php

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<?php
1+
<?php declare(strict_types=1);
2+
23
/**
34
* @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl>
45
*
@@ -23,29 +24,23 @@
2324

2425
namespace Test\Authentication\Token;
2526

27+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
2628
use OC\Authentication\Exceptions\InvalidTokenException;
2729
use OC\Authentication\Exceptions\PasswordlessTokenException;
2830
use OC\Authentication\Token\DefaultToken;
2931
use OC\Authentication\Token\DefaultTokenProvider;
3032
use OC\Authentication\Token\Manager;
3133
use OC\Authentication\Token\PublicKeyToken;
32-
use OC\Authentication\Token\PublicKeyTokenMapper;
3334
use OC\Authentication\Token\PublicKeyTokenProvider;
34-
use OC\Authentication\Token\ExpiredTokenException;
3535
use OC\Authentication\Token\IToken;
36-
use OCP\AppFramework\Db\DoesNotExistException;
37-
use OCP\AppFramework\Utility\ITimeFactory;
38-
use OCP\IConfig;
39-
use OCP\ILogger;
40-
use OCP\IUser;
41-
use OCP\Security\ICrypto;
36+
use PHPUnit\Framework\MockObject\MockObject;
4237
use Test\TestCase;
4338

4439
class ManagerTest extends TestCase {
4540

46-
/** @var PublicKeyTokenProvider|\PHPUnit_Framework_MockObject_MockObject */
41+
/** @var PublicKeyTokenProvider|MockObject */
4742
private $publicKeyTokenProvider;
48-
/** @var DefaultTokenProvider|\PHPUnit_Framework_MockObject_MockObject */
43+
/** @var DefaultTokenProvider|MockObject */
4944
private $defaultTokenProvider;
5045
/** @var Manager */
5146
private $manager;
@@ -92,6 +87,44 @@ public function testGenerateToken() {
9287
$this->assertSame($token, $actual);
9388
}
9489

90+
public function testGenerateConflictingToken() {
91+
/** @var MockObject|UniqueConstraintViolationException $exception */
92+
$exception = $this->createMock(UniqueConstraintViolationException::class);
93+
$this->defaultTokenProvider->expects($this->never())
94+
->method('generateToken');
95+
96+
$token = new PublicKeyToken();
97+
$token->setUid('uid');
98+
99+
$this->publicKeyTokenProvider->expects($this->once())
100+
->method('generateToken')
101+
->with(
102+
'token',
103+
'uid',
104+
'loginName',
105+
'password',
106+
'name',
107+
IToken::TEMPORARY_TOKEN,
108+
IToken::REMEMBER
109+
)->willThrowException($exception);
110+
$this->publicKeyTokenProvider->expects($this->once())
111+
->method('getToken')
112+
->with('token')
113+
->willReturn($token);
114+
115+
$actual = $this->manager->generateToken(
116+
'token',
117+
'uid',
118+
'loginName',
119+
'password',
120+
'name',
121+
IToken::TEMPORARY_TOKEN,
122+
IToken::REMEMBER
123+
);
124+
125+
$this->assertSame($token, $actual);
126+
}
127+
95128
public function tokenData(): array {
96129
return [
97130
[new DefaultToken()],

0 commit comments

Comments
 (0)