Skip to content

Commit a7b0c65

Browse files
authored
Merge pull request #59796 from nextcloud/backport/59785/stable26
[stable26] fix: Reduce the mixups between apptokens and session ids
2 parents 0294eda + 4fd438a commit a7b0c65

2 files changed

Lines changed: 59 additions & 22 deletions

File tree

lib/private/User/Session.php

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
4646
use OC\Authentication\Token\IProvider;
4747
use OC\Authentication\Token\IToken;
48+
use OC\Authentication\Token\PublicKeyToken;
4849
use OC\Hooks\Emitter;
4950
use OC\Hooks\PublicEmitter;
5051
use OC_User;
@@ -439,8 +440,15 @@ public function logClientIn($user,
439440
}
440441

441442
try {
442-
$isTokenPassword = $this->isTokenPassword($password);
443-
} catch (ExpiredTokenException $e) {
443+
$dbToken = $this->getTokenFromPassword($password);
444+
$isTokenPassword = $dbToken !== null;
445+
if (($dbToken instanceof PublicKeyToken)
446+
&& ($dbToken->getType() !== IToken::PERMANENT_TOKEN)
447+
) {
448+
// Refuse session tokens here, only app tokens are handled
449+
return false;
450+
}
451+
} catch (ExpiredTokenException) {
444452
// Just return on an expired token no need to check further or record a failed login
445453
return false;
446454
}
@@ -461,7 +469,6 @@ public function logClientIn($user,
461469
}
462470

463471
if ($isTokenPassword) {
464-
$dbToken = $this->tokenProvider->getToken($password);
465472
$userFromToken = $this->manager->get($dbToken->getUID());
466473
$isValidEmailLogin = $userFromToken->getEMailAddress() === $user
467474
&& $this->validateTokenLoginName($userFromToken->getEMailAddress(), $dbToken);
@@ -551,6 +558,24 @@ public function isTokenPassword($password) {
551558
}
552559
}
553560

561+
/**
562+
* Check if the given 'password' is actually a device token
563+
*
564+
* @throws ExpiredTokenException
565+
*/
566+
private function getTokenFromPassword(string $password): ?IToken {
567+
try {
568+
return $this->tokenProvider->getToken($password);
569+
} catch (ExpiredTokenException $e) {
570+
throw $e;
571+
} catch (InvalidTokenException $ex) {
572+
$this->logger->debug('Token is not valid: ' . $ex->getMessage(), [
573+
'exception' => $ex,
574+
]);
575+
return null;
576+
}
577+
}
578+
554579
protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) {
555580
if ($refreshCsrfToken) {
556581
// TODO: mock/inject/use non-static
@@ -855,32 +880,39 @@ private function validateTokenLoginName(?string $loginName, IToken $token): bool
855880
*/
856881
public function tryTokenLogin(IRequest $request) {
857882
$authHeader = $request->getHeader('Authorization');
883+
$tokenFromCookie = false;
858884
if (strpos($authHeader, 'Bearer ') === 0) {
859885
$token = substr($authHeader, 7);
860886
} elseif ($request->getCookie($this->config->getSystemValueString('instanceid')) !== null) {
861887
// No auth header, let's try session id, but only if this is an existing
862888
// session and the request has a session cookie
863889
try {
864890
$token = $this->session->getId();
891+
$tokenFromCookie = true;
865892
} catch (SessionNotAvailableException $ex) {
866893
return false;
867894
}
868895
} else {
869896
return false;
870897
}
871898

872-
if (!$this->loginWithToken($token)) {
899+
try {
900+
$dbToken = $this->tokenProvider->getToken($token);
901+
} catch (InvalidTokenException $e) {
902+
// Can't really happen but better safe than sorry
873903
return false;
874904
}
875-
if (!$this->validateToken($token)) {
905+
906+
if ($dbToken instanceof PublicKeyToken && $dbToken->getType() === IToken::TEMPORARY_TOKEN && !$tokenFromCookie) {
907+
// Session token but from Bearer header, not allowed
876908
return false;
877909
}
878910

879-
try {
880-
$dbToken = $this->tokenProvider->getToken($token);
881-
} catch (InvalidTokenException $e) {
882-
// Can't really happen but better save than sorry
883-
return true;
911+
if (!$this->loginWithToken($token)) {
912+
return false;
913+
}
914+
if (!$this->validateToken($token)) {
915+
return false;
884916
}
885917

886918
// Set the session variable so we know this is an app password

tests/lib/User/SessionTest.php

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -405,16 +405,18 @@ public function testLogClientInWithTokenPassword() {
405405
$manager = $this->createMock(Manager::class);
406406
$session = $this->createMock(ISession::class);
407407
$request = $this->createMock(IRequest::class);
408+
$token = $this->createMock(IToken::class);
408409

409410
/** @var Session $userSession */
410411
$userSession = $this->getMockBuilder(Session::class)
411412
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
412-
->setMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser'])
413+
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
413414
->getMock();
414415

415-
$userSession->expects($this->once())
416-
->method('isTokenPassword')
417-
->willReturn(true);
416+
$this->tokenProvider->expects($this->once())
417+
->method('getToken')
418+
->with('I-AM-AN-APP-PASSWORD')
419+
->willReturn($token);
418420
$userSession->expects($this->once())
419421
->method('login')
420422
->with('john', 'I-AM-AN-APP-PASSWORD')
@@ -1154,16 +1156,18 @@ public function testLogClientInThrottlerUsername() {
11541156
$manager = $this->createMock(Manager::class);
11551157
$session = $this->createMock(ISession::class);
11561158
$request = $this->createMock(IRequest::class);
1159+
$token = $this->createMock(IToken::class);
11571160

11581161
/** @var Session $userSession */
11591162
$userSession = $this->getMockBuilder(Session::class)
11601163
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
1161-
->setMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser'])
1164+
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
11621165
->getMock();
11631166

1164-
$userSession->expects($this->once())
1165-
->method('isTokenPassword')
1166-
->willReturn(true);
1167+
$this->tokenProvider->expects($this->once())
1168+
->method('getToken')
1169+
->with('I-AM-AN-PASSWORD')
1170+
->willReturn($token);
11671171
$userSession->expects($this->once())
11681172
->method('login')
11691173
->with('john', 'I-AM-AN-PASSWORD')
@@ -1204,12 +1208,13 @@ public function testLogClientInThrottlerEmail() {
12041208
/** @var Session $userSession */
12051209
$userSession = $this->getMockBuilder(Session::class)
12061210
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
1207-
->setMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser'])
1211+
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
12081212
->getMock();
12091213

1210-
$userSession->expects($this->once())
1211-
->method('isTokenPassword')
1212-
->willReturn(false);
1214+
$this->tokenProvider->expects($this->once())
1215+
->method('getToken')
1216+
->with('I-AM-AN-PASSWORD')
1217+
->willThrowException(new InvalidTokenException());
12131218
$userSession->expects($this->once())
12141219
->method('login')
12151220
->with('john@foo.bar', 'I-AM-AN-PASSWORD')

0 commit comments

Comments
 (0)