Passed
Pull Request — master (#30)
by
unknown
01:44
created

VerifyHandler::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
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 SilverStripe\Control\HTTPRequest;
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\AttestationStatement\AttestationObjectLoader;
17
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
18
use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport;
19
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
20
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
21
use Webauthn\AuthenticatorAssertionResponse;
22
use Webauthn\AuthenticatorAssertionResponseValidator;
23
use Webauthn\PublicKeyCredentialDescriptor;
24
use Webauthn\PublicKeyCredentialLoader;
25
use Webauthn\PublicKeyCredentialRequestOptions;
26
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
27
28
class VerifyHandler implements VerifyHandlerInterface
29
{
30
    /**
31
     * Dependency injection configuration
32
     *
33
     * @config
34
     * @var array
35
     */
36
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
37
        'Logger' => LoggerInterface::class . '.mfa',
38
    ];
39
40
    /**
41
     * @var LoggerInterface
42
     */
43
    protected $logger = null;
44
45
    public function setLogger(LoggerInterface $logger): self
46
    {
47
        $this->logger = $logger;
48
        return $this;
49
    }
50
51
    /**
52
     * Stores any data required to handle a login process with a method, and returns relevant state to be applied to the
53
     * front-end application managing the process.
54
     *
55
     * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated
56
     * @param RegisteredMethod $method The RegisteredMethod instance that is being verified
57
     * @return array Props to be passed to a front-end component
58
     */
59
    public function start(StoreInterface $store, RegisteredMethod $method): array
60
    {
61
        return [
62
            'publicKey' => $this->getCredentialRequestOptions($store, $method, true),
63
        ];
64
    }
65
66
    /**
67
     * Verify the request has provided the right information to verify the member that aligns with any sessions state
68
     * that may have been set prior
69
     *
70
     * @param HTTPRequest $request
71
     * @param StoreInterface $store
72
     * @param RegisteredMethod $registeredMethod The RegisteredMethod instance that is being verified
73
     * @return Result
74
     * @throws Exception
75
     */
76
    public function verify(HTTPRequest $request, StoreInterface $store, RegisteredMethod $registeredMethod): Result
77
    {
78
        $options = $this->getCredentialRequestOptions($store, $registeredMethod);
79
80
        $data = json_decode($request->getBody(), true);
81
82
        // CBOR
83
        $decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
84
85
        // Attestation statement support manager
86
        $attestationStatementSupportManager = new AttestationStatementSupportManager();
87
        $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
88
        $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport($decoder));
89
90
        // Attestation object loader
91
        $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager, $decoder);
92
93
        $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader, $decoder);
94
95
        $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

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