2014-05-28 00:09:08 +03:00
< ? php
2024-05-27 10:08:53 +02:00
2014-05-28 00:09:08 +03:00
/**
2024-05-27 10:08:53 +02:00
* SPDX - FileCopyrightText : 2016 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2014-05-28 00:09:08 +03:00
*/
2016-01-20 10:42:19 +01:00
namespace OC\Core\Controller ;
2014-05-28 00:09:08 +03:00
2022-03-21 09:49:45 +01:00
use Exception ;
2022-10-17 14:36:24 +02:00
use OC\Authentication\TwoFactorAuth\Manager ;
use OC\Core\Events\BeforePasswordResetEvent ;
use OC\Core\Events\PasswordResetEvent ;
use OC\Core\Exception\ResetPasswordException ;
use OC\Security\RateLimiting\Exception\RateLimitExceededException ;
use OC\Security\RateLimiting\Limiter ;
2019-11-22 20:52:10 +01:00
use OCP\AppFramework\Controller ;
2024-07-25 13:24:59 +02:00
use OCP\AppFramework\Http\Attribute\AnonRateLimit ;
use OCP\AppFramework\Http\Attribute\BruteForceProtection ;
2024-01-10 12:35:44 +01:00
use OCP\AppFramework\Http\Attribute\FrontpageRoute ;
2024-07-25 13:24:59 +02:00
use OCP\AppFramework\Http\Attribute\NoCSRFRequired ;
2024-01-18 10:38:37 +01:00
use OCP\AppFramework\Http\Attribute\OpenAPI ;
2024-07-25 13:24:59 +02:00
use OCP\AppFramework\Http\Attribute\PublicPage ;
2017-04-14 13:42:40 +02:00
use OCP\AppFramework\Http\JSONResponse ;
2019-11-22 20:52:10 +01:00
use OCP\AppFramework\Http\TemplateResponse ;
2022-03-21 09:49:45 +01:00
use OCP\AppFramework\Services\IInitialState ;
2017-04-07 15:42:43 -05:00
use OCP\Defaults ;
2018-08-10 16:12:34 +02:00
use OCP\Encryption\IEncryptionModule ;
2016-09-29 16:38:29 +02:00
use OCP\Encryption\IManager ;
2022-03-21 09:26:55 +01:00
use OCP\EventDispatcher\IEventDispatcher ;
2021-06-29 19:20:33 -04:00
use OCP\HintException ;
2019-11-22 20:52:10 +01:00
use OCP\IConfig ;
use OCP\IL10N ;
use OCP\IRequest ;
use OCP\IURLGenerator ;
2017-03-28 20:39:36 +02:00
use OCP\IUser ;
2014-10-20 19:05:48 +02:00
use OCP\IUserManager ;
2015-02-12 16:03:51 +01:00
use OCP\Mail\IMailer ;
2022-10-17 14:36:24 +02:00
use OCP\Security\VerificationToken\InvalidTokenException ;
use OCP\Security\VerificationToken\IVerificationToken ;
2022-04-12 17:55:01 +02:00
use Psr\Log\LoggerInterface ;
2021-06-29 19:20:33 -04:00
use function array_filter ;
use function count ;
2019-07-09 11:58:14 +02:00
use function reset ;
2014-05-28 00:09:08 +03:00
2014-10-20 19:05:48 +02:00
/**
* Class LostController
*
2014-10-24 13:41:44 +02:00
* Successfully changing a password will emit the post_passwordReset hook .
*
2016-01-20 10:42:19 +01:00
* @ package OC\Core\Controller
2014-10-20 19:05:48 +02:00
*/
2024-01-18 10:38:37 +01:00
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
2014-05-28 00:09:08 +03:00
class LostController extends Controller {
2022-04-12 17:55:01 +02:00
protected string $from ;
2022-03-21 09:26:55 +01:00
2023-06-05 18:42:42 +03:30
public function __construct (
string $appName ,
IRequest $request ,
private IURLGenerator $urlGenerator ,
private IUserManager $userManager ,
private Defaults $defaults ,
private IL10N $l10n ,
private IConfig $config ,
string $defaultMailAddress ,
private IManager $encryptionManager ,
private IMailer $mailer ,
private LoggerInterface $logger ,
private Manager $twoFactorManager ,
private IInitialState $initialState ,
private IVerificationToken $verificationToken ,
private IEventDispatcher $eventDispatcher ,
private Limiter $limiter ,
) {
2014-05-28 00:09:08 +03:00
parent :: __construct ( $appName , $request );
2016-09-29 16:38:29 +02:00
$this -> from = $defaultMailAddress ;
2014-05-28 00:09:08 +03:00
}
/**
2014-06-06 16:36:10 +02:00
* Someone wants to reset their password :
2014-05-28 00:09:08 +03:00
*/
2024-07-25 13:24:59 +02:00
#[PublicPage]
#[NoCSRFRequired]
2024-07-27 22:34:29 +02:00
#[BruteForceProtection(action: 'passwordResetEmail')]
#[AnonRateLimit(limit: 10, period: 300)]
2024-01-10 12:35:44 +01:00
#[FrontpageRoute(verb: 'GET', url: '/lostpassword/reset/form/{token}/{userId}')]
2022-04-12 17:55:01 +02:00
public function resetform ( string $token , string $userId ) : TemplateResponse {
2016-05-19 13:23:12 +02:00
try {
$this -> checkPasswordResetToken ( $token , $userId );
2022-03-21 09:49:45 +01:00
} catch ( Exception $e ) {
2021-09-10 22:40:10 +02:00
if ( $this -> config -> getSystemValue ( 'lost_password_link' , '' ) !== 'disabled'
|| ( $e instanceof InvalidTokenException
&& ! in_array ( $e -> getCode (), [ InvalidTokenException :: TOKEN_NOT_FOUND , InvalidTokenException :: USER_UNKNOWN ]))
) {
2023-01-20 13:10:09 +01:00
$response = new TemplateResponse (
2021-09-10 22:40:10 +02:00
'core' , 'error' , [
'errors' => [[ 'error' => $e -> getMessage ()]]
],
TemplateResponse :: RENDER_AS_GUEST
);
2023-01-20 13:10:09 +01:00
$response -> throttle ();
return $response ;
2021-09-10 22:40:10 +02:00
}
return new TemplateResponse ( 'core' , 'error' , [
'errors' => [[ 'error' => $this -> l10n -> t ( 'Password reset is disabled' )]]
],
TemplateResponse :: RENDER_AS_GUEST
2016-05-19 13:23:12 +02:00
);
}
2022-03-21 09:49:45 +01:00
$this -> initialState -> provideInitialState ( 'resetPasswordUser' , $userId );
$this -> initialState -> provideInitialState ( 'resetPasswordTarget' ,
2019-07-25 17:04:33 +02:00
$this -> urlGenerator -> linkToRouteAbsolute ( 'core.lost.setPassword' , [ 'userId' => $userId , 'token' => $token ])
);
2016-05-19 13:23:12 +02:00
2014-06-06 16:36:10 +02:00
return new TemplateResponse (
2016-01-20 10:42:19 +01:00
'core' ,
2019-07-25 17:04:33 +02:00
'login' ,
[],
2014-06-06 16:36:10 +02:00
'guest'
);
2014-05-28 00:09:08 +03:00
}
2014-06-06 16:36:10 +02:00
2016-05-19 13:23:12 +02:00
/**
2022-03-21 09:49:45 +01:00
* @ throws Exception
2016-05-19 13:23:12 +02:00
*/
2021-08-13 15:53:17 +02:00
protected function checkPasswordResetToken ( string $token , string $userId ) : void {
2016-08-28 14:22:29 +02:00
try {
2021-09-10 19:06:50 +02:00
$user = $this -> userManager -> get ( $userId );
$this -> verificationToken -> check ( $token , $user , 'lostpassword' , $user ? $user -> getEMailAddress () : '' , true );
2021-08-13 15:53:17 +02:00
} 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' );
2022-03-21 09:49:45 +01:00
throw new Exception ( $error , ( int ) $e -> getCode (), $e );
2016-05-19 13:23:12 +02:00
}
}
2022-04-12 17:55:01 +02:00
private function error ( string $message , array $additional = []) : array {
2020-03-26 09:30:18 +01:00
return array_merge ([ 'status' => 'error' , 'msg' => $message ], $additional );
2014-06-06 16:51:58 +02:00
}
2022-04-12 17:55:01 +02:00
private function success ( array $data = []) : array {
2020-10-05 15:12:57 +02:00
return array_merge ( $data , [ 'status' => 'success' ]);
2014-06-06 16:51:58 +02:00
}
2024-07-25 13:24:59 +02:00
#[PublicPage]
2024-07-27 22:34:29 +02:00
#[BruteForceProtection(action: 'passwordResetEmail')]
#[AnonRateLimit(limit: 10, period: 300)]
2024-01-10 12:35:44 +01:00
#[FrontpageRoute(verb: 'POST', url: '/lostpassword/email')]
2022-04-12 17:55:01 +02:00
public function email ( string $user ) : JSONResponse {
2017-05-11 16:46:43 +02:00
if ( $this -> config -> getSystemValue ( 'lost_password_link' , '' ) !== '' ) {
return new JSONResponse ( $this -> error ( $this -> l10n -> t ( 'Password reset is disabled' )));
}
2023-04-04 10:03:15 -04:00
$user = trim ( $user );
2023-05-15 09:21:07 +02:00
2024-03-15 11:46:19 +01:00
if ( strlen ( $user ) > 255 ) {
return new JSONResponse ( $this -> error ( $this -> l10n -> t ( 'Unsupported email length (>255)' )));
}
2017-12-22 15:22:49 +01:00
\OCP\Util :: emitHook (
'\OCA\Files_Sharing\API\Server2Server' ,
'preLoginNameUsedAsUserName' ,
[ 'uid' => & $user ]
);
2014-06-06 16:36:10 +02:00
// FIXME: use HTTP error codes
2014-05-28 20:13:07 +03:00
try {
2023-04-04 10:03:15 -04:00
$this -> sendEmail ( $user );
2019-07-26 15:21:41 +02:00
} catch ( ResetPasswordException $e ) {
2019-01-14 21:05:52 +01:00
// Ignore the error since we do not want to leak this info
2019-07-26 15:21:41 +02:00
$this -> logger -> warning ( 'Could not send password reset email: ' . $e -> getMessage ());
2022-03-21 09:49:45 +01:00
} catch ( Exception $e ) {
2022-04-12 17:55:01 +02:00
$this -> logger -> error ( $e -> getMessage (), [ 'exception' => $e ]);
2014-05-28 20:13:07 +03:00
}
2014-06-06 16:36:10 +02:00
2017-04-14 13:42:40 +02:00
$response = new JSONResponse ( $this -> success ());
$response -> throttle ();
return $response ;
2014-05-28 20:13:07 +03:00
}
2014-06-06 16:36:10 +02:00
2024-07-25 13:24:59 +02:00
#[PublicPage]
2024-07-27 22:34:29 +02:00
#[BruteForceProtection(action: 'passwordResetEmail')]
#[AnonRateLimit(limit: 10, period: 300)]
2024-01-10 12:35:44 +01:00
#[FrontpageRoute(verb: 'POST', url: '/lostpassword/set/{token}/{userId}')]
2023-05-15 09:21:07 +02:00
public function setPassword ( string $token , string $userId , string $password , bool $proceed ) : JSONResponse {
2016-09-29 16:38:29 +02:00
if ( $this -> encryptionManager -> isEnabled () && ! $proceed ) {
2018-08-10 16:12:34 +02:00
$encryptionModules = $this -> encryptionManager -> getEncryptionModules ();
foreach ( $encryptionModules as $module ) {
/** @var IEncryptionModule $instance */
$instance = call_user_func ( $module [ 'callback' ]);
// this way we can find out whether per-user keys are used or a system wide encryption key
if ( $instance -> needDetailedAccessList ()) {
2023-05-15 09:21:07 +02:00
return new JSONResponse ( $this -> error ( '' , [ 'encryption' => true ]));
2018-08-10 16:12:34 +02:00
}
}
2014-10-20 19:05:48 +02:00
}
2014-05-28 20:13:07 +03:00
try {
2016-05-19 13:23:12 +02:00
$this -> checkPasswordResetToken ( $token , $userId );
2014-06-06 16:51:58 +02:00
$user = $this -> userManager -> get ( $userId );
2014-06-06 16:36:10 +02:00
2022-03-21 09:26:55 +01:00
$this -> eventDispatcher -> dispatchTyped ( new BeforePasswordResetEvent ( $user , $password ));
2020-03-26 09:30:18 +01:00
\OC_Hook :: emit ( '\OC\Core\LostPassword\Controller\LostController' , 'pre_passwordReset' , [ 'uid' => $userId , 'password' => $password ]);
2017-01-02 21:24:37 +01:00
2023-01-04 11:23:43 +01:00
if ( strlen ( $password ) > IUserManager :: MAX_PASSWORD_LENGTH ) {
2023-01-03 16:36:01 +01:00
throw new HintException ( 'Password too long' , $this -> l10n -> t ( 'Password is too long. Maximum allowed length is 469 characters.' ));
}
2014-07-24 12:50:39 +02:00
if ( ! $user -> setPassword ( $password )) {
2022-03-21 09:49:45 +01:00
throw new Exception ();
2014-05-28 20:13:07 +03:00
}
2014-06-06 16:36:10 +02:00
2022-03-21 09:26:55 +01:00
$this -> eventDispatcher -> dispatchTyped ( new PasswordResetEvent ( $user , $password ));
2020-03-26 09:30:18 +01:00
\OC_Hook :: emit ( '\OC\Core\LostPassword\Controller\LostController' , 'post_passwordReset' , [ 'uid' => $userId , 'password' => $password ]);
2014-10-24 13:41:44 +02:00
2019-01-28 16:12:06 +01:00
$this -> twoFactorManager -> clearTwoFactorPending ( $userId );
2016-08-23 15:01:38 +02:00
$this -> config -> deleteUserValue ( $userId , 'core' , 'lostpassword' );
2017-07-24 12:17:53 +02:00
@ \OC :: $server -> getUserSession () -> unsetMagicInCookie ();
2018-05-20 12:51:50 +02:00
} catch ( HintException $e ) {
2023-05-15 09:21:07 +02:00
$response = new JSONResponse ( $this -> error ( $e -> getHint ()));
$response -> throttle ();
return $response ;
2022-03-21 09:49:45 +01:00
} catch ( Exception $e ) {
2023-05-15 09:21:07 +02:00
$response = new JSONResponse ( $this -> error ( $e -> getMessage ()));
$response -> throttle ();
return $response ;
2014-05-28 20:13:07 +03:00
}
2014-06-06 16:36:10 +02:00
2023-05-15 09:21:07 +02:00
return new JSONResponse ( $this -> success ([ 'user' => $userId ]));
2014-05-28 20:13:07 +03:00
}
2014-06-06 16:36:10 +02:00
2014-10-20 19:05:48 +02:00
/**
2019-07-26 15:21:41 +02:00
* @ throws ResetPasswordException
* @ throws \OCP\PreConditionNotMetException
2014-10-20 19:05:48 +02:00
*/
2022-04-12 17:55:01 +02:00
protected function sendEmail ( string $input ) : void {
2017-03-28 20:39:36 +02:00
$user = $this -> findUserByIdOrMail ( $input );
$email = $user -> getEMailAddress ();
2014-06-06 16:36:10 +02:00
2014-05-28 20:13:07 +03:00
if ( empty ( $email )) {
2019-07-26 15:21:41 +02:00
throw new ResetPasswordException ( 'Could not send reset e-mail since there is no email for username ' . $input );
2014-05-28 20:13:07 +03:00
}
2014-06-06 16:36:10 +02:00
2022-10-17 14:36:24 +02:00
try {
$this -> limiter -> registerUserRequest ( 'lostpasswordemail' , 5 , 1800 , $user );
} catch ( RateLimitExceededException $e ) {
throw new ResetPasswordException ( 'Could not send reset e-mail, 5 of them were already sent in the last 30 minutes' , 0 , $e );
}
2016-08-28 14:22:29 +02:00
// Generate the token. It is stored encrypted in the database with the
// secret being the users' email address appended with the system secret.
// This makes the token automatically invalidate once the user changes
// their email address.
2021-08-13 15:53:17 +02:00
$token = $this -> verificationToken -> create ( $user , 'lostpassword' , $email );
2014-10-20 19:05:48 +02:00
2020-03-26 09:30:18 +01:00
$link = $this -> urlGenerator -> linkToRouteAbsolute ( 'core.lost.resetform' , [ 'userId' => $user -> getUID (), 'token' => $token ]);
2014-06-06 16:36:10 +02:00
2017-09-04 15:07:19 +02:00
$emailTemplate = $this -> mailer -> createEMailTemplate ( 'core.ResetPassword' , [
2017-08-24 18:02:37 +02:00
'link' => $link ,
]);
2017-04-11 17:24:58 -05:00
2017-09-15 10:59:11 +02:00
$emailTemplate -> setSubject ( $this -> l10n -> t ( '%s password reset' , [ $this -> defaults -> getName ()]));
2017-04-11 17:24:58 -05:00
$emailTemplate -> addHeader ();
$emailTemplate -> addHeading ( $this -> l10n -> t ( 'Password reset' ));
$emailTemplate -> addBodyText (
2018-02-15 12:18:51 +01:00
htmlspecialchars ( $this -> l10n -> t ( 'Click the following button to reset your password. If you have not requested the password reset, then ignore this email.' )),
2017-04-11 17:24:58 -05:00
$this -> l10n -> t ( 'Click the following link to reset your password. If you have not requested the password reset, then ignore this email.' )
);
$emailTemplate -> addBodyButton (
2018-02-15 12:18:51 +01:00
htmlspecialchars ( $this -> l10n -> t ( 'Reset your password' )),
2017-04-11 17:24:58 -05:00
$link ,
false
);
$emailTemplate -> addFooter ();
2014-06-06 16:36:10 +02:00
2014-05-28 20:13:07 +03:00
try {
2015-02-12 16:03:51 +01:00
$message = $this -> mailer -> createMessage ();
2021-02-18 12:38:43 +01:00
$message -> setTo ([ $email => $user -> getDisplayName ()]);
2015-02-12 16:03:51 +01:00
$message -> setFrom ([ $this -> from => $this -> defaults -> getName ()]);
2017-09-15 11:01:21 +02:00
$message -> useTemplate ( $emailTemplate );
2015-02-12 16:03:51 +01:00
$this -> mailer -> send ( $message );
2022-03-21 09:49:45 +01:00
} catch ( Exception $e ) {
2019-07-26 15:21:41 +02:00
// Log the exception and continue
2022-04-12 17:55:01 +02:00
$this -> logger -> error ( $e -> getMessage (), [ 'app' => 'core' , 'exception' => $e ]);
2014-05-28 20:13:07 +03:00
}
}
2014-05-28 00:09:08 +03:00
2017-03-28 20:39:36 +02:00
/**
2019-07-26 15:21:41 +02:00
* @ throws ResetPasswordException
2017-03-28 20:39:36 +02:00
*/
2022-04-12 17:55:01 +02:00
protected function findUserByIdOrMail ( string $input ) : IUser {
2017-03-28 20:39:36 +02:00
$user = $this -> userManager -> get ( $input );
if ( $user instanceof IUser ) {
2017-08-18 13:03:40 +02:00
if ( ! $user -> isEnabled ()) {
2022-09-21 17:44:32 +02:00
throw new ResetPasswordException ( 'Account ' . $user -> getUID () . ' is disabled' );
2017-08-18 13:03:40 +02:00
}
2017-03-28 20:39:36 +02:00
return $user ;
}
2017-08-18 13:03:40 +02:00
2019-07-09 11:58:14 +02:00
$users = array_filter ( $this -> userManager -> getByEmail ( $input ), function ( IUser $user ) {
2018-08-18 16:51:59 +02:00
return $user -> isEnabled ();
});
2019-07-09 11:58:14 +02:00
if ( count ( $users ) === 1 ) {
return reset ( $users );
2017-03-28 20:39:36 +02:00
}
2022-08-19 18:30:32 +02:00
throw new ResetPasswordException ( 'Could not find user ' . $input );
2017-03-28 20:39:36 +02:00
}
2014-05-28 00:09:08 +03:00
}