Completed
Push — master ( f0b03a...fa0cb8 )
by Robbie
27s queued 11s
created

SecurityExtension   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 174
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 89
dl 0
loc 174
rs 10
c 0
b 0
f 0
wmc 9

3 Methods

Rating   Name   Duplication   Size   Complexity  
A ResetAccountForm() 0 30 1
A doResetAccount() 0 62 4
A resetaccount() 0 55 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 BaseHandlerTrait;
32
    use Extensible;
33
34
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
35
        'GET reset-account' => 'resetaccount',
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)
44
    {
45
        if (Security::getCurrentUser()) {
46
            $output = $this->owner->renderWith(
47
                'Security',
48
                [
49
                    'Title' => _t(
50
                        __CLASS__ . '.ALREADYAUTHENTICATEDTITLE',
51
                        'Already authenticated'
52
                    ),
53
                    'Content' => _t(
54
                        __CLASS__ . '.ALREADYAUTHENTICATEDBODY',
55
                        'You must be logged out to reset your account.'
56
                    ),
57
                ]
58
            );
59
            return $this->owner->getResponse()->setBody($output)->setStatusCode(400);
60
        }
61
62
        $vars = $request->getVars();
63
64
        /** @var Member|MemberExtension $member */
65
        $member = Member::get()->byID(intval($vars['m'] ?? 0));
66
67
        if (is_null($member) || $member->verifyAccountResetToken($vars['t'] ?? '') === false) {
68
            $output = $this->owner->renderWith(
69
                'Security',
70
                [
71
                    'Title' => _t(
72
                        __CLASS__ . '.INVALIDTOKENTITLE',
73
                        'Invalid member or token'
74
                    ),
75
                    'Content' => _t(
76
                        __CLASS__ . '.INVALIDTOKENBODY',
77
                        'Your account reset token may have expired. Please contact an administrator.'
78
                    )
79
                ]
80
            );
81
            return $this->owner->getResponse()->setBody($output)->setStatusCode(400);
82
        }
83
84
        $request->getSession()->set('MemberID', $member->ID);
85
86
        return $this->owner->getResponse()->setBody($this->owner->renderWith(
87
            'Security',
88
            [
89
                'Title' => _t(
90
                    __CLASS__ . '.ACCOUNT_RESET_TITLE',
91
                    'Reset account'
92
                ),
93
                'Message' => _t(
94
                    __CLASS__ . '.ACCOUNT_RESET_DESCRIPTION',
95
                    'Your password will be changed, and any registered MFA methods will be removed.'
96
                ),
97
                'Form' => $this->ResetAccountForm(),
98
            ]
99
        ));
100
    }
101
102
    public function ResetAccountForm(): Form
103
    {
104
        $fields = FieldList::create([
105
            PasswordField::create(
106
                'NewPassword1',
107
                _t(
108
                    'SilverStripe\\Security\\Member.NEWPASSWORD',
109
                    'New password'
110
                )
111
            ),
112
            PasswordField::create(
113
                'NewPassword2',
114
                _t(
115
                    'SilverStripe\\Security\\Member.CONFIRMNEWPASSWORD',
116
                    'Confirm new password'
117
                )
118
            ),
119
        ]);
120
121
        $actions = FieldList::create([
122
            FormAction::create('doResetAccount', 'Reset account'),
123
        ]);
124
125
        $validation = RequiredFields::create(['NewPassword1', 'NewPassword2']);
126
127
        $form = Form::create($this->owner, 'ResetAccountForm', $fields, $actions, $validation);
128
129
        $this->owner->extend('updateResetAccountForm', $form);
130
131
        return $form;
132
    }
133
134
    /**
135
     * Resets the user's password, and triggers other account reset procedures
136
     *
137
     * @param array $data
138
     * @param Form $form
139
     * @return HTTPResponse
140
     */
141
    public function doResetAccount(array $data, Form $form): HTTPResponse
142
    {
143
        $memberID = $this->owner->getRequest()->getSession()->get('MemberID');
144
145
        // If the ID isn't in the session, politely assume the session has expired
146
        if (!$memberID) {
147
            $form->sessionMessage(
148
                _t(
149
                    __CLASS__ . '.RESETTIMEDOUT',
150
                    "The account reset process timed out. Please click the link in the email and try again."
151
                ),
152
                ValidationResult::TYPE_ERROR
153
            );
154
155
            return $this->owner->redirectBack();
156
        }
157
158
        /** @var Member&MemberExtension $member */
159
        $member = Member::get()->byID((int) $memberID);
160
161
        // Fail if passwords do not match
162
        if ($data['NewPassword1'] !== $data['NewPassword2']) {
163
            $form->sessionMessage(
164
                _t(
165
                    'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
166
                    'You have entered your new password differently, try again'
167
                ),
168
                ValidationResult::TYPE_ERROR
169
            );
170
171
            return $this->owner->redirectBack();
172
        }
173
174
        // Check if the new password is accepted
175
        $validationResult = $member->changePassword($data['NewPassword1']);
176
        if (!$validationResult->isValid()) {
177
            $form->setSessionValidationResult($validationResult);
178
179
            return $this->owner->redirectBack();
180
        }
181
182
        // Clear locked out status
183
        $member->LockedOutUntil = null;
184
        $member->FailedLoginCount = null;
185
186
        // Clear account reset data
187
        $member->AccountResetHash = null;
188
        $member->AccountResetExpired = DBDatetime::create()->now();
189
        $member->write();
190
191
        // Pass off to extensions to perform any additional reset actions
192
        $this->extend('handleAccountReset', $member);
193
194
        // Send the user along to the login form (allowing any additional factors to kick in as needed)
195
        $this->owner->setSessionMessage(
196
            _t(
197
                __CLASS__ . '.RESETSUCCESSMESSAGE',
198
                'Reset complete. Please log in with your new password.'
199
            ),
200
            ValidationResult::TYPE_GOOD
201
        );
202
        return $this->owner->redirect($this->owner->Link('login'));
203
    }
204
}
205