Completed
Push — master ( 4282cd...e9aacf )
by Garion
23s queued 10s
created

getAuthenticatorAttestationResponseValidator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 11
rs 10
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\WebAuthn;
4
5
use Cose\Algorithms;
6
use Exception;
7
use GuzzleHttp\Psr7\ServerRequest;
8
use Psr\Log\LoggerInterface;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Control\HTTPRequest;
11
use SilverStripe\Core\Config\Configurable;
12
use SilverStripe\Core\Extensible;
13
use SilverStripe\MFA\Method\Handler\RegisterHandlerInterface;
14
use SilverStripe\MFA\State\Result;
15
use SilverStripe\MFA\Store\StoreInterface;
16
use SilverStripe\Security\Member;
17
use SilverStripe\SiteConfig\SiteConfig;
18
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
19
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
20
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
21
use Webauthn\AuthenticatorAttestationResponse;
22
use Webauthn\AuthenticatorAttestationResponseValidator;
23
use Webauthn\AuthenticatorSelectionCriteria;
24
use Webauthn\PublicKeyCredentialCreationOptions;
25
use Webauthn\PublicKeyCredentialParameters;
26
use Webauthn\PublicKeyCredentialRpEntity;
27
use Webauthn\PublicKeyCredentialUserEntity;
28
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
29
30
class RegisterHandler implements RegisterHandlerInterface
31
{
32
    use BaseHandlerTrait;
33
    use Extensible;
34
    use Configurable;
35
36
    /**
37
     * Provide a user help link that will be available when registering backup codes
38
     * TODO Will this have a user help link as a default?
39
     *
40
     * @config
41
     * @var string
42
     */
43
    private static $user_help_link;
0 ignored issues
show
introduced by
The private property $user_help_link is not used, and could be removed.
Loading history...
44
45
    /**
46
     * The default attachment mode to use for Authentication Selection Criteria.
47
     *
48
     * See {@link getAuthenticatorSelectionCriteria()} for more information.
49
     *
50
     * @config
51
     * @var string
52
     */
53
    private static $authenticator_attachment = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
0 ignored issues
show
introduced by
The private property $authenticator_attachment is not used, and could be removed.
Loading history...
54
55
    /**
56
     * Dependency injection configuration
57
     *
58
     * @config
59
     * @var array
60
     */
61
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
62
        'Logger' => '%$' . LoggerInterface::class . '.mfa',
63
    ];
64
65
    /**
66
     * @var LoggerInterface
67
     */
68
    protected $logger = null;
69
70
    /**
71
     * Sets the {@see $logger} member variable
72
     *
73
     * @param LoggerInterface|null $logger
74
     * @return self
75
     */
76
    public function setLogger(?LoggerInterface $logger): self
77
    {
78
        $this->logger = $logger;
79
        return $this;
80
    }
81
82
    /**
83
     * Stores any data required to handle a registration process with a method, and returns relevant state to be applied
84
     * to the front-end application managing the process.
85
     *
86
     * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated
87
     * @return array Props to be passed to a front-end component
88
     * @throws Exception When there is no valid source of CSPRNG
89
     */
90
    public function start(StoreInterface $store): array
91
    {
92
        $options = $this->getCredentialCreationOptions($store, true);
93
94
        return [
95
            'keyData' => $options,
96
        ];
97
    }
98
99
    /**
100
     * Confirm that the provided details are valid, and create a new RegisteredMethod against the member.
101
     *
102
     * @param HTTPRequest $request
103
     * @param StoreInterface $store
104
     * @return Result
105
     * @throws Exception
106
     */
107
    public function register(HTTPRequest $request, StoreInterface $store): Result
108
    {
109
        $options = $this->getCredentialCreationOptions($store);
110
        $data = json_decode((string) $request->getBody(), true);
111
112
        try {
113
            if (empty($data['credentials'])) {
114
                throw new ResponseDataException('Incomplete data, required information missing');
115
            }
116
117
            $decoder = $this->getDecoder();
118
            $attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder);
119
            $attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder);
120
            $publicKeyCredentialLoader = $this->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder);
121
            $publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data['credentials']));
122
            $response = $publicKeyCredential->getResponse();
123
124
            if (!$response instanceof AuthenticatorAttestationResponse) {
125
                throw new ResponseTypeException('Unexpected response type found');
126
            }
127
128
            if (!$response->getAttestationObject()->getAuthData()->hasAttestedCredentialData()) {
129
                throw new ResponseDataException('Incomplete data, required information missing');
130
            }
131
132
            // Create a PSR-7 request
133
            $psrRequest = ServerRequest::fromGlobals();
134
135
            // Validate the webauthn response
136
            $this->getAuthenticatorAttestationResponseValidator($attestationStatementSupportManager, $store)
137
                ->check($response, $options, $psrRequest);
138
        } catch (Exception $e) {
139
            $this->logger->error($e->getMessage());
140
            return Result::create(false, 'Registration failed: ' . $e->getMessage());
141
        }
142
143
        return Result::create()->setContext([
144
            'descriptor' => $publicKeyCredential->getPublicKeyCredentialDescriptor(),
145
            'data' => $response->getAttestationObject()->getAuthData()->getAttestedCredentialData(),
146
            'counter' => null,
147
        ]);
