Passed
Pull Request — master (#110)
by Robbie
02:00
created

ChangePasswordHandler::mfa()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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