VerifyHandler   A
last analyzed

Complexity

Total Complexity 13

Size/Duplication

Total Lines 165
Duplicated Lines 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
eloc 59
c 7
b 1
f 0
dl 0
loc 165
rs 10
wmc 13

6 Methods

Rating   Name   Duplication   Size   Complexity  
A verify() 0 38 4
A getAuthenticatorAssertionResponseValidator() 0 9 1
A getCredentialRequestOptions() 0 35 5
A setLogger() 0 4 1
A getComponent() 0 3 1
A start() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\WebAuthn;
6
7
use CBOR\Decoder;
8
use Exception;
9
use GuzzleHttp\Psr7\ServerRequest;
10
use InvalidArgumentException;
11
use Psr\Log\LoggerInterface;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\MFA\Exception\AuthenticationFailedException;
14
use SilverStripe\MFA\Method\Handler\VerifyHandlerInterface;
15
use SilverStripe\MFA\Model\RegisteredMethod;
16
use SilverStripe\MFA\State\Result;
17
use SilverStripe\MFA\Store\StoreInterface;
18
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
19
use Webauthn\AuthenticatorAssertionResponse;
20
use Webauthn\AuthenticatorAssertionResponseValidator;
21
use Webauthn\PublicKeyCredentialDescriptor;
22
use Webauthn\PublicKeyCredentialRequestOptions;
23
use Webauthn\PublicKeyCredentialSource;
24
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
25
26
class VerifyHandler implements VerifyHandlerInterface
27
{
28
    use BaseHandlerTrait;
29
    use CredentialRepositoryProviderTrait;
0 ignored issues
show
introduced by
The trait SilverStripe\WebAuthn\Cr...RepositoryProviderTrait requires some properties which are not provided by SilverStripe\WebAuthn\VerifyHandler: $Data, $ID
Loading history...
30
31
    /**
32
     * Dependency injection configuration
33
     *
34
     * @config
35
     * @var array
36
     */
37
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
38
        'Logger' => '%$' . LoggerInterface::class . '.mfa',
39
    ];
40
41
    /**
42
     * @var LoggerInterface
43
     */
44
    protected $logger;
45
46
    /**
47
     * Sets the {@see $logger} member variable
48
     *
49
     * @param LoggerInterface|null $logger
50
     * @return self
51
     */
52
    public function setLogger(?LoggerInterface $logger): self
53
    {
54
        $this->logger = $logger;
55
        return $this;
56
    }
57
58
    /**
59
     * Stores any data required to handle a log in process with a method, and returns relevant state to be applied to
60
     * the front-end application managing the process.
61
     *
62
     * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated
63
     * @param RegisteredMethod $method The RegisteredMethod instance that is being verified
64
     * @return array Props to be passed to a front-end component
65
     */
66
    public function start(StoreInterface $store, RegisteredMethod $method): array
67
    {
68
        return [
69
            'publicKey' => $this->getCredentialRequestOptions($store, $method, true),
70
        ];
71
    }
72
73
    /**
74
     * Verify the request has provided the right information to verify the member that aligns with any sessions state
75
     * that may have been set prior
76
     *
77
     * @param HTTPRequest $request
78
     * @param StoreInterface $store
79
     * @param RegisteredMethod $registeredMethod The RegisteredMethod instance that is being verified
80
     * @return Result
81
     */
82
    public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result
83
    {
84
        $data = json_decode((string) $request->getBody(), true);
85
86
        try {
87
            if (empty($data['credentials'])) {
88
                throw new ResponseDataException('Incomplete data, required information missing');
89
            }
90
91
            $decoder = $this->getDecoder();
92
            $attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder);
93
            $attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder);
94
            $publicKeyCredential = $this
95
                ->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder)
96
                ->load(base64_decode($data['credentials']));
97
98
            $response = $publicKeyCredential->getResponse();
99
            if (!$response instanceof AuthenticatorAssertionResponse) {
100
                throw new ResponseTypeException('Unexpected response type found');
101
            }
102
103
            // Create a PSR-7 request
104
            $psrRequest = ServerRequest::fromGlobals();
105
106
            $this->getAuthenticatorAssertionResponseValidator($decoder, $store)
107
                ->check(
108
                    $publicKeyCredential->getRawId(),
109
                    $response,
110
                    $this->getCredentialRequestOptions($store, $registeredMethod),
111
                    $psrRequest,
112
                    (string) $store->getMember()->ID
113
                );
114
        } catch (Exception $e) {
115
            $this->logger->error($e->getMessage());
116
            return Result::create(false, 'Verification failed: ' . $e->getMessage());
117
        }
118
119
        return Result::create();
120
    }
121
122
    /**
123
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
124
     *
125
     * @return string
126
     */
127
    public function getComponent(): string
128
    {
129
        return 'WebAuthnVerify';
130
    }
131
132
    /**
133
     * @param StoreInterface $store
134
     * @param RegisteredMethod|null $registeredMethod
135
     * @param bool $reset
136
     * @return PublicKeyCredentialRequestOptions
137
     * @throws AuthenticationFailedException
138
     * @throws Exception
139
     */
140
    protected function getCredentialRequestOptions(
141
        StoreInterface $store,
142
        RegisteredMethod $registeredMethod = null,
143
        $reset = false
144
    ): PublicKeyCredentialRequestOptions {
145
        $state = $store->getState();
146
147
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
148
            return PublicKeyCredentialRequestOptions::createFromArray($state['credentialOptions']);
149
        }
150
151
        // Use the interface methods (despite the fact the "repository" is per-member in this module)
152
        $validCredentials = $this->getCredentialRepository($store, $registeredMethod)
153
            ->findAllForUserEntity($this->getUserEntity($store->getMember()));
0 ignored issues
show
Bug introduced by
It seems like $store->getMember() can also be of type null; however, parameter $member of SilverStripe\WebAuthn\Ve...andler::getUserEntity() does only seem to accept SilverStripe\Security\Member, 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

153
            ->findAllForUserEntity($this->getUserEntity(/** @scrutinizer ignore-type */ $store->getMember()));
Loading history...
154
155
        if (!count($validCredentials)) {
156
            throw new AuthenticationFailedException('User does not appear to have any credentials loaded for webauthn');
157
        }
158
159
        $descriptors = array_map(function (PublicKeyCredentialSource $source) {
160
            return $source->getPublicKeyCredentialDescriptor();
161
        }, $validCredentials);
162
163
        $options = new PublicKeyCredentialRequestOptions(
164
            random_bytes(32),
165
            40000,
166
            null,
167
            $descriptors,
168
            PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED
169
        );
170
171
        // Persist the options for later
172
        $store->addState(['credentialOptions' => $options]);
173
174
        return $options;
175
    }
176
177
    /**
178
     * @param Decoder $decoder
179
     * @param StoreInterface $store
180
     * @return AuthenticatorAssertionResponseValidator
181
     */
182
    protected function getAuthenticatorAssertionResponseValidator(
183
        Decoder $decoder,
184
        StoreInterface $store
185
    ): AuthenticatorAssertionResponseValidator {
186
        return new AuthenticatorAssertionResponseValidator(
187
            $this->getCredentialRepository($store),
188
            $decoder,
189
            new TokenBindingNotSupportedHandler(),
190
            new ExtensionOutputCheckerHandler()
191
        );
192
    }
193
}
194