Passed
Pull Request — master (#165)
by Garion
02:02
created

SecurityExtension   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 130
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 61
dl 0
loc 130
rs 10
c 0
b 0
f 0
wmc 9

3 Methods

Rating   Name   Duplication   Size   Complexity  
A ResetAccountForm() 0 18 1
A doResetAccount() 0 62 4
A resetaccount() 0 23 4
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\Extension\AccountReset;
4
5
use SilverStripe\Control\HTTPRequest;
6
use SilverStripe\Control\HTTPResponse;
7
use SilverStripe\Core\Extensible;
8
use SilverStripe\Core\Extension;
9
use SilverStripe\Forms\FieldList;
10
use SilverStripe\Forms\Form;
11
use SilverStripe\Forms\FormAction;
12
use SilverStripe\Forms\PasswordField;
13
use SilverStripe\Forms\RequiredFields;
14
use SilverStripe\MFA\JSONResponse;
15
use SilverStripe\MFA\RequestHandler\BaseHandlerTrait;
16
use SilverStripe\ORM\FieldType\DBDatetime;
17
use SilverStripe\ORM\ValidationResult;
18
use SilverStripe\Security\Member;
19
use SilverStripe\Security\Security;
20
21
/**
22
 * Extends the Security controller to support Account Resets. This extension can
23
 * itself be extended to add procedures to the reset action (such as removing
24
 * additional authentication factors, sending alerts, etc.)
25
 *
26
 * @package SilverStripe\MFA\Extension
27
 * @property Security owner
28
 */
29
class SecurityExtension extends Extension
30
{
31
    use JSONResponse, BaseHandlerTrait, Extensible;
32
33
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
34
        'GET reset-account' => 'resetaccount',
35
        'ResetAccountForm' => 'ResetAccountForm',
36
    ];
37
38
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
39
        'resetaccount',
40
        'ResetAccountForm',
41
    ];
42
43
    public function resetaccount(HTTPRequest $request): HTTPResponse
44
    {
45
        if (Security::getCurrentUser()) {
46
            return $this->jsonResponse(['error' => 'Already authenticated'], 400);
47
        }
48
49
        ['m' => $memberID, 't' => $token] = $request->getVars();
50
51
        /** @var Member|MemberExtension $member */
52
        $member = Member::get()->byID($memberID);
53
54
        if (is_null($member) || $member->verifyAccountResetToken($token) === false) {
55
            return $this->jsonResponse(['error' => 'Invalid member or token'], 400);
56
        }
57
58
        $request->getSession()->set('MemberID', $member->ID);
59
60
        return $this->owner->getResponse()->setBody($this->owner->renderWith(
61
            'Security',
62
            [
63
                'Title' => _t(__CLASS__ . '.ACCOUNT_RESET_TITLE', 'Reset Account'),
64
                'Message' => _t(__CLASS__ . '.ACCOUNT_RESET_DESCRIPTION', 'Your password will be changed, and any registered MFA methods will be removed.'),
65
                'Form' => $this->ResetAccountForm(),
66
            ]
67
        ));
68
    }
69
70
    public function ResetAccountForm(): Form
71
    {
72
        $fields = FieldList::create([
73
            PasswordField::create('NewPassword1', _t('SilverStripe\\Security\\Member.NEWPASSWORD', 'New Password')),
74
            PasswordField::create('NewPassword2', _t('SilverStripe\\Security\\Member.CONFIRMNEWPASSWORD', 'Confirm New Password'))
75
        ]);
76
77
        $actions = FieldList::create([
78
            FormAction::create('doResetAccount', 'Reset Account'),
79
        ]);
80
81
        $validation = RequiredFields::create(['NewPassword1', 'NewPassword2']);
82
83
        $form = Form::create($this->owner, 'ResetAccountForm', $fields, $actions, $validation);
84
85
        $this->owner->extend('updateResetAccountForm', $form);
86
87
        return $form;
88
    }
89
90
    /**
91
     * Resets the user's password, and triggers other account reset procedures
92
     *
93
     * @param array $data
94
     * @param Form $form
95
     * @return HTTPResponse
96
     */
97
    public function doResetAccount(array $data, Form $form): HTTPResponse
98
    {
99
        $memberID = $this->owner->getRequest()->getSession()->get('MemberID');
100
101
        // If the ID isn't in the session, politely assume the session has expired
102
        if (!$memberID) {
103
            $form->sessionMessage(
104
                _t(
105
                    __CLASS__ . '.RESETTIMEDOUT',
106
                    "The account reset process timed out. Please click the link in the email and try again."
107
                ),
108
                ValidationResult::TYPE_ERROR
109
            );
110
111
            return $this->owner->redirectBack();
112
        }
113
114
        /** @var Member&MemberExtension $member */
115
        $member = Member::get()->byID(intval($memberID));
116
117
        // Fail if passwords do not match
118
        if ($data['NewPassword1'] !== $data['NewPassword2']) {
119
            $form->sessionMessage(
120
                _t(
121
                    'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
122
                    'You have entered your new password differently, try again'
123
                ),
124
                ValidationResult::TYPE_ERROR
125
            );
126
127
            return $this->owner->redirectBack();
128
        }
129
130
        // Check if the new password is accepted
131
        $validationResult = $member->changePassword($data['NewPassword1']);
132
        if (!$validationResult->isValid()) {
133
            $form->setSessionValidationResult($validationResult);
134
135
            return $this->owner->redirectBack();
136
        }
137
138
        // Clear locked out status
139
        $member->LockedOutUntil = null;
140
        $member->FailedLoginCount = null;
141
142
        // Clear account reset data
143
        $member->AccountResetHash = null;
144
        $member->AccountResetExpired = DBDatetime::create()->now();
145
        $member->write();
146
147
        // Pass off to extensions to perform any additional reset actions
148
        $this->extend('handleAccountReset', $member);
149
150
        // Send the user along to the login form (allowing any additional factors to kick in as needed)
151
        $this->owner->setSessionMessage(
152
            _t(
153
                __CLASS__ . '.RESETSUCCESSMESSAGE',
154
                'Reset complete. Please log in with your new password.'
155
            ),
156
            ValidationResult::TYPE_GOOD
157
        );
158
        return $this->owner->redirect($this->owner->Link('login'));
159
    }
160
}
161