Issues (102)

src/Extension/AccountReset/SecurityExtension.php (2 issues)

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