BootstrapMFALoginHandler::doLogin()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 5
nop 3
dl 0
loc 30
rs 9.5222
c 0
b 0
f 0
1
<?php
2
3
namespace Firesphere\BootstrapMFA\Handlers;
4
5
use Firesphere\BootstrapMFA\Authenticators\BootstrapMFAAuthenticator;
6
use Firesphere\BootstrapMFA\Extensions\MemberExtension;
7
use Firesphere\BootstrapMFA\Forms\BootstrapMFALoginForm;
8
use InvalidArgumentException;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Control\HTTPResponse;
11
use SilverStripe\Control\Session;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Manifest\ClassLoader;
14
use SilverStripe\ORM\ArrayList;
15
use SilverStripe\ORM\ValidationResult;
16
use SilverStripe\Security\IdentityStore;
17
use SilverStripe\Security\Member;
18
use SilverStripe\Security\MemberAuthenticator\LoginHandler;
19
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
20
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
21
use SilverStripe\Security\Security;
22
use SilverStripe\Security\SecurityToken;
23
use SilverStripe\View\ArrayData;
24
25
/**
26
 * Class BootstrapMFALoginHandler
27
 * @package Firesphere\BootstrapMFA\Handlers
28
 */
29
class BootstrapMFALoginHandler extends LoginHandler
30
{
31
    const VERIFICATION_METHOD = 'validateMFA';
32
33
    /**
34
     * @var array
35
     */
36
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
37
        'verify' => 'secondFactor'
38
    ];
39
40
    /**
41
     * @var array
42
     */
43
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
44
        'LoginForm',
45
        'dologin',
46
        'secondFactor',
47
        'validateMFA',
48
    ];
49
50
    /**
51
     * Class names of descendants of BootstrapMFAAuthenticator
52
     *
53
     * @var string[]
54
     */
55
    protected $availableAuthenticators = [];
56
57
    /**
58
     * BootstrapMFALoginHandler constructor.
59
     * Sets up the available Authenticators
60
     * @param string $link
61
     * @param MemberAuthenticator $authenticator
62
     */
63
    public function __construct($link, MemberAuthenticator $authenticator)
64
    {
65
        $classManifest = ClassLoader::inst()->getManifest();
66
        $this->availableAuthenticators = $classManifest->getDescendantsOf(BootstrapMFAAuthenticator::class);
67
68
        parent::__construct($link, $authenticator);
69
    }
70
71
    /**
72
     * Return the MemberLoginForm form
73
     */
74
    public function LoginForm()
75
    {
76
        return BootstrapMFALoginForm::create(
77
            $this,
78
            get_class($this->authenticator),
79
            'LoginForm'
80
        );
81
    }
82
83
    /**
84
     * Override the doLogin method to do our own work here
85
     *
86
     * @param array $data
87
     * @param MemberLoginForm $form
88
     * @param HTTPRequest $request
89
     * @return HTTPResponse
90
     */
91
    public function doLogin($data, MemberLoginForm $form, HTTPRequest $request)
92
    {
93
        /**
94
         * @var ValidationResult $message
95
         * @var Member|MemberExtension $member
96
         */
97
        $member = $this->checkLogin($data, $request, $message);
98
99
        if (!$member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
100
            return $this->redirectBack();
101
        }
102
103
        // If we're in grace period, continue to the parent
104
        if ($member->isInGracePeriod()) {
105
            return parent::doLogin($data, $form, $request);
106
        }
107
108
        if ($message->isValid()) {
109
            /** @var Session $session */
110
            $session = $request->getSession();
111
            $session->set(BootstrapMFAAuthenticator::SESSION_KEY . '.MemberID', $member->ID);
112
            $session->set(BootstrapMFAAuthenticator::SESSION_KEY . '.Data', $data);
113
            if (!empty($data['BackURL'])) {
114
                $session->set(BootstrapMFAAuthenticator::SESSION_KEY . '.BackURL', $data['BackURL']);
115
            }
116
117
            return $this->redirect($this->link('verify'));
118
        }
119
120
        return $this->redirectBack();
121
    }
122
123
    /**
124
     * Render the second factor forms for displaying at the frontend
125
     *
126
     * @param HTTPRequest $request
127
     * @return array
128
     * @throws \Exception
129
     */
130
    public function secondFactor(HTTPRequest $request)
131
    {
132
        $memberID = $request->getSession()->get(BootstrapMFAAuthenticator::SESSION_KEY . '.MemberID');
133
        /** @var Member|MemberExtension $member */
134
        $member = Member::get()->byID($memberID);
135
136
        if (!$member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
137
            // Assume the session has gone state...
138
            return $this->redirectBack();
139
        }
140
141
        $primary = $member->PrimaryMFA;
142
        $formList = $this->getFormList();
143
144
        $view = ArrayData::create(['Forms' => ArrayList::create($formList)]);
145
        $rendered = [
146
            'Forms'   => $formList,
147
            'Form'    => $view->renderWith(self::class . '_MFAForms'),
148
            'Primary' => $primary
149
        ];
150
151
        $this->extend('onBeforeSecondFactor', $rendered, $view);
152
153
        return $rendered;
154
    }
