Passed
Push — master ( c219a4...e7152d )
by Garion
02:24
created

getAuthenticatorSelectionCriteria()   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 0
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 Cose\Algorithms;
9
use Exception;
10
use GuzzleHttp\Psr7\ServerRequest;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Core\Config\Configurable;
14
use SilverStripe\Core\Extensible;
15
use SilverStripe\MFA\Method\Handler\RegisterHandlerInterface;
16
use SilverStripe\MFA\State\Result;
17
use SilverStripe\MFA\Store\StoreInterface;
18
use SilverStripe\Security\Member;
19
use SilverStripe\SiteConfig\SiteConfig;
20
use Webauthn\AttestationStatement\AttestationObjectLoader;
21
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
22
use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport;
23
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
24
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
25
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
26
use Webauthn\AuthenticatorAttestationResponse;
27
use Webauthn\AuthenticatorAttestationResponseValidator;
28
use Webauthn\AuthenticatorSelectionCriteria;
29
use Webauthn\PublicKeyCredentialCreationOptions;
30
use Webauthn\PublicKeyCredentialLoader;
31
use Webauthn\PublicKeyCredentialParameters;
32
use Webauthn\PublicKeyCredentialRpEntity;
33
use Webauthn\PublicKeyCredentialUserEntity;
34
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
35
36
class RegisterHandler implements RegisterHandlerInterface
37
{
38
    use Extensible;
39
    use Configurable;
40
41
    /**
42
     * Provide a user help link that will be available when registering backup codes
43
     * TODO Will this have a user help link as a default?
44
     *
45
     * @config
46
     * @var string
47
     */
48
    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...
49
50
    /**
51
     * The default attachment mode to use for Authentication Selection Criteria.
52
     *
53
     * See {@link getAuthenticatorSelectionCriteria()} for more information.
54
     *
55
     * @config
56
     * @var string
57
     */
58
    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...
59
60
    /**
61
     * Stores any data required to handle a registration process with a method, and returns relevant state to be applied
62
     * to the front-end application managing the process.
63
     *
64
     * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated
65
     * @return array Props to be passed to a front-end component
66
     * @throws Exception When there is no valid source of CSPRNG
67
     */
68
    public function start(StoreInterface $store): array
69
    {
70
        $options = $this->getCredentialCreationOptions($store, true);
71
72
        return [
73
            'keyData' => $options,
74
        ];
75
    }
76
77
    /**
78
     * Confirm that the provided details are valid, and create a new RegisteredMethod against the member.
79
     *
80
     * @param HTTPRequest $request
81
     * @param StoreInterface $store
82
     * @return Result
83
     * @throws Exception
84
     */
85
    public function register(HTTPRequest $request, StoreInterface $store): Result
86
    {
87
        $options = $this->getCredentialCreationOptions($store);
88
        $data = json_decode($request->getBody(), true);
89
90
        // CBOR
91
        $decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
92
93
        // Attestation statement support manager
94
        $attestationStatementSupportManager = new AttestationStatementSupportManager();
95
        $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
96
        $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport($decoder));
97
98
        // Attestation object loader
99
        $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager, $decoder);
100
101
        $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader, $decoder);
102
103
        $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

103
        $credentialRepository = new CredentialRepository(/** @scrutinizer ignore-type */ $store->getMember());
Loading history...
104
105
        $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
106
            $attestationStatementSupportManager,
107
            $credentialRepository,
108
            new TokenBindingNotSupportedHandler(),
109
            new ExtensionOutputCheckerHandler()
110
        );
111
112
        // Create a PSR-7 request
113
        $request = ServerRequest::fromGlobals();
114
115
        try {
116
            $publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data['credentials']));
117
            $response = $publicKeyCredential->getResponse();
118
119
            if (!$response instanceof AuthenticatorAttestationResponse) {
120
                die('why even have this?');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return SilverStripe\MFA\State\Result. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
121
            }
122
123
            $authenticatorAttestationResponseValidator->check($response, $options, $request);
124
        } catch (Exception $e) {
125
            return Result::create(false, 'Registration failed: ' . $e->getMessage());
126
        }
127
128
        if (!$response->getAttestationObject()->getAuthData()->hasAttestedCredentialData()) {
129
            die('something else that might go wrong but probably wont');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
130
        }
