ChangePasswordHandler::getSchema()   A
last analyzed

Complexity

Conditions 2
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 12
c 1
b 0
f 1
nc 4
nop 0
dl 0
loc 18
rs 9.8666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\MFA\Authenticator;
6
7
use LogicException;
8
use Psr\Log\LoggerInterface;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Control\HTTPResponse;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\MFA\Exception\InvalidMethodException;
13
use SilverStripe\MFA\Extension\MemberExtension;
14
use SilverStripe\MFA\RequestHandler\BaseHandlerTrait;
15
use SilverStripe\MFA\RequestHandler\VerificationHandlerTrait;
16
use SilverStripe\MFA\Service\MethodRegistry;
17
use SilverStripe\MFA\Service\SchemaGenerator;
18
use SilverStripe\MFA\Store\StoreInterface;
19
use SilverStripe\Security\Member;
20
use SilverStripe\Security\MemberAuthenticator\ChangePasswordHandler as BaseChangePasswordHandler;
21
use Throwable;
22
23
/**
24
 * Extends the "MemberAuthenticator version of the ChangePasswordHandler in order to allow MFA to be
25
 * inserted into the flow when an AutoLoginHash is being used  - that is when the user has clicked a
26
 * reset password link in an email after using the "forgot password" functionality.
27
 * When an "auto login" is not being used (a user is already logged in), it is existing functionality
28
 * to ask a user for their password before allowing a change - so this flow does not require MFA.
29
 */
30
class ChangePasswordHandler extends BaseChangePasswordHandler
31
{
32
    use BaseHandlerTrait;
33
    use VerificationHandlerTrait;
0 ignored issues
show
Bug introduced by
The trait SilverStripe\MFA\Request...erificationHandlerTrait requires the property $DefaultRegisteredMethod which is not provided by SilverStripe\MFA\Authent...r\ChangePasswordHandler.
Loading history...
34
35
    /**
36
     * Session key used to track whether multi-factor authentication has been verified during a change password
37
     * request flow.
38
     *
39
     * @var string
40
     */
41
    public const MFA_VERIFIED_ON_CHANGE_PASSWORD = 'MultiFactorAuthenticated';
42
43
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
44
        'GET mfa/schema' => 'getSchema', // Provides details about existing registered methods, etc.
45
        'GET mfa/login/$Method' => 'startMFACheck', // Initiates login process for $Method
46
        'POST mfa/login/$Method' => 'verifyMFACheck', // Verifies login via $Method
47
        'GET mfa' => 'mfa', // Renders the MFA Login Page to init the app
48
    ];
49
50
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
51
        'changepassword',
52
        'mfa',
53
        'getSchema',
54
        'startMFACheck',
55
        'verifyMFACheck',
56
    ];
57
58
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
59
        'Logger' => '%$' . LoggerInterface::class . '.mfa',
60
    ];
61
62
    /**
63
     * @var LoggerInterface
64
     */
65
    protected $logger;
66
67
    /**
68
     * Respond with the given array as a JSON response
69
     *
70
     * @param array $response
71
     * @param int $code The HTTP response code to set on the response
72
     * @return HTTPResponse
73
     */
74
    protected function jsonResponse(array $response, int $code = 200): HTTPResponse
75
    {
76
        return HTTPResponse::create(json_encode($response))
77
            ->addHeader('Content-Type', 'application/json')
78
            ->setStatusCode($code);
79
    }
80
81
    /**
82
     * Supply JavaScript application configuration details, required for an MFA check
83
     *
84
     * @return HTTPResponse
85
     */
86
    public function getSchema(): HTTPResponse
87
    {
88
        try {
89
            $member = $this->getStore()->getMember();
90
            $schema = SchemaGenerator::create()->getSchema($member);
91
            return $this->jsonResponse(
92
                array_merge($schema, [
93
                    'endpoints' => [
94
                        'verify' => $this->Link('mfa/login/{urlSegment}'),
95
                        'complete' => $this->Link(),
96
                    ],
97
                    'shouldRedirect' => false,
98
                ])
99
            );
100
        } catch (Throwable $exception) {
101
            $this->logger->debug($exception->getMessage());
102
            // If we don't have a valid member we shouldn't be here...
103
            return $this->redirectBack();
104
        }
105
    }
