diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 5a21d27898f..917bc061563 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -143,6 +143,11 @@ class LoginController extends Controller { $this->config->getSystemValue('login_form_autocomplete', true) === true ); + $this->initialState->provideInitialState( + 'loginCanRememberme', + $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) > 0 + ); + if (!empty($redirect_url)) { [$url, ] = explode('?', $redirect_url); if ($url !== $this->urlGenerator->linkToRoute('core.login.logout')) { @@ -287,6 +292,7 @@ class LoginController extends Controller { ITrustedDomainHelper $trustedDomainHelper, string $user = '', string $password = '', + bool $rememberme = false, ?string $redirect_url = null, string $timezone = '', string $timezone_offset = '', @@ -339,9 +345,10 @@ class LoginController extends Controller { $this->request, $user, $password, + $rememberme, $redirect_url, $timezone, - $timezone_offset + $timezone_offset, ); $result = $loginChain->process($data); if (!$result->isSuccess()) { diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue index caf95b245ac..e779b2e6176 100644 --- a/core/src/components/login/LoginForm.vue +++ b/core/src/components/login/LoginForm.vue @@ -84,6 +84,17 @@ data-login-form-input-password required /> + + {{ t('core', 'Remember me') }} + + config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) === 0) { $loginData->setRememberLogin(false); + } + if ($loginData->isRememberLogin()) { + $tokenType = IToken::REMEMBER; + } else { $tokenType = IToken::DO_NOT_REMEMBER; } diff --git a/lib/private/Authentication/Login/LoginData.php b/lib/private/Authentication/Login/LoginData.php index 2567da9b340..8791b00413b 100644 --- a/lib/private/Authentication/Login/LoginData.php +++ b/lib/private/Authentication/Login/LoginData.php @@ -16,12 +16,11 @@ class LoginData { /** @var IUser|false|null */ private $user = null; - private bool $rememberLogin = true; - public function __construct( private IRequest $request, private string $username, private ?string $password, + private bool $rememberLogin = true, private ?string $redirectUrl = null, private string $timeZone = '', private string $timeZoneOffset = '', diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index 18baaf5b08c..b4fd95513df 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -31,6 +31,7 @@ use OCP\IUserManager; use OCP\Notification\IManager; use OCP\Security\Bruteforce\IThrottler; use OCP\Security\ITrustedDomainHelper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -277,7 +278,7 @@ class LoginControllerTest extends TestCase { '', ] ]; - $this->initialState->expects($this->exactly(13)) + $this->initialState->expects($this->exactly(14)) ->method('provideInitialState') ->willReturnCallback(function () use (&$calls): void { $expected = array_shift($calls); @@ -309,12 +310,16 @@ class LoginControllerTest extends TestCase { 'loginAutocomplete', false ], + [ + 'loginCanRememberme', + false + ], [ 'loginRedirectUrl', 'login/flow' ], ]; - $this->initialState->expects($this->exactly(14)) + $this->initialState->expects($this->exactly(15)) ->method('provideInitialState') ->willReturnCallback(function () use (&$calls): void { $expected = array_shift($calls); @@ -351,7 +356,7 @@ class LoginControllerTest extends TestCase { ]; } - #[\PHPUnit\Framework\Attributes\DataProvider('passwordResetDataProvider')] + #[DataProvider('passwordResetDataProvider')] public function testShowLoginFormWithPasswordResetOption($canChangePassword, $expectedResult): void { $this->userSession @@ -386,13 +391,13 @@ class LoginControllerTest extends TestCase { 'loginUsername', 'LdapUser' ], - [], [], [], + [], [], [], [], [ 'loginCanResetPassword', $expectedResult ], ]; - $this->initialState->expects($this->exactly(13)) + $this->initialState->expects($this->exactly(14)) ->method('provideInitialState') ->willReturnCallback(function () use (&$calls): void { $expected = array_shift($calls); @@ -445,6 +450,10 @@ class LoginControllerTest extends TestCase { 'loginAutocomplete', true ], + [ + 'loginCanRememberme', + false + ], [], [ 'loginResetPasswordLink', @@ -455,7 +464,7 @@ class LoginControllerTest extends TestCase { false ], ]; - $this->initialState->expects($this->exactly(13)) + $this->initialState->expects($this->exactly(14)) ->method('provideInitialState') ->willReturnCallback(function () use (&$calls): void { $expected = array_shift($calls); @@ -476,7 +485,19 @@ class LoginControllerTest extends TestCase { $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', '')); } - public function testLoginWithInvalidCredentials(): void { + public static function remembermeProvider(): array { + return [ + [ + true, + ], + [ + false, + ], + ]; + } + + #[DataProvider('remembermeProvider')] + public function testLoginWithInvalidCredentials(bool $rememberme): void { $user = 'MyUserName'; $password = 'secret'; $loginPageUrl = '/login?redirect_url=/apps/files'; @@ -491,6 +512,7 @@ class LoginControllerTest extends TestCase { $this->request, $user, $password, + $rememberme, '/apps/files' ); $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD); @@ -509,12 +531,13 @@ class LoginControllerTest extends TestCase { $expected = new RedirectResponse($loginPageUrl); $expected->throttle(['user' => 'MyUserName']); - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, '/apps/files'); + $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/files'); $this->assertEquals($expected, $response); } - public function testLoginWithValidCredentials(): void { + #[DataProvider('remembermeProvider')] + public function testLoginWithValidCredentials(bool $rememberme): void { $user = 'MyUserName'; $password = 'secret'; $loginChain = $this->createMock(LoginChain::class); @@ -527,7 +550,8 @@ class LoginControllerTest extends TestCase { $loginData = new LoginData( $this->request, $user, - $password + $password, + $rememberme, ); $loginResult = LoginResult::success($loginData); $loginChain->expects($this->once()) @@ -540,10 +564,11 @@ class LoginControllerTest extends TestCase { ->willReturn('/default/foo'); $expected = new RedirectResponse('/default/foo'); - $this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password)); + $this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme)); } - public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(): void { + #[DataProvider('remembermeProvider')] + public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(bool $rememberme): void { /** @var IUser|MockObject $user */ $user = $this->createMock(IUser::class); $user->expects($this->any()) @@ -567,14 +592,15 @@ class LoginControllerTest extends TestCase { $this->userSession->expects($this->never()) ->method('createRememberMeToken'); - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $originalUrl); + $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl); $expected = new RedirectResponse(''); $expected->throttle(['user' => 'Jane']); $this->assertEquals($expected, $response); } - public function testLoginWithoutPassedCsrfCheckAndLoggedIn(): void { + #[DataProvider('remembermeProvider')] + public function testLoginWithoutPassedCsrfCheckAndLoggedIn(bool $rememberme): void { /** @var IUser|MockObject $user */ $user = $this->createMock(IUser::class); $user->expects($this->any()) @@ -607,13 +633,14 @@ class LoginControllerTest extends TestCase { ->with('remember_login_cookie_lifetime') ->willReturn(1234); - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $originalUrl); + $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl); $expected = new RedirectResponse($redirectUrl); $this->assertEquals($expected, $response); } - public function testLoginWithValidCredentialsAndRedirectUrl(): void { + #[DataProvider('remembermeProvider')] + public function testLoginWithValidCredentialsAndRedirectUrl(bool $rememberme): void { $user = 'MyUserName'; $password = 'secret'; $redirectUrl = 'https://next.cloud/apps/mail'; @@ -628,6 +655,7 @@ class LoginControllerTest extends TestCase { $this->request, $user, $password, + $rememberme, '/apps/mail' ); $loginResult = LoginResult::success($loginData); @@ -644,12 +672,13 @@ class LoginControllerTest extends TestCase { ->willReturn($redirectUrl); $expected = new RedirectResponse($redirectUrl); - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, '/apps/mail'); + $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/mail'); $this->assertEquals($expected, $response); } - public function testToNotLeakLoginName(): void { + #[DataProvider('remembermeProvider')] + public function testToNotLeakLoginName(bool $rememberme): void { $loginChain = $this->createMock(LoginChain::class); $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class); $trustedDomainHelper->method('isTrustedUrl')->willReturn(true); @@ -662,6 +691,7 @@ class LoginControllerTest extends TestCase { $this->request, 'john@doe.com', 'just wrong', + $rememberme, '/apps/files' ); $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD); @@ -688,6 +718,7 @@ class LoginControllerTest extends TestCase { $trustedDomainHelper, 'john@doe.com', 'just wrong', + $rememberme, '/apps/files' ); diff --git a/tests/lib/Authentication/Login/ALoginTestCommand.php b/tests/lib/Authentication/Login/ALoginTestCommand.php index b955b20beba..da29e74e683 100644 --- a/tests/lib/Authentication/Login/ALoginTestCommand.php +++ b/tests/lib/Authentication/Login/ALoginTestCommand.php @@ -83,6 +83,7 @@ abstract class ALoginTestCommand extends TestCase { $this->request, $this->username, $this->password, + true, $this->redirectUrl ); $data->setUser($this->user); @@ -94,6 +95,7 @@ abstract class ALoginTestCommand extends TestCase { $this->request, $this->username, $this->password, + true, null, $this->timezone, $this->timeZoneOffset