Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #165 from creative-commoners/pulls/1.0/process-acc…
…ount-reset Group AccountReset extensions, implement full Account Reset flow
- Loading branch information
Showing
13 changed files
with
436 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace SilverStripe\MFA\Extension\AccountReset; | ||
|
||
use SilverStripe\Core\Extension; | ||
use SilverStripe\MFA\Extension\MemberExtension as MemberExtension; | ||
use SilverStripe\Security\Member; | ||
|
||
/** | ||
* Handles removing a member's registered MFA methods during Account Reset. Also | ||
* resets the 'MFA Skipped' flag on the member so that they are prompted to | ||
* set up MFA again when they next log in. | ||
* | ||
* @package SilverStripe\MFA\Extension\AccountReset | ||
*/ | ||
class MFAResetExtension extends Extension | ||
{ | ||
/** | ||
* @param Member&MemberExtension $member | ||
*/ | ||
public function handleAccountReset(Member $member) | ||
{ | ||
foreach ($member->RegisteredMFAMethods() as $method) { | ||
$method->delete(); | ||
} | ||
|
||
$member->HasSkippedMFARegistration = false; | ||
$member->write(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace SilverStripe\MFA\Extension\AccountReset; | ||
|
||
use SilverStripe\Control\HTTPRequest; | ||
use SilverStripe\Control\HTTPResponse; | ||
use SilverStripe\Core\Extensible; | ||
use SilverStripe\Core\Extension; | ||
use SilverStripe\Forms\FieldList; | ||
use SilverStripe\Forms\Form; | ||
use SilverStripe\Forms\FormAction; | ||
use SilverStripe\Forms\PasswordField; | ||
use SilverStripe\Forms\RequiredFields; | ||
use SilverStripe\MFA\JSONResponse; | ||
use SilverStripe\MFA\RequestHandler\BaseHandlerTrait; | ||
use SilverStripe\ORM\FieldType\DBDatetime; | ||
use SilverStripe\ORM\ValidationResult; | ||
use SilverStripe\Security\Member; | ||
use SilverStripe\Security\Security; | ||
|
||
/** | ||
* Extends the Security controller to support Account Resets. This extension can | ||
* itself be extended to add procedures to the reset action (such as removing | ||
* additional authentication factors, sending alerts, etc.) | ||
* | ||
* @package SilverStripe\MFA\Extension | ||
* @property Security owner | ||
*/ | ||
class SecurityExtension extends Extension | ||
{ | ||
use BaseHandlerTrait; | ||
use Extensible; | ||
|
||
private static $url_handlers = [ | ||
'GET reset-account' => 'resetaccount', | ||
]; | ||
|
||
private static $allowed_actions = [ | ||
'resetaccount', | ||
'ResetAccountForm', | ||
]; | ||
|
||
public function resetaccount(HTTPRequest $request) | ||
{ | ||
if (Security::getCurrentUser()) { | ||
$output = $this->owner->renderWith( | ||
'Security', | ||
[ | ||
'Title' => _t( | ||
__CLASS__ . '.ALREADYAUTHENTICATEDTITLE', | ||
'Already authenticated' | ||
), | ||
'Content' => _t( | ||
__CLASS__ . '.ALREADYAUTHENTICATEDBODY', | ||
'You must be logged out to reset your account.' | ||
), | ||
] | ||
); | ||
return $this->owner->getResponse()->setBody($output)->setStatusCode(400); | ||
} | ||
|
||
$vars = $request->getVars(); | ||
|
||
/** @var Member|MemberExtension $member */ | ||
$member = Member::get()->byID(intval($vars['m'] ?? 0)); | ||
|
||
if (is_null($member) || $member->verifyAccountResetToken($vars['t'] ?? '') === false) { | ||
$output = $this->owner->renderWith( | ||
'Security', | ||
[ | ||
'Title' => _t( | ||
__CLASS__ . '.INVALIDTOKENTITLE', | ||
'Invalid member or token' | ||
), | ||
'Content' => _t( | ||
__CLASS__ . '.INVALIDTOKENBODY', | ||
'Your account reset token may have expired. Please contact an administrator.' | ||
) | ||
] | ||
); | ||
return $this->owner->getResponse()->setBody($output)->setStatusCode(400); | ||
} | ||
|
||
$request->getSession()->set('MemberID', $member->ID); | ||
|
||
return $this->owner->getResponse()->setBody($this->owner->renderWith( | ||
'Security', | ||
[ | ||
'Title' => _t( | ||
__CLASS__ . '.ACCOUNT_RESET_TITLE', | ||
'Reset account' | ||
), | ||
'Message' => _t( | ||
__CLASS__ . '.ACCOUNT_RESET_DESCRIPTION', | ||
'Your password will be changed, and any registered MFA methods will be removed.' | ||
), | ||
'Form' => $this->ResetAccountForm(), | ||
] | ||
)); | ||
} | ||
|
||
public function ResetAccountForm(): Form | ||
{ | ||
$fields = FieldList::create([ | ||
PasswordField::create( | ||
'NewPassword1', | ||
_t( | ||
'SilverStripe\\Security\\Member.NEWPASSWORD', | ||
'New password' | ||
) | ||
), | ||
PasswordField::create( | ||
'NewPassword2', | ||
_t( | ||
'SilverStripe\\Security\\Member.CONFIRMNEWPASSWORD', | ||
'Confirm new password' | ||
) | ||
), | ||
]); | ||
|
||
$actions = FieldList::create([ | ||
FormAction::create('doResetAccount', 'Reset account'), | ||
]); | ||
|
||
$validation = RequiredFields::create(['NewPassword1', 'NewPassword2']); | ||
|
||
$form = Form::create($this->owner, 'ResetAccountForm', $fields, $actions, $validation); | ||
|
||
$this->owner->extend('updateResetAccountForm', $form); | ||
|
||
return $form; | ||
} | ||
|
||
/** | ||
* Resets the user's password, and triggers other account reset procedures | ||
* | ||
* @param array $data | ||
* @param Form $form | ||
* @return HTTPResponse | ||
*/ | ||
public function doResetAccount(array $data, Form $form): HTTPResponse | ||
{ | ||
$memberID = $this->owner->getRequest()->getSession()->get('MemberID'); | ||
|
||
// If the ID isn't in the session, politely assume the session has expired | ||
if (!$memberID) { | ||
$form->sessionMessage( | ||
_t( | ||
__CLASS__ . '.RESETTIMEDOUT', | ||
"The account reset process timed out. Please click the link in the email and try again." | ||
), | ||
ValidationResult::TYPE_ERROR | ||
); | ||
|
||
return $this->owner->redirectBack(); | ||
} | ||
|
||
/** @var Member&MemberExtension $member */ | ||
$member = Member::get()->byID((int) $memberID); | ||
|
||
// Fail if passwords do not match | ||
if ($data['NewPassword1'] !== $data['NewPassword2']) { | ||
$form->sessionMessage( | ||
_t( | ||
'SilverStripe\\Security\\Member.ERRORNEWPASSWORD', | ||
'You have entered your new password differently, try again' | ||
), | ||
ValidationResult::TYPE_ERROR | ||
); | ||
|
||
return $this->owner->redirectBack(); | ||
} | ||
|
||
// Check if the new password is accepted | ||
$validationResult = $member->changePassword($data['NewPassword1']); | ||
if (!$validationResult->isValid()) { | ||
$form->setSessionValidationResult($validationResult); | ||
|
||
return $this->owner->redirectBack(); | ||
} | ||
|
||
// Clear locked out status | ||
$member->LockedOutUntil = null; | ||
$member->FailedLoginCount = null; | ||
|
||
// Clear account reset data | ||
$member->AccountResetHash = null; | ||
$member->AccountResetExpired = DBDatetime::create()->now(); | ||
$member->write(); | ||
|
||
// Pass off to extensions to perform any additional reset actions | ||
$this->extend('handleAccountReset', $member); | ||
|
||
// Send the user along to the login form (allowing any additional factors to kick in as needed) | ||
$this->owner->setSessionMessage( | ||
_t( | ||
__CLASS__ . '.RESETSUCCESSMESSAGE', | ||
'Reset complete. Please log in with your new password.' | ||
), | ||
ValidationResult::TYPE_GOOD | ||
); | ||
return $this->owner->redirect($this->owner->Link('login')); | ||
} | ||
} |
Oops, something went wrong.