148
    }
149
150
    /**
151
     * @param AttestationStatementSupportManager $attestationStatementSupportManager
152
     * @param StoreInterface $store
153
     * @return AuthenticatorAttestationResponseValidator
154
     */
155
    protected function getAuthenticatorAttestationResponseValidator(
156
        AttestationStatementSupportManager $attestationStatementSupportManager,
157
        StoreInterface $store
158
    ): AuthenticatorAttestationResponseValidator {
159
        $credentialRepository = new CredentialRepository($store->getMember());
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

159
        $credentialRepository = new CredentialRepository(/** @scrutinizer ignore-type */ $store->getMember());
Loading history...
160
161
        return new AuthenticatorAttestationResponseValidator(
162
            $attestationStatementSupportManager,
163
            $credentialRepository,
164
            new TokenBindingNotSupportedHandler(),
165
            new ExtensionOutputCheckerHandler()
166
        );
167
    }
168
169
    /**
170
     * Provide a localised name for this MFA Method.
171
     *
172
     * @return string
173
     */
174
    public function getName(): string
175
    {
176
        return _t(__CLASS__ . '.NAME', 'Security key');
177
    }
178
179
    /**
180
     * Provide a localised description of this MFA Method.
181
     *
182
     * eg. "Verification codes are created by an app on your phone"
183
     *
184
     * @return string
185
     */
186
    public function getDescription(): string
187
    {
188
        return _t(
189
            __CLASS__ . '.DESCRIPTION',
190
            'A small USB device which is used for verifying you'
191
        );
192
    }
193
194
    /**
195
     * Provide a localised URL to a support article about the registration process for this MFA Method.
196
     *
197
     * @return string
198
     */
199
    public function getSupportLink(): string
200
    {
201
        return static::config()->get('user_help_link') ?: '';
202
    }
203
204
    /**
205
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
206
     *
207
     * @return string
208
     */
209
    public function getComponent(): string
210
    {
211
        return 'WebAuthnRegister';
212
    }
213
214
    /**
215
     * @return PublicKeyCredentialRpEntity
216
     */
217
    protected function getRelyingPartyEntity(): PublicKeyCredentialRpEntity
218
    {
219
        // Relying party entity ONLY allows domains or subdomains. Remove ports or anything else that isn't already.
220
        // See https://github.com/web-auth/webauthn-framework/blob/v1.2.2/doc/webauthn/PublicKeyCredentialCreation.md#relying-party-entity
221
        $host = parse_url(Director::host(), PHP_URL_HOST);
222
223
        return new PublicKeyCredentialRpEntity(
224
            (string) SiteConfig::current_site_config()->Title,
225
            $host,
226
            static::config()->get('application_logo')
227
        );
228
    }
229
230
    /**
231
     * @param Member $member
232
     * @return PublicKeyCredentialUserEntity
233
     */
234
    protected function getUserEntity(Member $member): PublicKeyCredentialUserEntity
235
    {
236
        return new PublicKeyCredentialUserEntity(
237
            $member->getName(),
238
            (string) $member->ID,
239
            $member->getName()
240
        );
241
    }
242
243
    /**
244
     * @param StoreInterface $store
245
     * @param bool $reset
246
     * @return PublicKeyCredentialCreationOptions
247
     * @throws Exception
248
     */
249
    protected function getCredentialCreationOptions(
250
        StoreInterface $store,
251
        bool $reset = false
252
    ): PublicKeyCredentialCreationOptions {
253
        $state = $store->getState();
254
255
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
256
            return PublicKeyCredentialCreationOptions::createFromArray($state['credentialOptions']);
257
        }
258
259
        $credentialOptions = new PublicKeyCredentialCreationOptions(
260
            $this->getRelyingPartyEntity(),
261
            $this->getUserEntity($store->getMember()),
0 ignored issues
show
Bug introduced by
It seems like $store->getMember() can also be of type null; however, parameter $member of SilverStripe\WebAuthn\Re...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

261
            $this->getUserEntity(/** @scrutinizer ignore-type */ $store->getMember()),
Loading history...
262
            random_bytes(32),
263
            [new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256)],
264
            40000,
265
            [],
266
            $this->getAuthenticatorSelectionCriteria(),
267
            PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
268
            new AuthenticationExtensionsClientInputs()
269
        );
270
271
        $store->setState(['credentialOptions' => $credentialOptions] + $state);
272
273
        return $credentialOptions;
274
    }
275
276
    /**
277
     * Returns an "Authenticator Selection Criteria" object which is intended to select the appropriate authenticators
278
     * to participate in the creation operation.
279
     *
280
     * The default is to allow only "cross platform" authenticators, e.g. disabling "single platform" authenticators
281
     * such as touch ID.
282
     *
283
     * For more information: https://github.com/web-auth/webauthn-framework/blob/v1.2/doc/webauthn/PublicKeyCredentialCreation.md#authenticator-selection-criteria
284
     *
285
     * @return AuthenticatorSelectionCriteria
286
     */
287
    protected function getAuthenticatorSelectionCriteria(): AuthenticatorSelectionCriteria
288
    {
289
        return new AuthenticatorSelectionCriteria(
290
            (string) $this->config()->get('authenticator_attachment')
291
        );
292
    }
293
}
294