Completed
Push — master ( a08ba5...c0fb6f )
by
unknown
28s queued 12s
created

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

131
        $credentialRepository = new CredentialRepository(/** @scrutinizer ignore-type */ $store->getMember());
Loading history...
132
133
        $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
134
            $attestationStatementSupportManager,
135
            $credentialRepository,
136
            new TokenBindingNotSupportedHandler(),
137
            new ExtensionOutputCheckerHandler()
138
        );
139
140
        // Create a PSR-7 request
141
        $request = ServerRequest::fromGlobals();
142
143
        try {
144
            $publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data['credentials']));
145
            $response = $publicKeyCredential->getResponse();
146
147
            if (!$response instanceof AuthenticatorAttestationResponse) {
148
                throw new ResponseTypeException('Unexpected response type found');
149
            }
150
151
            if (!$response->getAttestationObject()->getAuthData()->hasAttestedCredentialData()) {
152
                throw new ResponseDataException('Incomplete data, required information missing');
153
            }
154
155
            $authenticatorAttestationResponseValidator->check($response, $options, $request);
156
        } catch (Exception $e) {
157
            $this->logger->error($e->getMessage());
158
            return Result::create(false, 'Registration failed: ' . $e->getMessage());
159
        }
160
161
        return Result::create()->setContext([
162
            'descriptor' => $publicKeyCredential->getPublicKeyCredentialDescriptor(),
163
            'data' => $response->getAttestationObject()->getAuthData()->getAttestedCredentialData(),
164
            'counter' => null,
165
        ]);
166
    }
167
168
    /**
169
     * Provide a localised name for this MFA Method.
170
     *
171
     * @return string
172
     */
173
    public function getName(): string
174
    {
175
        return _t(__CLASS__ . '.NAME', 'Security key');
176
    }
177
178
    /**
179
     * Provide a localised description of this MFA Method.
180
     *
181
     * eg. "Verification codes are created by an app on your phone"
182
     *
183
     * @return string
184
     */
185
    public function getDescription(): string
186
    {
187
        return _t(
188
            __CLASS__ . '.DESCRIPTION',
189
            'A small USB device which is used for verifying you'
190
        );
191
    }
192
193
    /**
194
     * Provide a localised URL to a support article about the registration process for this MFA Method.
195
     *
196
     * @return string
197
     */
198
    public function getSupportLink(): string
199
    {
200
        return static::config()->get('user_help_link') ?: '';
201
    }
202
203
    /**
204
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
205
     *
206
     * @return string
207
     */
208
    public function getComponent(): string
209
    {
210
        return 'WebAuthnRegister';
211
    }
212
213
    /**
214
     * @return PublicKeyCredentialRpEntity
215
     */
216
    protected function getRelyingPartyEntity(): PublicKeyCredentialRpEntity
217
    {
218
        // Relying party entity ONLY allows domains or subdomains. Remove ports or anything else that isn't already.
219
        // See https://github.com/web-auth/webauthn-framework/blob/v1.2.2/doc/webauthn/PublicKeyCredentialCreation.md#relying-party-entity
220
        $host = parse_url(Director::host(), PHP_URL_HOST);
221
222
        return new PublicKeyCredentialRpEntity(
223
            (string) SiteConfig::current_site_config()->Title,
224
            $host,
225
            static::config()->get('application_logo')
226
        );
227
    }
228
229
    /**
230
     * @param Member $member
231
     * @return PublicKeyCredentialUserEntity
232
     */
233
    protected function getUserEntity(Member $member): PublicKeyCredentialUserEntity
234
    {
235
        return new PublicKeyCredentialUserEntity(
236
            $member->getName(),
237
            (string) $member->ID,
238
            $member->getName()
239
        );
240
    }
241
242
    /**
243
     * @param StoreInterface $store
244
     * @param bool $reset
245
     * @return PublicKeyCredentialCreationOptions
246
     * @throws Exception
247
     */
248
    protected function getCredentialCreationOptions(
249
        StoreInterface $store,
250
        bool $reset = false
251
    ): PublicKeyCredentialCreationOptions {
252
        $state = $store->getState();
253
254
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
255
            return PublicKeyCredentialCreationOptions::createFromArray($state['credentialOptions']);
256
        }
257
258
        $credentialOptions = new PublicKeyCredentialCreationOptions(
259
            $this->getRelyingPartyEntity(),
260
            $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

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