Passed
Pull Request — master (#30)
by
unknown
04:24
created

VerifyHandler::start()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
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 CBOR\OtherObject\OtherObjectManager;
7
use CBOR\Tag\TagObjectManager;
8
use Exception;
9
use GuzzleHttp\Psr7\ServerRequest;
10
use Psr\Log\LoggerInterface;
11
use RuntimeException;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\MFA\Method\Handler\VerifyHandlerInterface;
14
use SilverStripe\MFA\Model\RegisteredMethod;
15
use SilverStripe\MFA\State\Result;
16
use SilverStripe\MFA\Store\StoreInterface;
17
use Webauthn\AttestationStatement\AttestationObjectLoader;
18
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
19
use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport;
20
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
21
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
22
use Webauthn\AuthenticatorAssertionResponse;
23
use Webauthn\AuthenticatorAssertionResponseValidator;
24
use Webauthn\PublicKeyCredentialDescriptor;
25
use Webauthn\PublicKeyCredentialLoader;
26
use Webauthn\PublicKeyCredentialRequestOptions;
27
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
28
29
class VerifyHandler implements VerifyHandlerInterface
30
{
31
    /**
32
     * Dependency injection configuration
33
     *
34
     * @config
35
     * @var array
36
     */
37
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
38
        'Logger' => LoggerInterface::class . '.mfa',
39
    ];
40
41
    /**
42
     * @var LoggerInterface
43
     */
44
    protected $logger = null;
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
     * @throws Exception
76
     */
77
    public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result
78
    {
79
        $options = $this->getCredentialRequestOptions($store, $registeredMethod);
80
81
        $data = json_decode($request->getBody(), true);
82
83
        // CBOR
84
        $decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
85
86
        // Attestation statement support manager
87
        $attestationStatementSupportManager = new AttestationStatementSupportManager();
88
        $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
89
        $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport($decoder));
90
91
        // Attestation object loader
92
        $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager, $decoder);
93
94
        $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader, $decoder);
95
96
        $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

96
        $credentialRepository = new CredentialRepository(/** @scrutinizer ignore-type */ $store->getMember(), $registeredMethod);
Loading history...
97
98
        $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
99
            $credentialRepository,
100
            $decoder,
101
            new TokenBindingNotSupportedHandler(),
102
            new ExtensionOutputCheckerHandler()
103
        );
104
105
        // Create a PSR-7 request
106
        $psrRequest = ServerRequest::fromGlobals();
107
108
        try {
109
            $publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data['credentials']));
110
            $response = $publicKeyCredential->getResponse();
111
112
            if (!$response instanceof AuthenticatorAssertionResponse) {
113
                throw new RuntimeException('Unexpected response type found');
114
            }
115
116
            $authenticatorAssertionResponseValidator->check(
117
                $publicKeyCredential->getRawId(),
118
                $publicKeyCredential->getResponse(),
119
                $options,
120
                $psrRequest,
121
                (string) $store->getMember()->ID
122
            );
123
124
            return Result::create();
125
        } catch (Exception $e) {
126
            $this->logger->error($e->getMessage());
127
            throw $e;
128
        }
129
    }
130
131
    /**
132
     * Provide a localised string that serves as a lead in for choosing this option for authentication
133
     *
134
     * eg. "Enter one of your recovery codes"
135
     *
136
     * @return string
137
     */
138
    public function getLeadInLabel(): string
139
    {
140
        return _t(__CLASS__ . '.LEAD_IN', 'Verify with security key');
141
    }
142
143
    /**
144
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
145
     *
146
     * @return string
147
     */
148
    public function getComponent(): string
149
    {
150
        return 'WebAuthnVerify';
151
    }
152
153
    protected function getCredentialRequestOptions(
154
        StoreInterface $store,
155
        RegisteredMethod $registeredMethod,
156
        $reset = false
157
    ): PublicKeyCredentialRequestOptions {
158
        $state = $store->getState();
159
160
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
161
            return PublicKeyCredentialRequestOptions::createFromArray($state['credentialOptions']);
162
        }
163
164
        $data = json_decode($registeredMethod->Data, true);
165
        $descriptor = PublicKeyCredentialDescriptor::createFromArray($data['descriptor']);
166
167
        $options = new PublicKeyCredentialRequestOptions(
168
            random_bytes(32),
169
            40000,
170
            null,
171
            [$descriptor],
172
            PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED
173
        );
174
175
        $state['credentialOptions'] = $options;
176
        $store->setState($state);
177
178
        return $options;
179
    }
180
}
181