131
132
        return Result::create()->setContext([
133
            'descriptor' => $publicKeyCredential->getPublicKeyCredentialDescriptor(),
134
            'data' => $response->getAttestationObject()->getAuthData()->getAttestedCredentialData(),
135
            'counter' => null,
136
        ]);
137
    }
138
139
    /**
140
     * Provide a localised name for this MFA Method.
141
     *
142
     * @return string
143
     */
144
    public function getName(): string
145
    {
146
        return _t(__CLASS__ . '.NAME', 'Security key');
147
    }
148
149
    /**
150
     * Provide a localised description of this MFA Method.
151
     *
152
     * eg. "Verification codes are created by an app on your phone"
153
     *
154
     * @return string
155
     */
156
    public function getDescription(): string
157
    {
158
        return _t(
159
            __CLASS__ . '.DESCRIPTION',
160
            'A small USB device which is used for verifying you'
161
        );
162
    }
163
164
    /**
165
     * Provide a localised URL to a support article about the registration process for this MFA Method.
166
     *
167
     * @return string
168
     */
169
    public function getSupportLink(): string
170
    {
171
        return static::config()->get('user_help_link') ?: '';
172
    }
173
174
    /**
175
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
176
     *
177
     * @return string
178
     */
179
    public function getComponent(): string
180
    {
181
        return 'WebAuthnRegister';
182
    }
183
184
    /**
185
     * @return PublicKeyCredentialRpEntity
186
     */
187
    protected function getRelyingPartyEntity(): PublicKeyCredentialRpEntity
188
    {
189
        // Relying party entity ONLY allows domains or subdomains. Remove ports or anything else that isn't already.
190
        // See https://github.com/web-auth/webauthn-framework/blob/v1.2.2/doc/webauthn/PublicKeyCredentialCreation.md#relying-party-entity
191
        $host = parse_url(Director::host(), PHP_URL_HOST);
192
193
        return new PublicKeyCredentialRpEntity(
194
            (string) SiteConfig::current_site_config()->Title,
195
            $host,
196
            static::config()->get('application_logo')
197
        );
198
    }
199
200
    /**
201
     * @param Member $member
202
     * @return PublicKeyCredentialUserEntity
203
     */
204
    protected function getUserEntity(Member $member): PublicKeyCredentialUserEntity
205
    {
206
        return new PublicKeyCredentialUserEntity(
207
            $member->getName(),
208
            (string) $member->ID,
209
            $member->getName()
210
        );
211
    }
212
213
    /**
214
     * @param StoreInterface $store
215
     * @param bool $reset
216
     * @return PublicKeyCredentialCreationOptions
217
     * @throws Exception
218
     */
219
    protected function getCredentialCreationOptions(
220
        StoreInterface $store,
221
        bool $reset = false
222
    ): PublicKeyCredentialCreationOptions {
223
        $state = $store->getState();
224
225
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
226
            return PublicKeyCredentialCreationOptions::createFromArray($state['credentialOptions']);
227
        }
228
229
        $credentialOptions = new PublicKeyCredentialCreationOptions(
230
            $this->getRelyingPartyEntity(),
231
            $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

231
            $this->getUserEntity(/** @scrutinizer ignore-type */ $store->getMember()),
Loading history...
232
            random_bytes(32),
233
            [new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256)],
234
            40000,
235
            [],
236
            $this->getAuthenticatorSelectionCriteria(),
237
            PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
238
            new AuthenticationExtensionsClientInputs()
239
        );
240
241
        $store->setState(['credentialOptions' => $credentialOptions] + $state);
242
243
        return $credentialOptions;
244
    }
245
246
    /**
247
     * Returns an "Authenticator Selection Criteria" object which is intended to select the appropriate authenticators
248
     * to participate in the creation operation.
249
     *
250
     * The default is to allow only "cross platform" authenticators, e.g. disabling "single platform" authenticators
251
     * such as touch ID.
252
     *
253
     * For more information: https://github.com/web-auth/webauthn-framework/blob/v1.2/doc/webauthn/PublicKeyCredentialCreation.md#authenticator-selection-criteria
254
     *
255
     * @return AuthenticatorSelectionCriteria
256
     */
257
    protected function getAuthenticatorSelectionCriteria(): AuthenticatorSelectionCriteria
258
    {
259
        return new AuthenticatorSelectionCriteria(
260
            (string) $this->config()->get('authenticator_attachment')
261
        );
262
    }
263
}
264