Completed
Push — master ( 81d457...c620f4 )
by Garion
12s 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 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 login process with a method, and returns relevant state to be applied to the
54
     * 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
     * Provide a localised string that serves as a lead in for choosing this option for authentication
118
     *
119
     * eg. "Enter one of your recovery codes"
120
     *
121
     * @return string
122
     */
123
    public function getLeadInLabel(): string
124
    {
125
        return _t(__CLASS__ . '.LEAD_IN', 'Verify with security key');
126
    }
127
128
    /**
129
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
130
     *
131
     * @return string
132
     */
133
    public function getComponent(): string
134
    {
135
        return 'WebAuthnVerify';
136
    }
137
138
    /**
139
     * @param StoreInterface $store
140
     * @param RegisteredMethod $registeredMethod
141
     * @param bool $reset
142
     * @return PublicKeyCredentialRequestOptions
143
     * @throws Exception
144
     */
145
    protected function getCredentialRequestOptions(
146
        StoreInterface $store,
147
        RegisteredMethod $registeredMethod,
148
        $reset = false
149
    ): PublicKeyCredentialRequestOptions {
150
        $state = $store->getState();
151
152
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
153
            return PublicKeyCredentialRequestOptions::createFromArray($state['credentialOptions']);
154
        }
155
156
        $data = json_decode((string) $registeredMethod->Data, true) ?? [];
157
        $descriptor = PublicKeyCredentialDescriptor::createFromArray($data['descriptor'] ?? []);
158
159
        $options = new PublicKeyCredentialRequestOptions(
160
            random_bytes(32),
161
            40000,
162
            null,
163
            [$descriptor],
164
            PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED
165
        );
166
167
        $state['credentialOptions'] = $options;
168
        $store->setState($state);
169
170
        return $options;
171
    }
172
173
    /**
174
     * @param Decoder $decoder
175
     * @param StoreInterface $store
176
     * @param RegisteredMethod $registeredMethod
177
     * @return AuthenticatorAssertionResponseValidator
178
     */
179
    protected function getAuthenticatorAssertionResponseValidator(
180
        Decoder $decoder,
181
        StoreInterface $store,
182
        RegisteredMethod $registeredMethod
183
    ): AuthenticatorAssertionResponseValidator {
184
        $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

184
        $credentialRepository = new CredentialRepository(/** @scrutinizer ignore-type */ $store->getMember(), $registeredMethod);
Loading history...
185
186
        return new AuthenticatorAssertionResponseValidator(
187
            $credentialRepository,
188
            $decoder,
189
            new TokenBindingNotSupportedHandler(),
190
            new ExtensionOutputCheckerHandler()
191
        );
192
    }
193
}
194