Passed
Push — master ( 4f3e79...0521ba )
by Robbie
05:56 queued 11s
created

VerifyHandler::getComponent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

172
        $credentialRepository = new CredentialRepository(/** @scrutinizer ignore-type */ $store->getMember(), $registeredMethod);
Loading history...
173
174
        return new AuthenticatorAssertionResponseValidator(
175
            $credentialRepository,
176
            $decoder,
177
            new TokenBindingNotSupportedHandler(),
178
            new ExtensionOutputCheckerHandler()
179
        );
180
    }
181
}
182