silverstripe / silverstripe-webauthn-authenticator
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;
30

31
    /**
32
     * Dependency injection configuration
33
     *
34
     * @config
35
     * @var array
36
     */
37
    private static $dependencies = [
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 2
    public function setLogger(?LoggerInterface $logger): self
53
    {
54 2
        $this->logger = $logger;
55 2
        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 2
    public function start(StoreInterface $store, RegisteredMethod $method): array
67
    {
68
        return [
69 2
            '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 2
    public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result
83
    {
84 2
        $data = json_decode((string) $request->getBody(), true);
85

86
        try {
87 2
            if (empty($data['credentials'])) {
88 2
                throw new ResponseDataException('Incomplete data, required information missing');
89
            }
90

91 2
            $decoder = $this->getDecoder();
92 2
            $attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder);
93 2
            $attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder);
94
            $publicKeyCredential = $this
95 2
                ->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder)
96 2
                ->load(base64_decode($data['credentials']));
97

98 2
            $response = $publicKeyCredential->getResponse();
99 2
            if (!$response instanceof AuthenticatorAssertionResponse) {
100 2
                throw new ResponseTypeException('Unexpected response type found');
101
            }
102

103
            // Create a PSR-7 request
104 2
            $psrRequest = ServerRequest::fromGlobals();
105

106 2
            $this->getAuthenticatorAssertionResponseValidator($decoder, $store)
107 2
                ->check(
108 2
                    $publicKeyCredential->getRawId(),
109
                    $response,
110 2
                    $this->getCredentialRequestOptions($store, $registeredMethod),
111
                    $psrRequest,
112 2
                    (string) $store->getMember()->ID
113
                );
114 2
        } catch (Exception $e) {
115 2
            $this->logger->error($e->getMessage());
116 2
            return Result::create(false, 'Verification failed: ' . $e->getMessage());
117
        }
118

119 2
        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 0
    public function getComponent(): string
128
    {
129 0
        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 2
    protected function getCredentialRequestOptions(
141
        StoreInterface $store,
142
        RegisteredMethod $registeredMethod = null,
143
        $reset = false
144
    ): PublicKeyCredentialRequestOptions {
145 2
        $state = $store->getState();
146

147 2
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
148 0
            return PublicKeyCredentialRequestOptions::createFromArray($state['credentialOptions']);
149
        }
150

151
        // Use the interface methods (despite the fact the "repository" is per-member in this module)
152 2
        $validCredentials = $this->getCredentialRepository($store, $registeredMethod)
153 2
            ->findAllForUserEntity($this->getUserEntity($store->getMember()));
154

155 2
        if (!count($validCredentials)) {
156 2
            throw new AuthenticationFailedException('User does not appear to have any credentials loaded for webauthn');
157
        }
158

159 2
        $descriptors = array_map(function (PublicKeyCredentialSource $source) {
160 2
            return $source->getPublicKeyCredentialDescriptor();
161 2
        }, $validCredentials);
162

163 2
        $options = new PublicKeyCredentialRequestOptions(
164 2
            random_bytes(32),
165 2
            40000,
166 2
            null,
167
            $descriptors,
168 2
            PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED
169
        );
170

171
        // Persist the options for later
172 2
        $store->addState(['credentialOptions' => $options]);
173

174 2
        return $options;
175
    }
176

177
    /**
178
     * @param Decoder $decoder
179
     * @param StoreInterface $store
180
     * @return AuthenticatorAssertionResponseValidator
181
     */
182 0
    protected function getAuthenticatorAssertionResponseValidator(
183
        Decoder $decoder,
184
        StoreInterface $store
185
    ): AuthenticatorAssertionResponseValidator {
186 0
        return new AuthenticatorAssertionResponseValidator(
187 0
            $this->getCredentialRepository($store),
188
            $decoder,
189 0
            new TokenBindingNotSupportedHandler(),
190 0
            new ExtensionOutputCheckerHandler()
191
        );
192
    }
193
}

Read our documentation on viewing source code .

Loading