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\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
19
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
20
use Webauthn\AuthenticatorAttestationResponse;
21
use Webauthn\AuthenticatorAttestationResponseValidator;
22
use Webauthn\AuthenticatorSelectionCriteria;
23
use Webauthn\PublicKeyCredentialCreationOptions;
24
use Webauthn\PublicKeyCredentialParameters;
25
use Webauthn\PublicKeyCredentialRpEntity;
26
use Webauthn\PublicKeyCredentialUserEntity;
27
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
28

29
class RegisterHandler implements RegisterHandlerInterface
30
{
31
    use BaseHandlerTrait;
32
    use Extensible;
33
    use Configurable;
34

35
    /**
36
     * Provide a user help link that will be available when registering backup codes
37
     * TODO Will this have a user help link as a default?
38
     *
39
     * @config
40
     * @var string
41
     */
42
    private static $user_help_link;
43

44
    /**
45
     * The default attachment mode to use for Authentication Selection Criteria.
46
     *
47
     * See {@link getAuthenticatorSelectionCriteria()} for more information.
48
     *
49
     * @config
50
     * @var string
51
     */
52
    private static $authenticator_attachment = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
53

54
    /**
55
     * Dependency injection configuration
56
     *
57
     * @config
58
     * @var array
59
     */
60
    private static $dependencies = [
61
        'Logger' => '%$' . LoggerInterface::class . '.mfa',
62
    ];
63

64
    /**
65
     * @var LoggerInterface
66
     */
67
    protected $logger = null;
68

69
    /**
70
     * Sets the {@see $logger} member variable
71
     *
72
     * @param LoggerInterface|null $logger
73
     * @return self
74
     */
75 1
    public function setLogger(?LoggerInterface $logger): self
76
    {
77 1
        $this->logger = $logger;
78 1
        return $this;
79
    }
80

81
    /**
82
     * Stores any data required to handle a registration process with a method, and returns relevant state to be applied
83
     * to the front-end application managing the process.
84
     *
85
     * @param StoreInterface $store An object that hold session data (and the Member) that can be mutated
86
     * @return array Props to be passed to a front-end component
87
     * @throws Exception When there is no valid source of CSPRNG
88
     */
89 1
    public function start(StoreInterface $store): array
90
    {
91 1
        $options = $this->getCredentialCreationOptions($store, true);
92

93
        return [
94 1
            'keyData' => $options,
95
        ];
96
    }
97

98
    /**
99
     * Confirm that the provided details are valid, and create a new RegisteredMethod against the member.
100
     *
101
     * @param HTTPRequest $request
102
     * @param StoreInterface $store
103
     * @return Result
104
     * @throws Exception
105
     */
106 0
    public function register(HTTPRequest $request, StoreInterface $store): Result
107
    {
108 0
        $options = $this->getCredentialCreationOptions($store);
109 0
        $data = json_decode($request->getBody(), true);
110

111 0
        $decoder = $this->getDecoder();
112 0
        $attestationStatementSupportManager = $this->getAttestationStatementSupportManager($decoder);
113 0
        $attestationObjectLoader = $this->getAttestationObjectLoader($attestationStatementSupportManager, $decoder);
114 0
        $publicKeyCredentialLoader = $this->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder);
115

116 0
        $credentialRepository = new CredentialRepository($store->getMember());
117

118 0
        $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
119 0
            $attestationStatementSupportManager,
120
            $credentialRepository,
121 0
            new TokenBindingNotSupportedHandler(),
122 0
            new ExtensionOutputCheckerHandler()
123
        );
124

125
        // Create a PSR-7 request
126 0
        $psrRequest = ServerRequest::fromGlobals();
127

128
        try {
129 0
            $publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data['credentials']));
130 0
            $response = $publicKeyCredential->getResponse();
131

132 0
            if (!$response instanceof AuthenticatorAttestationResponse) {
133 0
                throw new ResponseTypeException('Unexpected response type found');
134
            }
135

136 0
            if (!$response->getAttestationObject()->getAuthData()->hasAttestedCredentialData()) {
137 0
                throw new ResponseDataException('Incomplete data, required information missing');
138
            }
139

140 0
            $authenticatorAttestationResponseValidator->check($response, $options, $psrRequest);
141 0
        } catch (Exception $e) {
142 0
            $this->logger->error($e->getMessage());
143 0
            return Result::create(false, 'Registration failed: ' . $e->getMessage());
144
        }
145

146 0
        return Result::create()->setContext([
147 0
            'descriptor' => $publicKeyCredential->getPublicKeyCredentialDescriptor(),
148 0
            'data' => $response->getAttestationObject()->getAuthData()->getAttestedCredentialData(),
149
            'counter' => null,
150
        ]);
