Skip to content

Commit

Permalink
move verification token logic out of lost password controller
Browse files Browse the repository at this point in the history
- to make it reusable
- needed for local email verification

Signed-off-by: Arthur Schiwon <[email protected]>
  • Loading branch information
blizzz committed Aug 16, 2021
1 parent 4f01018 commit 6ede3a0
Show file tree
Hide file tree
Showing 8 changed files with 588 additions and 340 deletions.
112 changes: 30 additions & 82 deletions core/Controller/LostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\Encryption\IEncryptionModule;
use OCP\Encryption\IManager;
Expand All @@ -54,8 +53,8 @@
use OCP\IUser;
use OCP\IUserManager;
use OCP\Mail\IMailer;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Security\VerificationToken\InvalidTokenException;
use OCP\Security\VerificationToken\IVerificationToken;
use function array_filter;
use function count;
use function reset;
Expand All @@ -82,67 +81,46 @@ class LostController extends Controller {
protected $encryptionManager;
/** @var IConfig */
protected $config;
/** @var ISecureRandom */
protected $secureRandom;
/** @var IMailer */
protected $mailer;
/** @var ITimeFactory */
protected $timeFactory;
/** @var ICrypto */
protected $crypto;
/** @var ILogger */
private $logger;
/** @var Manager */
private $twoFactorManager;
/** @var IInitialStateService */
private $initialStateService;

/**
* @param string $appName
* @param IRequest $request
* @param IURLGenerator $urlGenerator
* @param IUserManager $userManager
* @param Defaults $defaults
* @param IL10N $l10n
* @param IConfig $config
* @param ISecureRandom $secureRandom
* @param string $defaultMailAddress
* @param IManager $encryptionManager
* @param IMailer $mailer
* @param ITimeFactory $timeFactory
* @param ICrypto $crypto
*/
public function __construct($appName,
IRequest $request,
IURLGenerator $urlGenerator,
IUserManager $userManager,
Defaults $defaults,
IL10N $l10n,
IConfig $config,
ISecureRandom $secureRandom,
$defaultMailAddress,
IManager $encryptionManager,
IMailer $mailer,
ITimeFactory $timeFactory,
ICrypto $crypto,
ILogger $logger,
Manager $twoFactorManager,
IInitialStateService $initialStateService) {
/** @var IVerificationToken */
private $verificationToken;

public function __construct(
$appName,
IRequest $request,
IURLGenerator $urlGenerator,
IUserManager $userManager,
Defaults $defaults,
IL10N $l10n,
IConfig $config,
$defaultMailAddress,
IManager $encryptionManager,
IMailer $mailer,
ILogger $logger,
Manager $twoFactorManager,
IInitialStateService $initialStateService,
IVerificationToken $verificationToken
) {
parent::__construct($appName, $request);
$this->urlGenerator = $urlGenerator;
$this->userManager = $userManager;
$this->defaults = $defaults;
$this->l10n = $l10n;
$this->secureRandom = $secureRandom;
$this->from = $defaultMailAddress;
$this->encryptionManager = $encryptionManager;
$this->config = $config;
$this->mailer = $mailer;
$this->timeFactory = $timeFactory;
$this->crypto = $crypto;
$this->logger = $logger;
$this->twoFactorManager = $twoFactorManager;
$this->initialStateService = $initialStateService;
$this->verificationToken = $verificationToken;
}

/**
Expand Down Expand Up @@ -192,36 +170,14 @@ public function resetform($token, $userId) {
* @param string $userId
* @throws \Exception
*/
protected function checkPasswordResetToken($token, $userId) {
$user = $this->userManager->get($userId);
if ($user === null || !$user->isEnabled()) {
throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
}

$encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
if ($encryptedToken === null) {
throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
}

protected function checkPasswordResetToken(string $token, string $userId): void {
try {
$mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
$decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
} catch (\Exception $e) {
throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
}

$splittedToken = explode(':', $decryptedToken);
if (count($splittedToken) !== 2) {
throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
}

if ($splittedToken[0] < ($this->timeFactory->getTime() - 60 * 60 * 24 * 7) ||
$user->getLastLogin() > $splittedToken[0]) {
throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
}

if (!hash_equals($splittedToken[1], $token)) {
throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
$this->verificationToken->check($token, $this->userManager->get($userId), 'lostpassword');
} catch (InvalidTokenException $e) {
$error = $e->getCode() === InvalidTokenException::TOKEN_EXPIRED
? $this->l10n->t('Could not reset password because the token is expired')
: $this->l10n->t('Could not reset password because the token is invalid');
throw new \Exception($error, (int)$e->getCode(), $e);
}
}

Expand Down Expand Up @@ -343,15 +299,7 @@ protected function sendEmail($input) {
// secret being the users' email address appended with the system secret.
// This makes the token automatically invalidate once the user changes
// their email address.
$token = $this->secureRandom->generate(
21,
ISecureRandom::CHAR_DIGITS.
ISecureRandom::CHAR_LOWER.
ISecureRandom::CHAR_UPPER
);
$tokenValue = $this->timeFactory->getTime() .':'. $token;
$encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
$this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
$token = $this->verificationToken->create($user, 'lostpassword', $email);

$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $user->getUID(), 'token' => $token]);

Expand Down
3 changes: 3 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,8 @@
'OCP\\Security\\ICrypto' => $baseDir . '/lib/public/Security/ICrypto.php',
'OCP\\Security\\IHasher' => $baseDir . '/lib/public/Security/IHasher.php',
'OCP\\Security\\ISecureRandom' => $baseDir . '/lib/public/Security/ISecureRandom.php',
'OCP\\Security\\VerificationToken\\IVerificationToken' => $baseDir . '/lib/public/Security/VerificationToken/IVerificationToken.php',
'OCP\\Security\\VerificationToken\\InvalidTokenException' => $baseDir . '/lib/public/Security/VerificationToken/InvalidTokenException.php',
'OCP\\Session\\Exceptions\\SessionNotAvailableException' => $baseDir . '/lib/public/Session/Exceptions/SessionNotAvailableException.php',
'OCP\\Settings\\IIconSection' => $baseDir . '/lib/public/Settings/IIconSection.php',
'OCP\\Settings\\IManager' => $baseDir . '/lib/public/Settings/IManager.php',
Expand Down Expand Up @@ -1371,6 +1373,7 @@
'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php',
'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php',
'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\VerificationToken' => $baseDir . '/lib/private/Security/VerificationToken/VerificationToken.php',
'OC\\Server' => $baseDir . '/lib/private/Server.php',
'OC\\ServerContainer' => $baseDir . '/lib/private/ServerContainer.php',
'OC\\ServerNotAvailableException' => $baseDir . '/lib/private/ServerNotAvailableException.php',
Expand Down
3 changes: 3 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\Security\\ICrypto' => __DIR__ . '/../../..' . '/lib/public/Security/ICrypto.php',
'OCP\\Security\\IHasher' => __DIR__ . '/../../..' . '/lib/public/Security/IHasher.php',
'OCP\\Security\\ISecureRandom' => __DIR__ . '/../../..' . '/lib/public/Security/ISecureRandom.php',
'OCP\\Security\\VerificationToken\\IVerificationToken' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/IVerificationToken.php',
'OCP\\Security\\VerificationToken\\InvalidTokenException' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/InvalidTokenException.php',
'OCP\\Session\\Exceptions\\SessionNotAvailableException' => __DIR__ . '/../../..' . '/lib/public/Session/Exceptions/SessionNotAvailableException.php',
'OCP\\Settings\\IIconSection' => __DIR__ . '/../../..' . '/lib/public/Settings/IIconSection.php',
'OCP\\Settings\\IManager' => __DIR__ . '/../../..' . '/lib/public/Settings/IManager.php',
Expand Down Expand Up @@ -1400,6 +1402,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php',
'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php',
'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\VerificationToken' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/VerificationToken.php',
'OC\\Server' => __DIR__ . '/../../..' . '/lib/private/Server.php',
'OC\\ServerContainer' => __DIR__ . '/../../..' . '/lib/private/ServerContainer.php',
'OC\\ServerNotAvailableException' => __DIR__ . '/../../..' . '/lib/private/ServerNotAvailableException.php',
Expand Down
111 changes: 111 additions & 0 deletions lib/private/Security/VerificationToken/VerificationToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

namespace OC\Security\VerificationToken;

use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IUser;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Security\VerificationToken\InvalidTokenException;
use OCP\Security\VerificationToken\IVerificationToken;

class VerificationToken implements IVerificationToken {

/** @var IConfig */
private $config;
/** @var ICrypto */
private $crypto;
/** @var ITimeFactory */
private $timeFactory;
/** @var ISecureRandom */
private $secureRandom;

public function __construct(
IConfig $config,
ICrypto $crypto,
ITimeFactory $timeFactory,
ISecureRandom $secureRandom
) {
$this->config = $config;
$this->crypto = $crypto;
$this->timeFactory = $timeFactory;
$this->secureRandom = $secureRandom;
}

/**
* @throws InvalidTokenException
*/
protected function throwInvalidTokenException(int $code): void {
throw new InvalidTokenException($code);
}

public function check(string $token, ?IUser $user, string $subject, string $passwordPrefix = ''): void {
if ($user === null || !$user->isEnabled()) {
$this->throwInvalidTokenException(InvalidTokenException::USER_UNKNOWN);
}

$encryptedToken = $this->config->getUserValue($user->getUID(), 'core', $subject, null);
if ($encryptedToken === null) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_NOT_FOUND);
}

try {
$decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix.$this->config->getSystemValue('secret'));
} catch (\Exception $e) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_DECRYPTION_ERROR);
}

