diff --git a/lib/private/Session/Internal.php b/lib/private/Session/Internal.php index d10091011573c..4d31d684adede 100644 --- a/lib/private/Session/Internal.php +++ b/lib/private/Session/Internal.php @@ -12,6 +12,7 @@ use OC\Authentication\Token\IProvider; use OC\Diagnostics\TLogSlowOperation; +use OC\User\Session as UserSession; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Server; use OCP\Session\Exceptions\SessionNotAvailableException; @@ -149,6 +150,7 @@ public function regenerateId(bool $deleteOldSession = true, bool $updateToken = try { $tokenProvider->renewSessionToken($oldId, $newId); + Server::get(UserSession::class)->renewMagicSessionId($oldId); } catch (InvalidTokenException $e) { // Just ignore } diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index 578f8e75b9e25..7bb68ef3df835 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -977,6 +977,17 @@ public function loginWithCookie($uid, $currentToken, $oldSessionId) { * @param IUser $user */ public function createRememberMeToken(IUser $user) { + // The cookie outlives the session lifetime, so its token must not be + // cleaned up as a short-lived one. + try { + $sessionToken = $this->tokenProvider->getToken($this->session->getId()); + if ($sessionToken instanceof PublicKeyToken && $sessionToken->getRemember() !== IToken::REMEMBER) { + $sessionToken->setRemember(IToken::REMEMBER); + $this->tokenProvider->updateToken($sessionToken); + } + } catch (InvalidTokenException|SessionNotAvailableException) { + } + $token = $this->random->generate(32); $this->config->setUserValue($user->getUID(), 'login_token', $token, (string)$this->timeFactory->getTime()); $this->setMagicInCookie($user->getUID(), $token); @@ -1061,6 +1072,18 @@ public function setMagicInCookie($username, $token) { } } + /** + * Point the remember-me cookie at the regenerated session id, so cookie + * login can still find the token that was renewed along with it. + */ + public function renewMagicSessionId(string $oldSessionId): void { + if (!isset($_COOKIE['nc_username'], $_COOKIE['nc_token'], $_COOKIE['nc_session_id']) + || $_COOKIE['nc_session_id'] !== $oldSessionId) { + return; + } + $this->setMagicInCookie($_COOKIE['nc_username'], $_COOKIE['nc_token']); + } + /** * Remove cookie for "remember username" */ diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php index f466569eb9b3c..d0ba56df49f2f 100644 --- a/tests/lib/User/SessionTest.php +++ b/tests/lib/User/SessionTest.php @@ -1211,6 +1211,67 @@ public function testCreateRememberMeToken(): void { $this->userSession->createRememberMeToken($user); } + public static function rememberMeSessionTokenData(): array { + return [ + 'session token that is not remembered is upgraded' => [IToken::DO_NOT_REMEMBER, true], + 'remembered session token is left alone' => [IToken::REMEMBER, false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'rememberMeSessionTokenData')] + public function testCreateRememberMeTokenRemembersSessionToken(int $remember, bool $expectUpdate): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('UserUid'); + $this->session->method('getId')->willReturn('sessionid'); + $token = new PublicKeyToken(); + $token->setRemember($remember); + $this->tokenProvider->method('getToken') + ->with('sessionid') + ->willReturn($token); + $this->tokenProvider->expects($expectUpdate ? $this->once() : $this->never()) + ->method('updateToken') + ->with($this->callback(fn (PublicKeyToken $t): bool => $t->getRemember() === IToken::REMEMBER)); + + $this->userSession->createRememberMeToken($user); + } + + public function testCreateRememberMeTokenWithoutSessionToken(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('UserUid'); + $this->random->method('generate')->willReturn('LongRandomToken'); + $this->session->method('getId')->willReturn('sessionid'); + $this->tokenProvider->method('getToken')->willThrowException(new InvalidTokenException()); + $this->tokenProvider->expects($this->never())->method('updateToken'); + $this->userSession->expects($this->once()) + ->method('setMagicInCookie') + ->with('UserUid', 'LongRandomToken'); + + $this->userSession->createRememberMeToken($user); + } + + public static function renewMagicSessionIdData(): array { + return [ + 'cookie holds the old session id' => [['nc_username' => 'u', 'nc_token' => 't', 'nc_session_id' => 'old'], true], + 'cookie holds another session id' => [['nc_username' => 'u', 'nc_token' => 't', 'nc_session_id' => 'other'], false], + 'no remember-me cookies' => [[], false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'renewMagicSessionIdData')] + public function testRenewMagicSessionId(array $cookies, bool $expectRenewal): void { + $this->userSession->expects($expectRenewal ? $this->once() : $this->never()) + ->method('setMagicInCookie') + ->with('u', 't'); + + $originalCookies = $_COOKIE; + $_COOKIE = $cookies; + try { + $this->userSession->renewMagicSessionId('old'); + } finally { + $_COOKIE = $originalCookies; + } + } + public function testTryBasicAuthLoginValid(): void { $request = $this->createMock(Request::class); $request->method('__get')