WebAuthnServer::finishRegistration()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 55
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.1406

Importance

Changes 9
Bugs 1 Features 1
Metric Value
cc 3
eloc 12
c 9
b 1
f 1
nc 3
nop 2
dl 0
loc 55
ccs 9
cts 12
cp 0.75
crap 3.1406
rs 9.8666

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace MadWizard\WebAuthn\Server;
4
5
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatRegistryInterface;
6
use MadWizard\WebAuthn\Config\RelyingPartyInterface;
7
use MadWizard\WebAuthn\Credential\CredentialId;
8
use MadWizard\WebAuthn\Credential\CredentialStoreInterface;
9
use MadWizard\WebAuthn\Credential\UserHandle;
10
use MadWizard\WebAuthn\Dom\AuthenticationExtensionsClientInputs;
11
use MadWizard\WebAuthn\Dom\AuthenticatorSelectionCriteria;
12
use MadWizard\WebAuthn\Dom\PublicKeyCredentialCreationOptions;
13
use MadWizard\WebAuthn\Dom\PublicKeyCredentialDescriptor;
14
use MadWizard\WebAuthn\Dom\PublicKeyCredentialInterface;
15
use MadWizard\WebAuthn\Dom\PublicKeyCredentialParameters;
16
use MadWizard\WebAuthn\Dom\PublicKeyCredentialRequestOptions;
17
use MadWizard\WebAuthn\Dom\PublicKeyCredentialRpEntity;
18
use MadWizard\WebAuthn\Dom\PublicKeyCredentialUserEntity;
19
use MadWizard\WebAuthn\Dom\ResidentKeyRequirement;
20
use MadWizard\WebAuthn\Dom\UserVerificationRequirement;
21
use MadWizard\WebAuthn\Exception\CredentialIdExistsException;
22
use MadWizard\WebAuthn\Exception\NoCredentialsException;
23
use MadWizard\WebAuthn\Exception\UntrustedException;
24
use MadWizard\WebAuthn\Exception\VerificationException;
25
use MadWizard\WebAuthn\Exception\WebAuthnException;
26
use MadWizard\WebAuthn\Extension\ExtensionRegistryInterface;
27
use MadWizard\WebAuthn\Format\ByteBuffer;
28
use MadWizard\WebAuthn\Metadata\MetadataResolverInterface;
29
use MadWizard\WebAuthn\Policy\PolicyInterface;
30
use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManagerInterface;
31
use MadWizard\WebAuthn\Server\Authentication\AuthenticationContext;
32
use MadWizard\WebAuthn\Server\Authentication\AuthenticationOptions;
33
use MadWizard\WebAuthn\Server\Authentication\AuthenticationRequest;
34
use MadWizard\WebAuthn\Server\Authentication\AuthenticationResultInterface;
35
use MadWizard\WebAuthn\Server\Authentication\AuthenticationVerifier;
36
use MadWizard\WebAuthn\Server\Registration\RegistrationContext;
37
use MadWizard\WebAuthn\Server\Registration\RegistrationOptions;
38
use MadWizard\WebAuthn\Server\Registration\RegistrationRequest;
39
use MadWizard\WebAuthn\Server\Registration\RegistrationResultInterface;
40
use MadWizard\WebAuthn\Server\Registration\RegistrationVerifier;
41
use Psr\Log\LoggerAwareInterface;
42
use Psr\Log\LoggerAwareTrait;
43
use Psr\Log\NullLogger;
44
45
final class WebAuthnServer implements ServerInterface, LoggerAwareInterface
46
{
47
    use LoggerAwareTrait;
48
49
    /**
50
     * @var RelyingPartyInterface
51
     */
52
    private $relyingParty;
53
54
    /**
55
     * @var PolicyInterface
56
     */
57
    private $policy;
58
59
    /**
60
     * @var CredentialStoreInterface
61
     */
62
    private $credentialStore;
63
64
    /**
65
     * @var AttestationFormatRegistryInterface
66
     */
67
    private $formatRegistry;
68
69
    /**
70
     * @var MetadataResolverInterface
71
     */
72
    private $metadataResolver;
73
74
    /**
75
     * @var TrustDecisionManagerInterface
76
     */
77
    private $trustDecisionManager;
78
79
    /**
80
     * @var ExtensionRegistryInterface
81
     */
82
    private $extensionRegistry;
83
84 18
    public function __construct(
85
        RelyingPartyInterface $relyingParty,
86
        PolicyInterface $policy,
87
        CredentialStoreInterface $credentialStore,
88
        AttestationFormatRegistryInterface $formatRegistry,
89
        MetadataResolverInterface $metadataResolver,
90
        TrustDecisionManagerInterface $trustDecisionManager,
91
        ExtensionRegistryInterface $extensionRegistry
92
    ) {
93 18
        $this->relyingParty = $relyingParty;
94 18
        $this->policy = $policy;
95 18
        $this->credentialStore = $credentialStore;
96 18
        $this->formatRegistry = $formatRegistry;
97 18
        $this->metadataResolver = $metadataResolver;
98 18
        $this->trustDecisionManager = $trustDecisionManager;
99 18
        $this->extensionRegistry = $extensionRegistry;
100 18
        $this->logger = new NullLogger();
101 18
    }
102
103 1
    public function startRegistration(RegistrationOptions $options): RegistrationRequest
104
    {
105 1
        $challenge = $this->createChallenge();
106
107 1
        $creationOptions = new PublicKeyCredentialCreationOptions(
108 1
            PublicKeyCredentialRpEntity::fromRelyingParty($this->relyingParty),
109 1
            $this->createUserEntity($options->getUser()),
110
            $challenge,
111 1
            $this->getCredentialParameters()
112
        );
113
114 1
        $creationOptions->setAttestation($options->getAttestation());
115 1
        $creationOptions->setTimeout($options->getTimeout());
116
117 1
        $selection = $this->createAuthenticatorSelection($options);
118
119 1
        $creationOptions->setAuthenticatorSelection($selection);
120 1
        $extensions = $options->getExtensionInputs();
121 1
        if (count($extensions) > 0) {
122
            $creationOptions->setExtensions(
123
                AuthenticationExtensionsClientInputs::fromArray($extensions)
124
            );
125
        }
126
127 1
        if ($options->getExcludeExistingCredentials()) {
128
            $credentialIds = $this->credentialStore->getUserCredentialIds($options->getUser()->getUserHandle());
129
            foreach ($credentialIds as $credential) {
130
                $creationOptions->addExcludeCredential(
131
                    new PublicKeyCredentialDescriptor($credential->toBuffer())
132
                );
133
            }
134
        }
135
136 1
        $context = $this->createRegistrationContext($options, $creationOptions);
137 1
        return new RegistrationRequest($creationOptions, $context);
138
    }
139
140 1
    private function createAuthenticatorSelection(RegistrationOptions $options): ?AuthenticatorSelectionCriteria
141
    {
142 1
        $criteria = null;
143 1
        $attachment = $options->getAuthenticatorAttachment();
144 1
        if ($attachment !== null) {
145
            $criteria = new AuthenticatorSelectionCriteria();
146
            $criteria->setAuthenticatorAttachment($attachment);
147
        }
148
149 1
        $userVerification = $options->getUserVerification();
150 1
        if ($userVerification !== null) {
151
            $criteria = $criteria ?? new AuthenticatorSelectionCriteria();
152
            $criteria->setUserVerification($userVerification);
153
        }
154
155 1
        $residentKey = $options->getResidentKey();
156 1
        if ($residentKey !== null) {
157
            $criteria = $criteria ?? new AuthenticatorSelectionCriteria();
158
            $criteria->setRequireResidentKey($residentKey === ResidentKeyRequirement::REQUIRED);
159
        }
160 1
        return $criteria;
161
    }
162
163 1
    private function createRegistrationContext(RegistrationOptions $regOptions, PublicKeyCredentialCreationOptions $options): RegistrationContext
164
    {
165 1
        $origin = $this->relyingParty->getOrigin();
166 1
        $rpId = $this->relyingParty->getEffectiveId();
167
168
        // TODO: mismatch $rp and rp in $options? Check?
169 1
        $context = new RegistrationContext($options->getChallenge(), $origin, $rpId, UserHandle::fromBuffer($options->getUserEntity()->getId()));
170
171 1
        $context->setUserPresenceRequired($this->policy->isUserPresenceRequired());
172
173 1
        if ($regOptions->getUserVerification() === UserVerificationRequirement::REQUIRED) {
174
            $context->setUserVerificationRequired(true);
175
        }
176
177 1
        foreach ($regOptions->getExtensionInputs() as $input) {
178
            $context->addExtensionInput($input);
179
        }
180 1
        return $context;
181
    }
182
183
    /**
184
     * @param PublicKeyCredentialInterface $credential Attestation credential response from the client
185
     *
186
     * @throws CredentialIdExistsException
187
     * @throws VerificationException
188
     */
189 1
    public function finishRegistration(PublicKeyCredentialInterface $credential, RegistrationContext $context): RegistrationResultInterface
190
    {
191 1
        $verifier = new RegistrationVerifier($this->formatRegistry, $this->extensionRegistry);
192 1
        $registrationResult = $verifier->verify($credential, $context);
193
194 1
        $response = $credential->getResponse()->asAttestationResponse();
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
195
196
        // 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or
197
        //     ECDAA-Issuer public keys) for that attestation type and attestation statement format fmt, from a trusted
198
        //     source or from policy.
199
200 1
        $metadata = $this->metadataResolver->getMetadata($registrationResult);
201 1
        $registrationResult = $registrationResult->withMetadata($metadata);
202
203
        // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14,
204
        //     as follows:
205
        //       If self attestation was used, check if self attestation is acceptable under Relying Party policy.
206
        //       If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in the
207
        //       set of acceptable trust anchors obtained in step 15.
208
        //       Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
209
        //       attestation public key correctly chains up to an acceptable root certificate.
210
211
        try {
212 1
            $this->trustDecisionManager->verifyTrust($registrationResult, $metadata);
213
        } catch (UntrustedException $e) {
214
            throw new VerificationException('The attestation is not trusted: ' . $e->getReason(), 0, $e);
215
        }
216
217
        // 17. Check that the credentialId is not yet registered to any other user. If registration is requested for a
218
        //     credential that is already registered to a different user, the Relying Party SHOULD fail this
219
        //     registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older
220
        //     registration.
221 1
        if ($this->credentialStore->findCredential($registrationResult->getCredentialId())) {
222
            throw new CredentialIdExistsException('Credential is already registered.');
223
        }
224
225
        // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, then register
226
        //     the new credential with the account that was denoted in the options.user passed to create(), by
227
        //     associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData,
228
        //     as appropriate for the Relying Party's system.
229
        // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
230
        //     the Relying Party SHOULD fail the registration ceremony.
231
        //
232
        //    NOTE: However, if permitted by policy, the Relying Party MAY register the credential ID and credential
233
        //    public key but treat the credential as one with self attestation (see §6.3.3 Attestation Types).
234
        //    If doing so, the Relying Party is asserting there is no cryptographic proof that the public key credential
235
        //    has been generated by a particular authenticator model. See [FIDOSecRef] and [UAFProtocol] for a more
236
        //    detailed discussion.
237
        //
238
        //    Verification of attestation objects requires that the Relying Party has a trusted method of determining
239
        //    acceptable trust anchors in step 15 above. Also, if certificates are being used, the Relying Party MUST
240
        //    have access to certificate status information for the intermediate CA certificates. The Relying Party MUST
241
        //    also be able to build the attestation certificate chain if the client did not provide this chain in the
242
        //    attestation information.
243 1
        return $registrationResult;
244
    }
245
246 1
    public function startAuthentication(AuthenticationOptions $options): AuthenticationRequest
247
    {
248 1
        $challenge = $this->createChallenge();
249
250 1
        $requestOptions = new PublicKeyCredentialRequestOptions($challenge);
251 1
        $requestOptions->setRpId($this->relyingParty->getId());
252 1
        $uv = $options->getUserVerification();
253 1
        if ($uv !== UserVerificationRequirement::DEFAULT) {
254 1
            $requestOptions->setUserVerification($uv);
255
        }
256 1
        $requestOptions->setTimeout($options->getTimeout());
257
258 1
        $this->addAllowCredentials($options, $requestOptions);
259
260 1
        $extensions = $options->getExtensionInputs();
261 1
        if (count($extensions) > 0) {
262
            $requestOptions->setExtensions(
263
                AuthenticationExtensionsClientInputs::fromArray($extensions)
264
            );
265
        }
266
267 1
        $context = $this->createAuthenticationContext($options, $requestOptions);
268 1
        return new AuthenticationRequest($requestOptions, $context);
269
    }
270
271 1
    private function createAuthenticationContext(AuthenticationOptions $authOptions, PublicKeyCredentialRequestOptions $options): AuthenticationContext
272
    {
273 1
        $origin = $this->relyingParty->getOrigin();
274 1
        $rpId = $this->relyingParty->getEffectiveId();
275
276
        // TODO: mismatch $rp and rp in $policy? Check?
277 1
        $context = new AuthenticationContext($options->getChallenge(), $origin, $rpId, $authOptions->getUserHandle());
278
279 1
        if ($authOptions->getUserVerification() === UserVerificationRequirement::REQUIRED) {
280
            $context->setUserVerificationRequired(true);
281
        }
282
283 1
        $context->setUserPresenceRequired($this->policy->isUserPresenceRequired());
284
285 1
        $allowCredentials = $options->getAllowCredentials();
286 1
        if ($allowCredentials !== null) {
287 1
            foreach ($allowCredentials as $credential) {
288 1
                $context->addAllowCredentialId(CredentialId::fromBuffer($credential->getId()));
289
            }
290
        }
291 1
        foreach ($authOptions->getExtensionInputs() as $input) {
292
            $context->addExtensionInput($input);
293
        }
294 1
        return $context;
295
    }
296
297
    /**
298
     * @param PublicKeyCredentialInterface $credential Assertion credential response from the client
299
     *
300
     * @throws VerificationException
301
     */
302 11
    public function finishAuthentication(PublicKeyCredentialInterface $credential, AuthenticationContext $context): AuthenticationResultInterface
303
    {
304 11
        $verifier = new AuthenticationVerifier($this->credentialStore, $this->extensionRegistry);
305
306 11
        $authenticationResult = $verifier->verifyAuthenticatonAssertion($credential, $context);
307
308 4
        return $authenticationResult;
309
    }
310
311
    /**
312
     * @throws WebAuthnException
313
     */
314 1
    private function addAllowCredentials(AuthenticationOptions $options, PublicKeyCredentialRequestOptions $requestOptions): void
315
    {
316 1
        $userHandle = $options->getUserHandle();
317 1
        if ($userHandle !== null) {
318
            $credentialIds = $this->credentialStore->getUserCredentialIds($userHandle);
319
            if (count($credentialIds) === 0) {
320
                throw new NoCredentialsException('User being authenticated has no credentials.');
321
            }
322
            foreach ($credentialIds as $credentialId) {
323
                $descriptor = new PublicKeyCredentialDescriptor($credentialId->toBuffer());
324
                $requestOptions->addAllowedCredential($descriptor);
325
            }
326
        }
327
328 1
        $credentialIds = $options->getAllowCredentials();
329 1
        if (count($credentialIds) > 0) {
330 1
            foreach ($credentialIds as $credential) {
331 1
                $credentialId = $credential->toBuffer();
332 1
                $descriptor = new PublicKeyCredentialDescriptor($credentialId);
333
                // TODO
334
//                foreach ($transports as $transport) {
335
//                    $descriptor->addTransport($transport);
336
//                }
337 1
                $requestOptions->addAllowedCredential($descriptor);
338
            }
339
        }
340 1
    }
341
342 1
    private function createUserEntity(UserIdentityInterface $user): PublicKeyCredentialUserEntity
343
    {
344 1
        return new PublicKeyCredentialUserEntity(
345 1
            $user->getUsername(),
346 1
            $user->getUserHandle()->toBuffer(),
347 1
            $user->getDisplayName()
348
        );
349
    }
350
351
    /**
352
     * @return PublicKeyCredentialParameters[]
353
     */
354 1
    private function getCredentialParameters(): array
355
    {
356 1
        $parameters = [];
357 1
        $algorithms = $this->policy->getAllowedAlgorithms();        // TODO: verify server side?
358 1
        foreach ($algorithms as $algorithm) {
359 1
            $parameters[] = new PublicKeyCredentialParameters($algorithm);
360
        }
361 1
        return $parameters;
362
    }
363
364 2
    private function createChallenge(): ByteBuffer
365
    {
366 2
        return ByteBuffer::randomBuffer($this->policy->getChallengeLength());
367
    }
368
}
369