155
156
    /**
157
     * Get all MFA forms from the enabled authenticators
158
     *
159
     * @return array
160
     */
161
    protected function getFormList()
162
    {
163
        $formList = [];
164
        foreach ($this->availableAuthenticators as $key => $className) {
165
            /** @var BootstrapMFAAuthenticator $class */
166
            $class = Injector::inst()->get($className);
167
            $formList[] = $class->getMFAForm($this, static::VERIFICATION_METHOD);
0 ignored issues
show
Bug introduced by
The method getMFAForm() does not exist on Firesphere\BootstrapMFA\...otstrapMFAAuthenticator. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

167
            /** @scrutinizer ignore-call */ 
168
            $formList[] = $class->getMFAForm($this, static::VERIFICATION_METHOD);
Loading history...
168
        }
169
170
        return $formList;
171
    }
172
173
    /**
174
     * @param HTTPRequest $request
175
     * @throws \InvalidArgumentException
176
     * @throws \Exception
177
     * @return HTTPResponse
178
     */
179
    public function validateMFA(HTTPRequest $request)
180
    {
181
        $postVars = $request->postVars();
182
        $this->validateFormData($request);
183
184
        $authenticationMethod = $postVars['AuthenticationMethod'];
185
        // Validate that the posted authentication method is a valid registered authenticator
186
        if (!$this->isValidAuthenticator($authenticationMethod)) {
187
            throw new InvalidArgumentException(
188
                sprintf('Unknown MFA authentication method "%s"', $authenticationMethod)
189
            );
190
        }
191
192
        /** @var BootstrapMFAAuthenticator $authenticator */
193
        $authenticator = Injector::inst()->get($authenticationMethod);
194
        $field = $authenticator->getTokenField();
195
196
        /** @var ValidationResult $result */
197
        $result = ValidationResult::create();
198
199
        /** @var Member $member */
200
        $member = $authenticator->verifyMFA($postVars, $request, $postVars[$field], $result);
0 ignored issues
show
Bug introduced by
The method verifyMFA() does not exist on Firesphere\BootstrapMFA\...otstrapMFAAuthenticator. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

200
        /** @scrutinizer ignore-call */ 
201
        $member = $authenticator->verifyMFA($postVars, $request, $postVars[$field], $result);
Loading history...
201
        // Manually login
202
        if ($member && $result->isValid()) {
203
            $data = $request->getSession()->get(BootstrapMFAAuthenticator::SESSION_KEY . '.Data');
204
            $backURL = $request->getSession()->get('BackURL'); // defaults to null, so it's fine
205
            $this->performLogin($member, $data, $request);
206
            // Redirecting after successful login expects a getVar to be set
207
            $request->offsetSet('BackURL', $backURL);
208
209
            return $this->redirectAfterSuccessfulLogin();
210
        }
211
212
        // Failure of login, trash session and redirect back
213
        $this->cancelLogin($request);
214
215
        BootstrapMFALoginForm::create($this, BootstrapMFAAuthenticator::class, 'LoginForm')->sessionMessage(
216
            _t(
217
                self::class . 'MFAFAILURE',
218
                'Multi Factor failure'
219
            )
220
        );
221
222
        return $this->redirect(Security::login_url());
223
    }
224
225
    /**
226
     * @param HTTPRequest $request
227
     * @throws \Exception
228
     */
229
    protected function validateFormData(HTTPRequest $request)
230
    {
231
        /** @var SecurityToken $securityToken */
232
        $securityToken = Injector::inst()->get(SecurityToken::class);
233
        $tokenCheck = $securityToken->check($request->postVar('SecurityID'));
234
235
        $authenticationMethod = $request->postVar('AuthenticationMethod');
236
        if (!$tokenCheck || !$this->isValidAuthenticator($authenticationMethod)) {
237
            // Failure of login, trash session and redirect back
238
            $this->cancelLogin($request);
239
            // User tampered with the authentication method input. Thus invalidate
240
            throw new \Exception('Invalid authentication', 1);
241
        }
242
    }
243
244
245
    /**
246
     * @param $authenticationMethod
247
     * @return bool
248
     */
249
    protected function isValidAuthenticator($authenticationMethod)
250
    {
251
        return in_array($authenticationMethod, $this->availableAuthenticators, true);
252
    }
253
254
    /**
255
     * @param HTTPRequest $request
256
     */
257
    protected function cancelLogin(HTTPRequest $request)
258
    {
259
        $request->getSession()->clear(BootstrapMFAAuthenticator::SESSION_KEY);
260
        Injector::inst()->get(IdentityStore::class)->logOut();
261
    }
262
}
263