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

ChangePasswordHandler::getSchema()   A

Complexity

Conditions 2
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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