Files
Jonas bffc20acd5 fix(session): only regenerate session id after valid remember-me cookie
loginWithCookie() regenerated the session id before validating the
remember-me cookie. With stale cookies (e.g. after the session token was
invalidated by an OIDC backchannel logout) every request rotated the
session and failed. Parallel requests then forked the same session, and
the browser could end up with a copy lacking data stored during the
login, such as user_oidc's OIDC state or the login flow v2 state token.

Regenerate the session id only once the cookie has been validated.

Signed-off-by: Jonas <jonas@freesources.org>
Assisted-by: ClaudeCode:claude-opus-5.5
2026-09-29 21:47:16 +02:00

1520 lines
49 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\User;
use OC\AppFramework\Http\Request;
use OC\Authentication\Events\LoginFailed;
use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\PublicKeyToken;
use OC\Security\CSRF\CsrfTokenManager;
use OC\Session\Memory;
use OC\User\LoginException;
use OC\User\Manager;
use OC\User\Session;
use OC\User\User;
use OCA\DAV\Connector\Sabre\Auth;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Authentication\Token\IToken;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IRequestId;
use OCP\ISession;
use OCP\IUser;
use OCP\Lockdown\ILockdownManager;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ISecureRandom;
use OCP\User\Events\BeforeUserLoggedInEvent;
use OCP\User\Events\PostLoginEvent;
use OCP\User\Events\UserLoggedInEvent;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
use function array_diff;
use function get_class_methods;
#[Group(name: 'DB')]
class SessionTest extends TestCase {
private ITimeFactory&MockObject $timeFactory;
private IProvider&MockObject $tokenProvider;
private IConfig&MockObject $config;
private IThrottler&MockObject $throttler;
private ISecureRandom&MockObject $random;
private Manager&MockObject $manager;
private ISession&MockObject $session;
private Session&MockObject $userSession;
private ILockdownManager&MockObject $lockdownManager;
private LoggerInterface&MockObject $logger;
private IEventDispatcher&MockObject $dispatcher;
#[\Override]
protected function setUp(): void {
parent::setUp();
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->timeFactory->expects($this->any())
->method('getTime')
->willReturn(10000);
$this->tokenProvider = $this->createMock(IProvider::class);
$this->config = $this->createMock(IConfig::class);
$this->throttler = $this->createMock(IThrottler::class);
$this->random = $this->createMock(ISecureRandom::class);
$this->manager = $this->createMock(Manager::class);
$this->manager
->method('getUserNameFromLoginName')
->willReturnArgument(0);
$this->session = $this->createMock(ISession::class);
$this->lockdownManager = $this->createMock(ILockdownManager::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->dispatcher = $this->createMock(IEventDispatcher::class);
$this->userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([
$this->manager,
$this->session,
$this->timeFactory,
$this->tokenProvider,
$this->config,
$this->random,
$this->lockdownManager,
$this->logger,
$this->dispatcher,
])
->onlyMethods([
'setMagicInCookie',
])
->getMock();
\OC_User::setIncognitoMode(false);
}
public static function isLoggedInData(): array {
return [
[true],
[false],
];
}
#[DataProvider(methodName: 'isLoggedInData')]
public function testIsLoggedIn($isLoggedIn): void {
$session = $this->createMock(Memory::class);
$manager = $this->createMock(Manager::class);
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods([
'getUser'
])
->getMock();
$user = new User('sepp', null, $this->createMock(IEventDispatcher::class));
$userSession->expects($this->once())
->method('getUser')
->willReturn($isLoggedIn ? $user : null);
$this->assertEquals($isLoggedIn, $userSession->isLoggedIn());
}
public function testSetUser(): void {
$session = $this->createMock(Memory::class);
$session->expects($this->once())
->method('set')
->with('user_id', 'foo');
$manager = $this->createMock(Manager::class);
$user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('foo');
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$userSession->setUser($user);
}
public function testLoginValidPasswordEnabled(): void {
$session = $this->createMock(Memory::class);
$session->expects($this->once())
->method('regenerateId');
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('bar')
->willThrowException(new InvalidTokenException());
$session->expects($this->exactly(2))
->method('set')
->with($this->callback(function (string $key): bool {
return $key === 'user_id' || $key === 'loginname';
}));
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('isEnabled')
->willReturn(true);
$user->expects($this->any())
->method('getUID')
->willReturn('foo');
$user->expects($this->once())
->method('updateLastLoginTimestamp');
$manager->expects($this->once())
->method('checkPasswordNoLogging')
->with('foo', 'bar')
->willReturn($user);
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods([
'prepareUserLogin'
])
->getMock();
$userSession->expects($this->once())
->method('prepareUserLogin');
$this->dispatcher->expects($this->exactly(2))
->method('dispatchTyped')
->with(
$this->callback(function (PostLoginEvent|UserLoggedInEvent $e): bool {
return $e->getUser()->getUID() === 'foo'
&& $e->getPassword() === 'bar'
&& $e->isTokenLogin() === false;
})
);
$userSession->login('foo', 'bar');
$this->assertEquals($user, $userSession->getUser());
}
public function testLoginValidPasswordDisabled(): void {
$this->expectException(LoginException::class);
$session = $this->createMock(Memory::class);
$session->expects($this->never())
->method('set');
$session->expects($this->once())
->method('regenerateId');
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('bar')
->willThrowException(new InvalidTokenException());
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('isEnabled')
->willReturn(false);
$user->expects($this->never())
->method('updateLastLoginTimestamp');
$manager->expects($this->once())
->method('checkPasswordNoLogging')
->with('foo', 'bar')
->willReturn($user);
$this->dispatcher->expects($this->never())
->method('dispatch');
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$userSession->login('foo', 'bar');
}
public function testLoginInvalidPassword(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$user = $this->createMock(IUser::class);
$session->expects($this->never())
->method('set');
$session->expects($this->once())
->method('regenerateId');
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('bar')
->willThrowException(new InvalidTokenException());
$user->expects($this->never())
->method('isEnabled');
$user->expects($this->never())
->method('updateLastLoginTimestamp');
$manager->expects($this->once())
->method('checkPasswordNoLogging')
->with('foo', 'bar')
->willReturn(false);
$this->dispatcher->expects($this->never())
->method('dispatch');
$userSession->login('foo', 'bar');
}
public function testPasswordlessLoginNoLastCheckUpdate(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
// Keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('foo');
$user->method('isEnabled')->willReturn(true);
$manager->method('get')
->with('foo')
->willReturn($user);
$session->expects($this->once())
->method('regenerateId');
$token = new PublicKeyToken();
$token->setId(1);
$token->setLoginName('foo');
$token->setLastCheck(0); // Never
$token->setUid('foo');
$this->tokenProvider
->method('getPassword')
->with($token)
->willThrowException(new PasswordlessTokenException());
$this->tokenProvider
->method('getToken')
->with('app-password')
->willReturn($token);
$this->tokenProvider->expects(self::never())
->method('updateToken');
$userSession->login('foo', 'app-password');
}
public function testLoginLastCheckUpdate(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
// Keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('foo');
$user->method('isEnabled')->willReturn(true);
$manager->method('get')
->with('foo')
->willReturn($user);
$session->expects($this->once())
->method('regenerateId');
$token = new PublicKeyToken();
$token->setId(1);
$token->setLoginName('foo');
$token->setLastCheck(0); // Never
$token->setUid('foo');
$this->tokenProvider
->method('getPassword')
->with($token)
->willReturn('secret');
$this->tokenProvider
->method('getToken')
->with('app-password')
->willReturn($token);
$this->tokenProvider->expects(self::once())
->method('updateToken');
$userSession->login('foo', 'app-password');
}
public function testLoginNonExisting(): void {
$session = $this->createMock(Memory::class);
$manager = $this->createMock(Manager::class);
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$session->expects($this->never())
->method('set');
$session->expects($this->once())
->method('regenerateId');
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('bar')
->willThrowException(new InvalidTokenException());
$manager->expects($this->once())
->method('checkPasswordNoLogging')
->with('foo', 'bar')
->willReturn(false);
$userSession->login('foo', 'bar');
}
public function testLogClientInNoTokenPasswordWith2fa(): void {
$this->expectException(PasswordLoginForbiddenException::class);
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::class);
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('doe')
->willThrowException(new InvalidTokenException());
$this->config->expects($this->once())
->method('getSystemValueBool')
->with('token_auth_enforced', false)
->willReturn(true);
$request
->expects($this->any())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->exactly(2))
->method('sleepDelayOrThrowOnMax')
->with('192.168.0.1');
$this->throttler
->expects($this->any())
->method('getDelay')
->with('192.168.0.1')
->willReturn(0);
$this->throttler
->expects($this->once())
->method('registerAttempt')
->with('login', '192.168.0.1', ['user' => 'john']);
$this->dispatcher
->expects($this->exactly(2))
->method('dispatchTyped')
->willReturnCallback(
function (Event $event): void {
if ($event instanceof LoginFailed) {
$this->assertEquals($event, new LoginFailed('john', 'doe'));
} elseif ($event instanceof BeforeUserLoggedInEvent) {
$this->assertEquals($event, new BeforeUserLoggedInEvent('john', 'doe'));
} else {
$this->fail('Unexpected event');
}
}
);
$userSession->logClientIn('john', 'doe', $request, $this->throttler);
}
public function testLogClientInUnexist(): void {
$manager = $this->createMock(Manager::class);
$manager
->method('getUserNameFromLoginName')
->willReturnArgument(0);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::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])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('doe')
->willThrowException(new InvalidTokenException());
$this->config->expects($this->once())
->method('getSystemValueBool')
->with('token_auth_enforced', false)
->willReturn(false);
$manager->method('getByEmail')
->with('unexist')
->willReturn([]);
$this->assertFalse($userSession->logClientIn('unexist', 'doe', $request, $this->throttler));
}
public function testLogClientInWithTokenPassword(): void {
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::class);
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('I-AM-AN-APP-PASSWORD')
->willReturn($this->createMock(IToken::class));
$userSession->expects($this->once())
->method('login')
->with('john', 'I-AM-AN-APP-PASSWORD')
->willReturn(true);
$session->expects($this->once())
->method('set')
->with('app_password', 'I-AM-AN-APP-PASSWORD');
$request
->expects($this->any())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelayOrThrowOnMax')
->with('192.168.0.1');
$this->throttler
->expects($this->any())
->method('getDelay')
->with('192.168.0.1')
->willReturn(0);
$this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request, $this->throttler));
}
public function testLogClientInNoTokenPasswordNo2fa(): void {
$this->expectException(PasswordLoginForbiddenException::class);
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::class);
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods(['login', 'isTwoFactorEnforced'])
->getMock();
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('doe')
->willThrowException(new InvalidTokenException());
$this->config->expects($this->once())
->method('getSystemValueBool')
->with('token_auth_enforced', false)
->willReturn(false);
$userSession->expects($this->once())
->method('isTwoFactorEnforced')
->with('john')
->willReturn(true);
$request
->expects($this->any())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->exactly(2))
->method('sleepDelayOrThrowOnMax')
->with('192.168.0.1');
$this->throttler
->expects($this->any())
->method('getDelay')
->with('192.168.0.1')
->willReturn(0);
$this->throttler
->expects($this->once())
->method('registerAttempt')
->with('login', '192.168.0.1', ['user' => 'john']);
$this->dispatcher
->expects($this->exactly(2))
->method('dispatchTyped')
->willReturnCallback(
function (Event $event): void {
if ($event instanceof LoginFailed) {
$this->assertEquals($event, new LoginFailed('john', 'doe'));
} elseif ($event instanceof BeforeUserLoggedInEvent) {
$this->assertEquals($event, new BeforeUserLoggedInEvent('john', 'doe'));
} else {
$this->fail('Unexpected event');
}
}
);
$userSession->logClientIn('john', 'doe', $request, $this->throttler);
}
public function testTryTokenLoginNoHeaderNoSessionCookie(): void {
$request = $this->createMock(IRequest::class);
$this->config->expects(self::once())
->method('getSystemValueString')
->with('instanceid')
->willReturn('abc123');
$request->method('getHeader')->with('Authorization')->willReturn('');
$request->method('getCookie')->with('abc123')->willReturn(null);
$this->tokenProvider->expects(self::never())
->method('getToken');
$loginResult = $this->userSession->tryTokenLogin($request);
self::assertFalse($loginResult);
}
public function testTryTokenLoginAuthorizationHeaderTokenNotFound(): void {
$request = $this->createMock(IRequest::class);
$request->method('getHeader')->with('Authorization')->willReturn('Bearer abcde-12345');
$this->tokenProvider->expects(self::once())
->method('getToken')
->with('abcde-12345')
->willThrowException(new InvalidTokenException());
$loginResult = $this->userSession->tryTokenLogin($request);
self::assertFalse($loginResult);
}
public function testTryTokenLoginSessionIdTokenNotFound(): void {
$request = $this->createMock(IRequest::class);
$this->config->expects(self::once())
->method('getSystemValueString')
->with('instanceid')
->willReturn('abc123');
$request->method('getHeader')->with('Authorization')->willReturn('');
$request->method('getCookie')->with('abc123')->willReturn('abcde12345');
$this->session->expects(self::once())
->method('getId')
->willReturn('abcde12345');
$this->tokenProvider->expects(self::once())
->method('getToken')
->with('abcde12345')
->willThrowException(new InvalidTokenException());
$loginResult = $this->userSession->tryTokenLogin($request);
self::assertFalse($loginResult);
}
public function testTryTokenLoginNotAnAppPassword(): void {
$request = $this->createMock(IRequest::class);
$this->config->expects(self::once())
->method('getSystemValueString')
->with('instanceid')
->willReturn('abc123');
$request->method('getHeader')->with('Authorization')->willReturn('');
$request->method('getCookie')->with('abc123')->willReturn('abcde12345');
$this->session->expects(self::once())
->method('getId')
->willReturn('abcde12345');
$dbToken = new PublicKeyToken();
$dbToken->setId(42);
$dbToken->setUid('johnny');
$dbToken->setLoginName('johnny');
$dbToken->setLastCheck(0);
$dbToken->setType(IToken::TEMPORARY_TOKEN);
$dbToken->setRemember(IToken::REMEMBER);
$this->tokenProvider->expects(self::any())
->method('getToken')
->with('abcde12345')
->willReturn($dbToken);
$this->session->method('set')
->willReturnCallback(function ($key, $value): void {
if ($key === 'app_password') {
throw new ExpectationFailedException('app_password should not be set in session');
}
});
$user = $this->createMock(IUser::class);
$user->method('isEnabled')->willReturn(true);
$this->manager->method('get')
->with('johnny')
->willReturn($user);
$loginResult = $this->userSession->tryTokenLogin($request);
self::assertTrue($loginResult);
}
public function testTryTokenLoginOcmAccessTokenRejectedFromBearerByDefault(): void {
$request = $this->createMock(IRequest::class);
$request->method('getHeader')->with('Authorization')->willReturn('Bearer ocm-access-token');
$dbToken = new PublicKeyToken();
$dbToken->setId(42);
$dbToken->setUid('alice');
$dbToken->setLoginName('alice');
$dbToken->setLastCheck(0);
$dbToken->setType(IToken::TEMPORARY_TOKEN);
$dbToken->setName(IToken::OCM_ACCESS_TOKEN_NAME);
$this->tokenProvider->expects(self::once())
->method('getToken')
->with('ocm-access-token')
->willReturn($dbToken);
// The guard must reject before any login is attempted.
$this->manager->expects(self::never())
->method('get');
$loginResult = $this->userSession->tryTokenLogin($request);
self::assertFalse($loginResult);
}
public function testTryTokenLoginOcmAccessTokenAllowedFromBearerWhenPermitted(): void {
$request = $this->createMock(IRequest::class);
$request->method('getHeader')->with('Authorization')->willReturn('Bearer ocm-access-token');
$dbToken = new PublicKeyToken();
$dbToken->setId(42);
$dbToken->setUid('alice');
$dbToken->setLoginName('alice');
$dbToken->setLastCheck(0);
$dbToken->setType(IToken::TEMPORARY_TOKEN);
$dbToken->setName(IToken::OCM_ACCESS_TOKEN_NAME);
$this->tokenProvider->method('getToken')
->with('ocm-access-token')
->willReturn($dbToken);
$this->session->method('set')
->willReturnCallback(function ($key, $value): void {
if ($key === 'app_password') {
throw new ExpectationFailedException('app_password should not be set in session');
}
});
$user = $this->createMock(IUser::class);
$user->method('isEnabled')->willReturn(true);
$this->manager->method('get')
->with('alice')
->willReturn($user);
$loginResult = $this->userSession->tryTokenLogin($request, true);
self::assertTrue($loginResult);
}
public function testRememberLoginValidToken(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = $this->getMockBuilder(Session::class)
//override, otherwise tests will fail because of setcookie()
->onlyMethods(['setMagicInCookie', 'setLoginName'])
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->getMock();
$user = $this->createMock(IUser::class);
$token = 'goodToken';
$oldSessionId = 'sess321';
$sessionId = 'sess123';
$session->expects($this->once())
->method('regenerateId');
$manager->expects($this->once())
->method('get')
->with('foo')
->willReturn($user);
$this->config->expects($this->once())
->method('getUserKeys')
->with('foo', 'login_token')
->willReturn([$token]);
$this->config->expects($this->once())
->method('deleteUserValue')
->with('foo', 'login_token', $token);
$this->random->expects($this->once())
->method('generate')
->with(32)
->willReturn('abcdefg123456');
$this->config->expects($this->once())
->method('setUserValue')
->with('foo', 'login_token', 'abcdefg123456', 10000);
$tokenObject = $this->createMock(IToken::class);
$tokenObject->expects($this->once())
->method('getLoginName')
->willReturn('foobar');
$tokenObject->method('getId')
->willReturn(42);
$session->expects($this->once())
->method('getId')
->willReturn($sessionId);
$this->tokenProvider->expects($this->once())
->method('renewSessionToken')
->with($oldSessionId, $sessionId)
->willReturn($tokenObject);
$oldTokenObject = $this->createMock(IToken::class);
$oldTokenObject->expects($this->once())
->method('getUID')
->willReturn('foo');
$this->tokenProvider->expects($this->once())
->method('getToken')
->willReturn($oldTokenObject);
$user->expects($this->any())
->method('getUID')
->willReturn('foo');
$userSession->expects($this->once())
->method('setMagicInCookie');
$user->expects($this->once())
->method('updateLastLoginTimestamp');
$setUID = false;
$session
->method('set')
->willReturnCallback(function ($k, $v) use (&$setUID): void {
if ($k === 'user_id' && $v === 'foo') {
$setUID = true;
}
});
$userSession->expects($this->once())
->method('setLoginName')
->willReturn('foobar');
$granted = $userSession->loginWithCookie('foo', $token, $oldSessionId);
$this->assertTrue($setUID);
$this->assertTrue($granted);
}
public function testRememberLoginInvalidSessionToken(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = $this->getMockBuilder(Session::class)
//override, otherwise tests will fail because of setcookie()
->onlyMethods(['setMagicInCookie'])
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->getMock();
$user = $this->createMock(IUser::class);
$token = 'goodToken';
$oldSessionId = 'sess321';
$sessionId = 'sess123';
$session->expects($this->once())
->method('regenerateId');
$manager->expects($this->once())
->method('get')
->with('foo')
->willReturn($user);
$this->config->expects($this->once())
->method('getUserKeys')
->with('foo', 'login_token')
->willReturn([$token]);
$this->config->expects($this->once())
->method('deleteUserValue')
->with('foo', 'login_token', $token);
$this->config->expects($this->once())
->method('setUserValue'); // TODO: mock new random value
$session->expects($this->once())
->method('getId')
->willReturn($sessionId);
$this->tokenProvider->expects($this->once())
->method('renewSessionToken')
->with($oldSessionId, $sessionId)
->willThrowException(new InvalidTokenException());
$oldTokenObject = $this->createMock(IToken::class);
$oldTokenObject->expects($this->once())
->method('getUID')
->willReturn('foo');
$this->tokenProvider->expects($this->once())
->method('getToken')
->willReturn($oldTokenObject);
$user->expects($this->once())
->method('getUID')
->willReturn('foo');
$userSession->expects($this->never())
->method('setMagicInCookie');
$user->expects($this->never())
->method('updateLastLoginTimestamp');
$session->expects($this->never())
->method('set')
->with('user_id', 'foo');
$granted = $userSession->loginWithCookie('foo', $token, $oldSessionId);
$this->assertFalse($granted);
}
public function testRememberLoginMissingSessionToken(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = $this->getMockBuilder(Session::class)
//override, otherwise tests will fail because of setcookie()
->onlyMethods(['setMagicInCookie'])
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->getMock();
$user = $this->createMock(IUser::class);
$token = 'goodToken';
$oldSessionId = 'sess321';
$session->expects($this->never())
->method('regenerateId');
$manager->expects($this->once())
->method('get')
->with('foo')
->willReturn($user);
$this->config->expects($this->once())
->method('getUserKeys')
->with('foo', 'login_token')
->willReturn([$token]);
$this->tokenProvider->expects($this->once())
->method('getToken')
->with($oldSessionId)
->willThrowException(new InvalidTokenException());
$this->config->expects($this->never())
->method('deleteUserValue');
$this->tokenProvider->expects($this->never())
->method('renewSessionToken');
$userSession->expects($this->never())
->method('setMagicInCookie');
$session->expects($this->never())
->method('set')
->with('user_id', 'foo');
$granted = $userSession->loginWithCookie('foo', $token, $oldSessionId);
$this->assertFalse($granted);
}
public function testRememberLoginInvalidToken(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = $this->getMockBuilder(Session::class)
//override, otherwise tests will fail because of setcookie()
->onlyMethods(['setMagicInCookie'])
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->getMock();
$user = $this->createMock(IUser::class);
$token = 'goodToken';
$oldSessionId = 'sess321';
$session->expects($this->never())
->method('regenerateId');
$manager->expects($this->once())
->method('get')
->with('foo')
->willReturn($user);
$this->config->expects($this->once())
->method('getUserKeys')
->with('foo', 'login_token')
->willReturn(['anothertoken']);
$this->config->expects($this->never())
->method('deleteUserValue')
->with('foo', 'login_token', $token);
$this->tokenProvider->expects($this->never())
->method('renewSessionToken');
$userSession->expects($this->never())
->method('setMagicInCookie');
$user->expects($this->never())
->method('updateLastLoginTimestamp');
$session->expects($this->never())
->method('set')
->with('user_id', 'foo');
$granted = $userSession->loginWithCookie('foo', $token, $oldSessionId);
$this->assertFalse($granted);
}
public function testRememberLoginInvalidUser(): void {
$session = $this->createMock(Memory::class);
$managerMethods = get_class_methods(Manager::class);
//keep following methods intact in order to ensure hooks are working
$mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']);
$manager = $this->getMockBuilder(Manager::class)
->onlyMethods($mockedManagerMethods)
->setConstructorArgs([
$this->config,
$this->createMock(ICacheFactory::class),
$this->createMock(IEventDispatcher::class),
$this->createMock(LoggerInterface::class),
])
->getMock();
$userSession = $this->getMockBuilder(Session::class)
//override, otherwise tests will fail because of setcookie()
->onlyMethods(['setMagicInCookie'])
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->getMock();
$token = 'goodToken';
$oldSessionId = 'sess321';
$session->expects($this->never())
->method('regenerateId');
$manager->expects($this->once())
->method('get')
->with('foo')
->willReturn(null);
$this->config->expects($this->never())
->method('getUserKeys')
->with('foo', 'login_token')
->willReturn(['anothertoken']);
$this->tokenProvider->expects($this->never())
->method('renewSessionToken');
$userSession->expects($this->never())
->method('setMagicInCookie');
$session->expects($this->never())
->method('set')
->with('user_id', 'foo');
$granted = $userSession->loginWithCookie('foo', $token, $oldSessionId);
$this->assertFalse($granted);
}
public function testActiveUserAfterSetSession(): void {
$users = [
'foo' => new User('foo', null, $this->createMock(IEventDispatcher::class)),
'bar' => new User('bar', null, $this->createMock(IEventDispatcher::class))
];
$manager = $this->getMockBuilder(Manager::class)
->disableOriginalConstructor()
->getMock();
$manager->expects($this->any())
->method('get')
->willReturnCallback(function ($uid) use ($users) {
return $users[$uid];
});
$session = new Memory();
$session->set('user_id', 'foo');
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods([
'validateSession'
])
->getMock();
$userSession->expects($this->any())
->method('validateSession');
$this->assertEquals($users['foo'], $userSession->getUser());
$session2 = new Memory();
$session2->set('user_id', 'bar');
$userSession->setSession($session2);
$this->assertEquals($users['bar'], $userSession->getUser());
}
public function testCreateSessionToken(): void {
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$user = $this->createMock(IUser::class);
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$requestId = $this->createMock(IRequestId::class);
$config = $this->createMock(IConfig::class);
$csrf = $this->getMockBuilder(CsrfTokenManager::class)
->disableOriginalConstructor()
->getMock();
$request = new Request([
'server' => [
'HTTP_USER_AGENT' => 'Firefox',
]
], $requestId, $config, $csrf);
$uid = 'user123';
$loginName = 'User123';
$password = 'passme';
$sessionId = 'abcxyz';
$manager->expects($this->once())
->method('get')
->with($uid)
->willReturn($user);
$session->expects($this->once())
->method('getId')
->willReturn($sessionId);
$this->tokenProvider->expects($this->once())
->method('getToken')
->with($password)
->willThrowException(new InvalidTokenException());
$this->tokenProvider->expects($this->once())
->method('generateToken')
->with($sessionId, $uid, $loginName, $password, 'Firefox', IToken::TEMPORARY_TOKEN, IToken::DO_NOT_REMEMBER);
$this->assertTrue($userSession->createSessionToken($request, $uid, $loginName, $password));
}
public function testCreateRememberedSessionToken(): void {
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$user = $this->createMock(IUser::class);
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$requestId = $this->createMock(IRequestId::class);
$config = $this->createMock(IConfig::class);
$csrf = $this->getMockBuilder(CsrfTokenManager::class)
->disableOriginalConstructor()
->getMock();
$request = new Request([
'server' => [
'HTTP_USER_AGENT' => 'Firefox',
]
], $requestId, $config, $csrf);
$uid = 'user123';
$loginName = 'User123';
$password = 'passme';
$sessionId = 'abcxyz';
$manager->expects($this->once())
->method('get')
->with($uid)
->willReturn($user);
$session->expects($this->once())
->method('getId')
->willReturn($sessionId);
$this->tokenProvider->expects($this->once())
->method('getToken')
->with($password)
->willThrowException(new InvalidTokenException());
$this->tokenProvider->expects($this->once())
->method('generateToken')
->with($sessionId, $uid, $loginName, $password, 'Firefox', IToken::TEMPORARY_TOKEN, IToken::REMEMBER);
$this->assertTrue($userSession->createSessionToken($request, $uid, $loginName, $password, IToken::REMEMBER));
}
public function testCreateSessionTokenWithTokenPassword(): void {
$manager = $this->getMockBuilder(Manager::class)
->disableOriginalConstructor()
->getMock();
$session = $this->createMock(ISession::class);
$token = $this->createMock(IToken::class);
$user = $this->createMock(IUser::class);
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$requestId = $this->createMock(IRequestId::class);
$config = $this->createMock(IConfig::class);
$csrf = $this->getMockBuilder(CsrfTokenManager::class)
->disableOriginalConstructor()
->getMock();
$request = new Request([
'server' => [
'HTTP_USER_AGENT' => 'Firefox',
]
], $requestId, $config, $csrf);
$uid = 'user123';
$loginName = 'User123';
$password = 'iamatoken';
$realPassword = 'passme';
$sessionId = 'abcxyz';
$manager->expects($this->once())
->method('get')
->with($uid)
->willReturn($user);
$session->expects($this->once())
->method('getId')
->willReturn($sessionId);
$this->tokenProvider->expects($this->once())
->method('getToken')
->with($password)
->willReturn($token);
$this->tokenProvider->expects($this->once())
->method('getPassword')
->with($token, $password)
->willReturn($realPassword);
$this->tokenProvider->expects($this->once())
->method('generateToken')
->with($sessionId, $uid, $loginName, $realPassword, 'Firefox', IToken::TEMPORARY_TOKEN, IToken::DO_NOT_REMEMBER);
$this->assertTrue($userSession->createSessionToken($request, $uid, $loginName, $password));
}
public function testCreateSessionTokenWithNonExistentUser(): void {
$manager = $this->getMockBuilder(Manager::class)
->disableOriginalConstructor()
->getMock();
$session = $this->createMock(ISession::class);
$userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher);
$request = $this->createMock(IRequest::class);
$uid = 'user123';
$loginName = 'User123';
$password = 'passme';
$manager->expects($this->once())
->method('get')
->with($uid)
->willReturn(null);
$this->assertFalse($userSession->createSessionToken($request, $uid, $loginName, $password));
}
public function testCreateRememberMeToken(): void {
$user = $this->createMock(IUser::class);
$user
->expects($this->exactly(2))
->method('getUID')
->willReturn('UserUid');
$this->random
->expects($this->once())
->method('generate')
->with(32)
->willReturn('LongRandomToken');
$this->config
->expects($this->once())
->method('setUserValue')
->with('UserUid', 'login_token', 'LongRandomToken', 10000);
$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');
$request = $this->createMock(IRequest::class);
$request->method('getCookie')->willReturnCallback(fn (string $key) => $cookies[$key] ?? null);
$this->overwriteService(IRequest::class, $request);
$this->userSession->renewMagicSessionId('old');
}
public function testTryBasicAuthLoginValid(): void {
$request = $this->createMock(Request::class);
$request->method('__get')
->willReturn([
'PHP_AUTH_USER' => 'username',
'PHP_AUTH_PW' => 'password',
]);
$request->method('__isset')
->with('server')
->willReturn(true);
$davAuthenticatedSet = false;
$this->session
->method('set')
->willReturnCallback(function (string $k, $v) use (&$davAuthenticatedSet): void {
switch ($k) {
case Auth::DAV_AUTHENTICATED:
$davAuthenticatedSet = $v;
return;
default:
throw new \Exception();
}
});
/** @var Session&MockObject $userSession */
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([
$this->manager,
$this->session,
$this->timeFactory,
$this->tokenProvider,
$this->config,
$this->random,
$this->lockdownManager,
$this->logger,
$this->dispatcher
])
->onlyMethods([
'logClientIn',
'getUser',
])
->getMock();
$userSession->expects($this->once())
->method('logClientIn')
->with(
$this->equalTo('username'),
$this->equalTo('password'),
$this->equalTo($request),
$this->equalTo($this->throttler)
)->willReturn(true);
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('username');
$userSession->expects($this->once())
->method('getUser')
->willReturn($user);
$this->assertTrue($userSession->tryBasicAuthLogin($request, $this->throttler));
/** @var string|false $davAuthenticatedSet */
$this->assertSame('username', $davAuthenticatedSet);
}
public function testTryBasicAuthLoginNoLogin(): void {
$request = $this->createMock(Request::class);
$request->method('__get')
->willReturn([]);
$request->method('__isset')
->with('server')
->willReturn(true);
$this->session->expects($this->never())
->method($this->anything());
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([
$this->manager,
$this->session,
$this->timeFactory,
$this->tokenProvider,
$this->config,
$this->random,
$this->lockdownManager,
$this->logger,
$this->dispatcher
])
->onlyMethods([
'logClientIn',
])
->getMock();
$userSession->expects($this->never())
->method('logClientIn');
$this->assertFalse($userSession->tryBasicAuthLogin($request, $this->throttler));
}
public function testUpdateTokens(): void {
$this->tokenProvider->expects($this->once())
->method('updatePasswords')
->with('uid', 'pass');
$this->userSession->updateTokens('uid', 'pass');
}
public function testLogClientInThrottlerUsername(): void {
$manager = $this->createMock(Manager::class);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::class);
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('I-AM-A-PASSWORD')
->willReturn($this->createMock(IToken::class));
$userSession->expects($this->once())
->method('login')
->with('john', 'I-AM-A-PASSWORD')
->willReturn(false);
$session->expects($this->never())
->method('set');
$request
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->exactly(2))
->method('sleepDelayOrThrowOnMax')
->with('192.168.0.1');
$this->throttler
->expects($this->any())
->method('getDelay')
->with('192.168.0.1')
->willReturn(0);
$this->throttler
->expects($this->once())
->method('registerAttempt')
->with('login', '192.168.0.1', ['user' => 'john']);
$this->dispatcher
->expects($this->exactly(2))
->method('dispatchTyped')
->willReturnCallback(
function (Event $event): void {
if ($event instanceof LoginFailed) {
$this->assertEquals($event, new LoginFailed('john', 'I-AM-A-PASSWORD'));
} elseif ($event instanceof BeforeUserLoggedInEvent) {
$this->assertEquals($event, new BeforeUserLoggedInEvent('john', 'I-AM-A-PASSWORD'));
} else {
$this->fail('Unexpected event');
}
}
);
$this->assertFalse($userSession->logClientIn('john', 'I-AM-A-PASSWORD', $request, $this->throttler));
}
public function testLogClientInThrottlerEmail(): void {
$manager = $this->createMock(Manager::class);
$manager
->method('getUserNameFromLoginName')
->willReturnArgument(0);
$session = $this->createMock(ISession::class);
$request = $this->createMock(IRequest::class);
$userSession = $this->getMockBuilder(Session::class)
->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher])
->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser'])
->getMock();
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('I-AM-A-PASSWORD')
->willThrowException(new InvalidTokenException());
$userSession->expects($this->once())
->method('login')
->with('john@foo.bar', 'I-AM-A-PASSWORD')
->willReturn(false);
$manager
->method('getByEmail')
->with('john@foo.bar')
->willReturn([]);
$session->expects($this->never())
->method('set');
$request
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->exactly(2))
->method('sleepDelayOrThrowOnMax')
->with('192.168.0.1');
$this->throttler
->expects($this->any())
->method('getDelay')
->with('192.168.0.1')
->willReturn(0);
$this->throttler
->expects($this->once())
->method('registerAttempt')
->with('login', '192.168.0.1', ['user' => 'john@foo.bar']);
$this->dispatcher
->expects($this->exactly(2))
->method('dispatchTyped')
->willReturnCallback(
function (Event $event): void {
if ($event instanceof LoginFailed) {
$this->assertEquals($event, new LoginFailed('john@foo.bar', 'I-AM-A-PASSWORD'));
} elseif ($event instanceof BeforeUserLoggedInEvent) {
$this->assertEquals($event, new BeforeUserLoggedInEvent('john@foo.bar', 'I-AM-A-PASSWORD'));
} else {
$this->fail('Unexpected event');
}
}
);
$this->assertFalse($userSession->logClientIn('john@foo.bar', 'I-AM-A-PASSWORD', $request, $this->throttler));
}
}