$splitToken = explode(':', $decryptedToken ?? '');
if (count($splitToken) !== 2) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_INVALID_FORMAT);
}

if ($splitToken[0] < ($this->timeFactory->getTime() - 60 * 60 * 24 * 7) ||
$user->getLastLogin() > $splitToken[0]) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_EXPIRED);
}

if (!hash_equals($splitToken[1], $token)) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_MISMATCH);
}
}

public function create(IUser $user, string $subject, string $passwordPrefix = ''): string {
$token = $this->secureRandom->generate(
21,
ISecureRandom::CHAR_DIGITS.
ISecureRandom::CHAR_LOWER.
ISecureRandom::CHAR_UPPER
);
$tokenValue = $this->timeFactory->getTime() .':'. $token;
$encryptedValue = $this->crypto->encrypt($tokenValue, $passwordPrefix . $this->config->getSystemValue('secret'));
$this->config->setUserValue($user->getUID(), 'core', $subject, $encryptedValue);

return $token;
}
}
55 changes: 55 additions & 0 deletions lib/public/Security/VerificationToken/IVerificationToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

namespace OCP\Security\VerificationToken;

use OCP\IUser;

/**
* @since 23.0.0
*/
interface IVerificationToken {

/**
* Checks whether the a provided tokent matches a stored token and its
* constraints. An InvalidTokenException is thrown on issues, otherwise
* the check is successful.
*
* null can be passed as $user, but mind that this is for conveniently
* passing the return of IUserManager::getUser() to this method. When
* $user is null, InvalidTokenException is thrown for all the issued
* tokens are user related.
*
* @throws InvalidTokenException
* @since 23.0.0
*/
public function check(string $token, ?IUser $user, string $subject, string $passwordPrefix = ''): void;

/**
* @since 23.0.0
*/
public function create(IUser $user, string $subject, string $passwordPrefix = ''): string;
}
Loading

0 comments on commit 6ede3a0

Please sign in to comment.