Merge pull request #56423 from nextcloud/backport/56343/stable32

[stable32] Add rememberme checkbox
pull/56505/head
Arthur Schiwon 2025-11-17 16:03:37 +07:00 committed by GitHub
commit 0e7e65842a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 97 additions and 59 deletions

@ -220,7 +220,7 @@ trait Auth {
'form_params' => [
'user' => 'user0',
'password' => '123456',
'remember_login' => $remember ? '1' : '0',
'rememberme' => $remember ? '1' : '0',
'requesttoken' => $this->requestToken,
],
'cookies' => $this->cookieJar,

@ -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()) {

@ -74,6 +74,16 @@
data-login-form-input-password
required />
<NcCheckboxRadioSwitch v-if="remembermeAllowed"
id="rememberme"
ref="rememberme"
name="rememberme"
value="1"
:checked.sync="rememberme"
data-login-form-input-rememberme>
{{ t('core', 'Remember me') }}
</NcCheckboxRadioSwitch>
<LoginButton data-login-form-submit :loading="loading" />
<input v-if="redirectUrl"
@ -102,7 +112,7 @@ import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl, imagePath } from '@nextcloud/router'
import debounce from 'debounce'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
@ -115,6 +125,7 @@ export default {
components: {
LoginButton,
NcCheckboxRadioSwitch,
NcPasswordField,
NcTextField,
NcNoteCard,
@ -147,6 +158,10 @@ export default {
type: Boolean,
default: true,
},
remembermeAllowed: {
type: Boolean,
default: true,
},
directLogin: {
type: Boolean,
default: false,
@ -180,6 +195,7 @@ export default {
loading: false,
user: '',
password: '',
rememberme: ['1'],
}
},

@ -15,6 +15,7 @@
:errors="errors"
:throttle-delay="throttleDelay"
:auto-complete-allowed="autoCompleteAllowed"
:rememberme-allowed="remembermeAllowed"
:email-states="emailStates"
@submit="loading = true" />
<NcButton v-if="hasPasswordless"
@ -139,6 +140,7 @@ export default {
canResetPassword: loadState('core', 'loginCanResetPassword', false),
resetPasswordLink: loadState('core', 'loginResetPasswordLink', ''),
autoCompleteAllowed: loadState('core', 'loginAutocomplete', true),
remembermeAllowed: loadState('core', 'loginCanRememberme', true),
resetPasswordTarget: loadState('core', 'resetPasswordTarget', ''),
resetPasswordUser: loadState('core', 'resetPasswordUser', ''),
directLogin: query.direct === '1',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -26,9 +26,12 @@ class CreateSessionTokenCommand extends ALoginCommand {
}
public function process(LoginData $loginData): LoginResult {
$tokenType = IToken::REMEMBER;
if ($this->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;
}

@ -6,48 +6,25 @@ declare(strict_types=1);
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Authentication\Login;
use OCP\IRequest;
use OCP\IUser;
class LoginData {
/** @var IRequest */
private $request;
/** @var string */
private $username;
/** @var string */
private $password;
/** @var string */
private $redirectUrl;
/** @var string */
private $timeZone;
/** @var string */
private $timeZoneOffset;
/** @var IUser|false|null */
private $user = null;
/** @var bool */
private $rememberLogin = true;
public function __construct(IRequest $request,
string $username,
?string $password,
?string $redirectUrl = null,
string $timeZone = '',
string $timeZoneOffset = '') {
$this->request = $request;
$this->username = $username;
$this->password = $password;
$this->redirectUrl = $redirectUrl;
$this->timeZone = $timeZone;
$this->timeZoneOffset = $timeZoneOffset;
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 = '',
) {
}
public function getRequest(): IRequest {
@ -81,7 +58,7 @@ class LoginData {
/**
* @param IUser|false|null $user
*/
public function setUser($user) {
public function setUser($user): void {
$this->user = $user;
}

@ -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'
);

@ -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