Passed
Push — master ( f3ad4b...c4e72e )
by Robbie
05:06
created

src/VerifyHandler.php (2 issues)

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

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