fix: Reduce the mixups between apptokens and session ids

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
This commit is contained in:
Côme Chilliet 2026-04-07 12:26:54 +02:00
parent a51c509c82
commit 6d3bba76c9
No known key found for this signature in database
GPG key ID: A3E2F658B28C760A
2 changed files with 59 additions and 21 deletions

View file

@ -45,6 +45,7 @@ use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
use OC\Authentication\Token\PublicKeyToken;
use OC\Hooks\Emitter;
use OC\Hooks\PublicEmitter;
use OC_User;
@ -445,8 +446,15 @@ class Session implements IUserSession, Emitter {
}
try {
$isTokenPassword = $this->isTokenPassword($password);
} catch (ExpiredTokenException $e) {
$dbToken = $this->getTokenFromPassword($password);
$isTokenPassword = $dbToken !== null;
if (($dbToken instanceof PublicKeyToken)
&& ($dbToken->getType() !== IToken::PERMANENT_TOKEN)
) {
// Refuse session tokens here, only app tokens are handled
return false;
}
} catch (ExpiredTokenException) {
// Just return on an expired token no need to check further or record a failed login
return false;
}
@ -548,6 +556,24 @@ class Session implements IUserSession, Emitter {
}
}
/**
* Check if the given 'password' is actually a device token
*
* @throws ExpiredTokenException
*/
private function getTokenFromPassword(string $password): ?\OCP\Authentication\Token\IToken {
try {
return $this->tokenProvider->getToken($password);
} catch (ExpiredTokenException $e) {
throw $e;
} catch (InvalidTokenException $ex) {
$this->logger->debug('Token is not valid: ' . $ex->getMessage(), [
'exception' => $ex,
]);
return null;
}
}
protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) {
if ($refreshCsrfToken) {
// TODO: mock/inject/use non-static
@ -829,17 +855,31 @@ class Session implements IUserSession, Emitter {
*/
public function tryTokenLogin(IRequest $request) {
$authHeader = $request->getHeader('Authorization');
$tokenFromCookie = false;
if (strpos($authHeader, 'Bearer ') === 0) {
$token = substr($authHeader, 7);
} else {
// No auth header, let's try session id
try {
$token = $this->session->getId();
$tokenFromCookie = true;
} catch (SessionNotAvailableException $ex) {
return false;
}
}
try {
$dbToken = $this->tokenProvider->getToken($token);
} catch (InvalidTokenException $e) {
// Can't really happen but better safe than sorry
return false;
}
if ($dbToken instanceof PublicKeyToken && $dbToken->getType() === IToken::TEMPORARY_TOKEN && !$tokenFromCookie) {
// Session token but from Bearer header, not allowed
return false;
}
if (!$this->loginWithToken($token)) {
return false;
}
@ -847,13 +887,6 @@ class Session implements IUserSession, Emitter {
return false;
}
try {
$dbToken = $this->tokenProvider->getToken($token);
} catch (InvalidTokenException $e) {
// Can't really happen but better save than sorry
return true;
}
// Remember me tokens are not app_passwords
if ($dbToken->getRemember() === IToken::DO_NOT_REMEMBER) {
// Set the session variable so we know this is an app password

View file

@ -489,16 +489,18 @@ class SessionTest extends \Test\TestCase {
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::class);
$token = $this->createMock(IToken::class);
/** @var \OC\User\Session $userSession */
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->setMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser'])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$userSession->expects($this->once())
->method('isTokenPassword')
->willReturn(true);
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('I-AM-AN-APP-PASSWORD')
->willReturn($token);
$userSession->expects($this->once())
->method('login')
->with('john', 'I-AM-AN-APP-PASSWORD')
@ -1478,16 +1480,18 @@ class SessionTest extends \Test\TestCase {
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::class);
$token = $this->createMock(IToken::class);
/** @var Session $userSession */
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->setMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser'])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$userSession->expects($this->once())
->method('isTokenPassword')
->willReturn(true);
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('I-AM-AN-PASSWORD')
->willReturn($token);
$userSession->expects($this->once())
->method('login')
->with('john', 'I-AM-AN-PASSWORD')
@ -1528,12 +1532,13 @@ class SessionTest extends \Test\TestCase {
/** @var Session $userSession */
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->setMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser'])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$userSession->expects($this->once())
->method('isTokenPassword')
->willReturn(true);
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('I-AM-AN-PASSWORD')
->willThrowException(new InvalidTokenException());
$userSession->expects($this->once())
->method('login')
->with('john@foo.bar', 'I-AM-AN-PASSWORD')