151
    }
152

153
    /**
154
     * Provide a localised name for this MFA Method.
155
     *
156
     * @return string
157
     */
158 1
    public function getName(): string
159
    {
160 1
        return _t(__CLASS__ . '.NAME', 'Security key');
161
    }
162

163
    /**
164
     * Provide a localised description of this MFA Method.
165
     *
166
     * eg. "Verification codes are created by an app on your phone"
167
     *
168
     * @return string
169
     */
170 1
    public function getDescription(): string
171
    {
172 1
        return _t(
173 1
            __CLASS__ . '.DESCRIPTION',
174 1
            'A small USB device which is used for verifying you'
175
        );
176
    }
177

178
    /**
179
     * Provide a localised URL to a support article about the registration process for this MFA Method.
180
     *
181
     * @return string
182
     */
183 1
    public function getSupportLink(): string
184
    {
185 1
        return static::config()->get('user_help_link') ?: '';
186
    }
187

188
    /**
189
     * Get the key that a React UI component is registered under (with @silverstripe/react-injector on the front-end)
190
     *
191
     * @return string
192
     */
193 1
    public function getComponent(): string
194
    {
195 1
        return 'WebAuthnRegister';
196
    }
197

198
    /**
199
     * @return PublicKeyCredentialRpEntity
200
     */
201 1
    protected function getRelyingPartyEntity(): PublicKeyCredentialRpEntity
202
    {
203
        // Relying party entity ONLY allows domains or subdomains. Remove ports or anything else that isn't already.
204
        // See https://github.com/web-auth/webauthn-framework/blob/v1.2.2/doc/webauthn/PublicKeyCredentialCreation.md#relying-party-entity
205 1
        $host = parse_url(Director::host(), PHP_URL_HOST);
206

207 1
        return new PublicKeyCredentialRpEntity(
208 1
            (string) SiteConfig::current_site_config()->Title,
209
            $host,
210 1
            static::config()->get('application_logo')
211
        );
212
    }
213

214
    /**
215
     * @param Member $member
216
     * @return PublicKeyCredentialUserEntity
217
     */
218 1
    protected function getUserEntity(Member $member): PublicKeyCredentialUserEntity
219
    {
220 1
        return new PublicKeyCredentialUserEntity(
221 1
            $member->getName(),
222 1
            (string) $member->ID,
223 1
            $member->getName()
224
        );
225
    }
226

227
    /**
228
     * @param StoreInterface $store
229
     * @param bool $reset
230
     * @return PublicKeyCredentialCreationOptions
231
     * @throws Exception
232
     */
233 1
    protected function getCredentialCreationOptions(
234
        StoreInterface $store,
235
        bool $reset = false
236
    ): PublicKeyCredentialCreationOptions {
237 1
        $state = $store->getState();
238

239 1
        if (!$reset && !empty($state) && !empty($state['credentialOptions'])) {
240 0
            return PublicKeyCredentialCreationOptions::createFromArray($state['credentialOptions']);
241
        }
242

243 1
        $credentialOptions = new PublicKeyCredentialCreationOptions(
244 1
            $this->getRelyingPartyEntity(),
245 1
            $this->getUserEntity($store->getMember()),
246 1
            random_bytes(32),
247 1
            [new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256)],
248 1
            40000,
249 1
            [],
250 1
            $this->getAuthenticatorSelectionCriteria(),
251 1
            PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
252 1
            new AuthenticationExtensionsClientInputs()
253
        );
254

255 1
        $store->setState(['credentialOptions' => $credentialOptions] + $state);
256

257 1
        return $credentialOptions;
258
    }
259

260
    /**
261
     * Returns an "Authenticator Selection Criteria" object which is intended to select the appropriate authenticators
262
     * to participate in the creation operation.
263
     *
264
     * The default is to allow only "cross platform" authenticators, e.g. disabling "single platform" authenticators
265
     * such as touch ID.
266
     *
267
     * For more information: https://github.com/web-auth/webauthn-framework/blob/v1.2/doc/webauthn/PublicKeyCredentialCreation.md#authenticator-selection-criteria
268
     *
269
     * @return AuthenticatorSelectionCriteria
270
     */
271 1
    protected function getAuthenticatorSelectionCriteria(): AuthenticatorSelectionCriteria
272
    {
273 1
        return new AuthenticatorSelectionCriteria(
274 1
            (string) $this->config()->get('authenticator_attachment')
275
        );
276
    }
277
}

Read our documentation on viewing source code .

Loading