106
107
    /**
108
     * Render the JavaScript app responsible for initiating an MFA check
109
     *
110
     * @return HTTPResponse|array
111
     */
112
    public function mfa()
113
    {
114
        $store = $this->getStore();
115
        if (!$store || !$store->getMember()) {
0 ignored issues
show
introduced by
$store is of type SilverStripe\MFA\Store\StoreInterface, thus it always evaluated to true.
Loading history...
116
            return $this->redirectBack();
117
        }
118
119
        $this->applyRequirements();
120
121
        return [
122
            'Form' => $this->renderWith($this->getViewerTemplates()),
123
            'ClassName' => 'mfa',
124
        ];
125
    }
126
127
    /**
128
     * Initiates the session for the user attempting to log in, in preparation for an MFA check
129
     *
130
     * @param HTTPRequest $request
131
     * @return HTTPResponse
132
     * @throws LogicException when no store is available
133
     */
134
    public function startMFACheck(HTTPRequest $request): HTTPResponse
135
    {
136
        $store = $this->getStore();
137
        if (!$store) {
0 ignored issues
show
introduced by
$store is of type SilverStripe\MFA\Store\StoreInterface, thus it always evaluated to true.
Loading history...
138
            throw new LogicException('Store not found, please create one first.');
139
        }
140
        $member = $store->getMember();
141
142
        // If we don't have a valid member we shouldn't be here...
143
        if (!$member) {
144
            return $this->jsonResponse(['message' => 'Forbidden'], 403);
145
        }
146
147
        $method = $request->param('Method');
148
149
        if (empty($method)) {
150
            return $this->jsonResponse(['message' => 'Invalid request: method not present'], 400);
151
        }
152
153
        // Use the provided trait method for handling login
154
        $response = $this->createStartVerificationResponse(
155
            $store,
156
            Injector::inst()->get(MethodRegistry::class)->getMethodByURLSegment($method)
157
        );
158
159
        // Ensure detail is saved to the store
160
        $store->save($request);
161
162
        return $response;
163
    }
164
165
    /**
166
     * Checks the MFA JavaScript app input to validate the user attempting to log in
167
     *
168
     * @param HTTPRequest $request
169
     * @return HTTPResponse
170
     */
171
    public function verifyMFACheck(HTTPRequest $request): HTTPResponse
172
    {
173
        $store = $this->getStore();
174
175
        try {
176
            $result = $this->completeVerificationRequest($store, $request);
177
        } catch (InvalidMethodException $exception) {
178
            // Invalid method usually means a timeout. A user might be trying to verify before "starting"
179
            $this->logger->debug($exception->getMessage());
180
            return $this->jsonResponse(['message' => 'Forbidden'], 403);
181
        }
182
183
        if (!$result->isSuccessful()) {
184
            return $this->jsonResponse([
185
                'message' => $result->getMessage(),
186
            ], 401);
187
        }
188
189
        if (!$this->isVerificationComplete($store)) {
190
            return $this->jsonResponse([
191
                'message' => 'Additional authentication required',
192
            ], 202);
193
        }
194
195
        $request->getSession()->set(self::MFA_VERIFIED_ON_CHANGE_PASSWORD, true);
196
        $store->clear($request);
197
198
        return $this->jsonResponse([
199
            'message' => 'Multi-factor authenticated',
200
        ], 200);
201
    }
202
203
    public function changepassword()
204
    {
205
        $session = $this->getRequest()->getSession();
206
        $hash = $session->get('AutoLoginHash');
207
        /** @var Member&MemberExtension $member */
208
        $member = Member::member_from_autologinhash($hash);
209
210
        if (
211
            $hash
212
            && $member
213
            && $member->RegisteredMFAMethods()->exists()
214
            && !$session->get(self::MFA_VERIFIED_ON_CHANGE_PASSWORD)
215
        ) {
216
            Injector::inst()->create(StoreInterface::class, $member)->save($this->getRequest());
217
            return $this->mfa();
218
        }
219
220
        return parent::changepassword();
221
    }
222
223
    /**
224
     * @param LoggerInterface $logger
225
     * @return $this
226
     */
227
    public function setLogger(LoggerInterface $logger): ChangePasswordHandler
228
    {
229
        $this->logger = $logger;
230
        return $this;
231
    }
232
}
233