Completed
Push — master ( c14fb2...0f9e03 )
by Robbie
22s queued 11s
created

createStartVerificationResponse()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 47
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 47
rs 8.9457
c 0
b 0
f 0
cc 6
nc 12
nop 2
1
<?php
2
3
namespace SilverStripe\MFA\RequestHandler;
4
5
use SilverStripe\Control\HTTPRequest;
6
use SilverStripe\Control\HTTPResponse;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\MFA\Exception\InvalidMethodException;
9
use SilverStripe\MFA\Method\MethodInterface;
10
use SilverStripe\MFA\Service\EnforcementManager;
11
use SilverStripe\MFA\Service\MethodRegistry;
12
use SilverStripe\MFA\Service\RegisteredMethodManager;
13
use SilverStripe\MFA\State\Result;
14
use SilverStripe\MFA\Store\StoreInterface;
15
use SilverStripe\Security\SecurityToken;
16
17
/**
18
 * This trait encapsulates logic that can be added to a `RequestHandler` to work with logging in using MFA front-end
19
 * app. It provides two main methods; @see createStartVerificationResponse - a response that can be easily consumed by
20
 * the MFA app to prompt a login, and @see completeVerificationRequest - used to verify a request sent by the MFA app
21
 * containing the login attempt.
22
 */
23
trait VerificationHandlerTrait
24
{
25
    /**
26
     * Create an HTTPResponse that provides information to the client side React MFA app to prompt the user to login
27
     * with their configured MFA method
28
     *
29
     * @param StoreInterface $store
30
     * @param MethodInterface|null $requestedMethod
31
     * @return HTTPResponse
32
     */
33
    protected function createStartVerificationResponse(
34
        StoreInterface $store,
35
        ?MethodInterface $requestedMethod = null
36
    ): HTTPResponse {
37
        $registeredMethod = null;
38
        $member = $store->getMember();
39
40
        // Use a requested method if provided
41
        if ($requestedMethod) {
42
            $registeredMethod = RegisteredMethodManager::singleton()->getFromMember($member, $requestedMethod);
43
        }
44
45
        // ...Or use the default (TODO: Should we have the default as a fallback? Maybe just if no method is specified?)
46
        if (!$registeredMethod) {
47
            $registeredMethod = $member->DefaultRegisteredMethod;
48
        }
49
50
        $response = HTTPResponse::create()
51
            ->addHeader('Content-Type', 'application/json');
52
53
        // We can't proceed with login if the Member doesn't have this method registered
54
        if (!$registeredMethod) {
55
            // We can display a specific message if there was no method specified
56
            if (!$requestedMethod) {
57
                $message = _t(
58
                    __CLASS__ . '.METHOD_NOT_PROVIDED',
59
                    'No method was provided to login with and the Member has no default'
60
                );
61
            } else {
62
                $message = _t(__CLASS__ . '.METHOD_NOT_REGISTERED', 'Member does not have this method registered');
63
            }
64
65
            return $response->setBody(json_encode(['errors' => [$message]]))->setStatusCode(400);
66
        }
67
68
        // Mark the given method as started within the store
69
        $store->setMethod($registeredMethod->getMethod()->getURLSegment());
70
        // Allow the authenticator to begin the process and generate some data to pass through to the front end
71
        $data = $registeredMethod->getVerifyHandler()->start($store, $registeredMethod) ?: [];
72
73
        // Add a CSRF token
74
        $token = SecurityToken::inst();
75
        $token->reset();
76
        $data[$token->getName()] = $token->getValue();
77
78
        // Respond with our method
79
        return $response->setBody(json_encode($data));
80
    }
81
82
    /**
83
     * Attempt to verify a login attempt provided by the given request
84
     *
85
     * @param StoreInterface $store
86
     * @param HTTPRequest $request
87
     * @return Result
88
     * @throws InvalidMethodException
89
     */
90
    protected function completeVerificationRequest(StoreInterface $store, HTTPRequest $request): Result
91
    {
92
        if (!SecurityToken::inst()->checkRequest($request)) {
93
            return Result::create(false, _t(
94
                __CLASS__ . '.CSRF_FAILURE',
95
                'Your request timed out. Please refresh and try again'
96
            ), ['code' => 403]);
97
        }
98
99
        $method = $store->getMethod();
100
        $methodInstance = $method ? MethodRegistry::singleton()->getMethodByURLSegment($method) : null;
101
102
        // The method must be tracked in session. If it's missing we can't continue
103
        if (!$methodInstance) {
104
            throw new InvalidMethodException('There is no method tracked in a store for this request');
105
        }
106
107
        // Get the member and authenticator ready
108
        $member = $store->getMember();
109
        $registeredMethod = RegisteredMethodManager::singleton()->getFromMember($member, $methodInstance);
110
        $authenticator = $registeredMethod->getVerifyHandler();
111
112
        $result = $authenticator->verify($request, $store, $registeredMethod);
113
        if ($result->isSuccessful()) {
114
            $store->addVerifiedMethod($method);
0 ignored issues
show
Bug introduced by
It seems like $method can also be of type null; however, parameter $method of SilverStripe\MFA\Store\S...ce::addVerifiedMethod() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

114
            $store->addVerifiedMethod(/** @scrutinizer ignore-type */ $method);
Loading history...
115
            $store->save($request);
116
            $this->extend('onMethodVerificationSuccess', $member, $methodInstance);
0 ignored issues
show
Bug introduced by
It seems like extend() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

116
            $this->/** @scrutinizer ignore-call */ 
117
                   extend('onMethodVerificationSuccess', $member, $methodInstance);
Loading history...
117
            return $result;
118
        }
119
120
        $this->extend('onMethodVerificationFailure', $member, $methodInstance);
121
        return $result;
122
    }
123
124
    /**
125
     * Indicates the current member has verified with MFA methods enough to be considered "verified"
126
     *
127
     * @param StoreInterface $store
128
     * @return bool
129
     */
130
    protected function isVerificationComplete(StoreInterface $store): bool
131
    {
132
        // Pull the successful methods from session
133
        $successfulMethods = $store->getVerifiedMethods();
134
135
        // Zero is "not complete". There's different config for optional MFA
136
        if (!is_array($successfulMethods) || !count($successfulMethods)) {
0 ignored issues
show
introduced by
The condition is_array($successfulMethods) is always true.
Loading history...
137
            return false;
138
        }
139
140
        return count($successfulMethods) >= Config::inst()->get(EnforcementManager::class, 'required_mfa_methods');
141
    }
142
}
143