Skip to content

Commit

Permalink
Merge pull request #33407 from nextcloud/backport/stable24/one-time-p…
Browse files Browse the repository at this point in the history
…assword

[stable24] Handle one time and large passwords
  • Loading branch information
PVince81 authored Aug 3, 2022
2 parents 313e336 + f4795f6 commit 3a3a52d
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 8 deletions.
12 changes: 11 additions & 1 deletion apps/settings/lib/Controller/ChangePasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public function changePersonalPassword(string $oldpassword = '', string $newpass
}

try {
if ($newpassword === null || $user->setPassword($newpassword) === false) {
if ($newpassword === null || strlen($newpassword) > 469 || $user->setPassword($newpassword) === false) {
return new JSONResponse([
'status' => 'error'
]);
Expand Down Expand Up @@ -155,6 +155,16 @@ public function changeUserPassword(string $username = null, string $password = n
]);
}

if (strlen($password) > 469) {
return new JSONResponse([
'status' => 'error',
'data' => [
'message' => $this->l->t('Unable to change password. Password too long.'),
],
]);
}


$currentUser = $this->userSession->getUser();
$targetUser = $this->userManager->get($username);
if ($currentUser === null || $targetUser === null ||
Expand Down
15 changes: 15 additions & 0 deletions config/config.sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,21 @@
*/
'auth.webauthn.enabled' => true,

/**
* Whether encrypted password should be stored in the database
*
* The passwords are only decrypted using the login token stored uniquely in the
* clients and allow to connect to external storages, autoconfigure mail account in
* the mail app and periodically check if the password it still valid.
*
* This might be desirable to disable this functionality when using one time
* passwords or when having a password policy enforcing long passwords (> 300
* characters).
*
* By default the passwords are stored encrypted in the database.
*/
'auth.storeCryptedPassword' => true,

/**
* By default the login form is always available. There are cases (SSO) where an
* admin wants to avoid users entering their credentials to the system if the SSO
Expand Down
9 changes: 6 additions & 3 deletions lib/private/Authentication/Token/PublicKeyTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ private function newToken(string $token,

$config = array_merge([
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048,
], $this->config->getSystemValue('openssl', []));

// Generate new key
Expand All @@ -368,7 +368,10 @@ private function newToken(string $token,
$dbToken->setPublicKey($publicKey);
$dbToken->setPrivateKey($this->encrypt($privateKey, $token));

if (!is_null($password)) {
if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
if (strlen($password) > 469) {
throw new \RuntimeException('Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php');
}
$dbToken->setPassword($this->encryptPassword($password, $publicKey));
}

Expand Down Expand Up @@ -398,7 +401,7 @@ public function updatePasswords(string $uid, string $password) {
$this->cache->clear();

// prevent setting an empty pw as result of pw-less-login
if ($password === '') {
if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
return;
}

Expand Down
87 changes: 83 additions & 4 deletions tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

use OC\Authentication\Exceptions\ExpiredTokenException;
use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Token\IToken;
use OC\Authentication\Token\PublicKeyToken;
use OC\Authentication\Token\PublicKeyTokenMapper;
Expand Down Expand Up @@ -83,6 +84,10 @@ public function testGenerateToken() {
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;

$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);
$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

$this->assertInstanceOf(PublicKeyToken::class, $actual);
Expand All @@ -93,6 +98,48 @@ public function testGenerateToken() {
$this->assertSame($password, $this->tokenProvider->getPassword($actual, $token));
}

public function testGenerateTokenNoPassword(): void {
$token = 'token';
$uid = 'user';
$user = 'User';
$password = 'passme';
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;
$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, false],
]);
$this->expectException(PasswordlessTokenException::class);

$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

$this->assertInstanceOf(PublicKeyToken::class, $actual);
$this->assertSame($uid, $actual->getUID());
$this->assertSame($user, $actual->getLoginName());
$this->assertSame($name, $actual->getName());
$this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember());
$this->tokenProvider->getPassword($actual, $token);
}

public function testGenerateTokenLongPassword() {
$token = 'token';
$uid = 'user';
$user = 'User';
$password = '';
for ($i = 0; $i < 500; $i++) {
$password .= 'e';
}
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;
$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);
$this->expectException(\RuntimeException::class);

$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
}

public function testGenerateTokenInvalidName() {
$token = 'token';
$uid = 'user';
Expand All @@ -103,6 +150,10 @@ public function testGenerateTokenInvalidName() {
. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;
$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);

$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

Expand All @@ -120,6 +171,10 @@ public function testUpdateToken() {
->method('updateActivity')
->with($tk, $this->time);
$tk->setLastActivity($this->time - 200);
$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);

$this->tokenProvider->updateTokenActivity($tk);

Expand Down Expand Up @@ -157,6 +212,10 @@ public function testGetPassword() {
$password = 'passme';
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;
$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);

$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

Expand Down Expand Up @@ -185,6 +244,10 @@ public function testGetPasswordInvalidToken() {
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;

$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);
$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

$this->tokenProvider->getPassword($actual, 'wrongtoken');
Expand All @@ -197,6 +260,10 @@ public function testSetPassword() {
$password = 'passme';
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;
$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);

$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

Expand Down Expand Up @@ -301,14 +368,18 @@ public function testRenewSessionTokenWithoutPassword() {
$this->tokenProvider->renewSessionToken('oldId', 'newId');
}

public function testRenewSessionTokenWithPassword() {
public function testRenewSessionTokenWithPassword(): void {
$token = 'oldId';
$uid = 'user';
$user = 'User';
$password = 'password';
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;

$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);
$oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

$this->mapper
Expand All @@ -319,7 +390,7 @@ public function testRenewSessionTokenWithPassword() {
$this->mapper
->expects($this->once())
->method('insert')
->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) {
->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name): bool {
return $token->getUID() === $uid &&
$token->getLoginName() === $user &&
$token->getName() === $name &&
Expand All @@ -331,14 +402,14 @@ public function testRenewSessionTokenWithPassword() {
$this->mapper
->expects($this->once())
->method('delete')
->with($this->callback(function ($token) use ($oldToken) {
->with($this->callback(function ($token) use ($oldToken): bool {
return $token === $oldToken;
}));

$this->tokenProvider->renewSessionToken('oldId', 'newId');
}

public function testGetToken() {
public function testGetToken(): void {
$token = new PublicKeyToken();

$this->config->method('getSystemValue')
Expand Down Expand Up @@ -441,6 +512,10 @@ public function testRotate() {
$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
$type = IToken::PERMANENT_TOKEN;

$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);
$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);

$new = $this->tokenProvider->rotate($actual, 'oldtoken', 'newtoken');
Expand Down Expand Up @@ -507,6 +582,10 @@ public function testUpdatePasswords() {
'random2',
IToken::PERMANENT_TOKEN,
IToken::REMEMBER);
$this->config->method('getSystemValueBool')
->willReturnMap([
['auth.storeCryptedPassword', true, true],
]);

$this->mapper->method('hasExpiredTokens')
->with($uid)
Expand Down

0 comments on commit 3a3a52d

Please sign in to comment.