Completed
Push — master ( a3802c...bae652 )
by Robbie
24s queued 11s
created

VerifyHandler   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 111
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 9
eloc 30
dl 0
loc 111
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A verifyCode() 0 3 1
A verify() 0 33 4
A getComponent() 0 3 1
A start() 0 6 1
A setNotificationService() 0 4 1
A getLeadInLabel() 0 3 1
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\BackupCode;
4
5
use RuntimeException;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Core\Manifest\ModuleLoader;
8
use SilverStripe\MFA\Method\Handler\VerifyHandlerInterface;
9
use SilverStripe\MFA\Model\RegisteredMethod;
10
use SilverStripe\MFA\Service\Notification;
11
use SilverStripe\MFA\State\Result;
12
use SilverStripe\MFA\Store\StoreInterface;
13
14
class VerifyHandler implements VerifyHandlerInterface
15
{
16
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
17
        'NotificationService' => '%$' . Notification::class
18
    ];
19
20
    /**
21
     * @var Notification
22
     */
23
    protected $notification;
24
25
    public function setNotificationService(Notification $notification): self
26
    {
27
        $this->notification = $notification;
28
        return $this;
29
    }
30
31
    /**
32
     * Stores any data required to handle a login process with a method, and returns relevant state to be applied to the
33
     * front-end application managing the process.
34
     *
35
     * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated
36
     * @param RegisteredMethod $method The RegisteredMethod instance that is being verified
37
     * @return array Props to be passed to a front-end component
38
     */
39
    public function start(StoreInterface $store, RegisteredMethod $method): array
40
    {
41
        // Provide a path to the graphic shown
42
        return [
43
            'graphic' => ModuleLoader::getModule('silverstripe/mfa')
44
                ->getResource('client/dist/images/recovery-codes.svg')->getURL(),
45
        ];
46
    }
47
48
    /**
49
     * Verify the request has provided the right information to verify the member that aligns with any sessions state
50
     * that may have been set prior
51
     *
52
     * @param HTTPRequest $request
53
     * @param StoreInterface $store
54
     * @param RegisteredMethod $registeredMethod The RegisteredMethod instance that is being verified
55
     * @return Result
56
     */
57
    public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result
58
    {
59
        $bodyJSON = json_decode($request->getBody(), true);
60
61
        if (!isset($bodyJSON['code'])) {
62
            throw new RuntimeException(
63
                'Verification of backup codes requires the code to be provided but it was not given'
64
            );
65
        }
66
67
        $code = $bodyJSON['code'];
68
69
        $candidates = json_decode($registeredMethod->Data, true);
70
71
        foreach ($candidates as $index => $candidate) {
72
            if ($this->verifyCode($code, $candidate)) {
73
                // Remove the verified code from the valid list of codes
74
                array_splice($candidates, $index, 1);
75
                $registeredMethod->Data = json_encode($candidates);
76
                $registeredMethod->write();
77
                $this->notification->send(
78
                    $registeredMethod->Member(),
0 ignored issues
show
Bug introduced by
The method Member() does not exist on SilverStripe\MFA\Model\RegisteredMethod. 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

78
                    $registeredMethod->/** @scrutinizer ignore-call */ 
79
                                       Member(),
Loading history...
79
                    'Email/MFA/Notification_backupcodeused',
80
                    [
81
                        'subject' => _t(self::class . '.MFAREMOVED', 'A recovery code was used to access your account'),
82
                        'CodesRemaining' => count($candidates),
83
                    ]
84
                );
85
                return Result::create();
86
            }
87
        }
88
89
        return Result::create(false, _t(__CLASS__ . '.INVALID_CODE', 'Invalid code'));
90
    }
91
92
    /**
93
     * Provide a localised string that serves as a lead in for choosing this option for authentication
94
     *
95
     * eg. "Enter one of your recovery codes"
96
     *
97
     * @return string
98
     */
99
    public function getLeadInLabel(): string
100
    {
101
        return _t(__CLASS__ . '.LEAD_IN', 'Verify with recovery code');
102
    }
103
104
    /**
105
     * Verifies the given code (user input) against the given hash. This uses the PHP password_hash API by default but
106
     * can be extended to handle a custom hash implementation
107
     *
108
     * @param string $code
109
     * @param string $hash
110
     * @return bool
111
     */
112
    protected function verifyCode($code, $hash): bool
113
    {
114
        return password_verify($code, $hash);
115
    }
116
117
    /**
118
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
119
     *
120
     * @return string
121
     */
122
    public function getComponent(): string
123
    {
124
        return 'BackupCodeVerify';
125
    }
126
}
127