Bug 1854550 - pt 10. Allow LOG() with zero extra arguments r=glandium
[gecko.git] / dom / webauthn / WebAuthnManager.cpp
bloba06271faff62e9f1dea3436c01cbc18fa4042754
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "hasht.h"
8 #include "nsHTMLDocument.h"
9 #include "nsIURIMutator.h"
10 #include "nsThreadUtils.h"
11 #include "WebAuthnCoseIdentifiers.h"
12 #include "WebAuthnEnumStrings.h"
13 #include "WebAuthnTransportIdentifiers.h"
14 #include "mozilla/Base64.h"
15 #include "mozilla/BasePrincipal.h"
16 #include "mozilla/dom/AuthenticatorAssertionResponse.h"
17 #include "mozilla/dom/AuthenticatorAttestationResponse.h"
18 #include "mozilla/dom/PublicKeyCredential.h"
19 #include "mozilla/dom/Promise.h"
20 #include "mozilla/dom/PWebAuthnTransaction.h"
21 #include "mozilla/dom/WebAuthnManager.h"
22 #include "mozilla/dom/WebAuthnTransactionChild.h"
23 #include "mozilla/dom/WebAuthnUtil.h"
24 #include "mozilla/ipc/BackgroundChild.h"
25 #include "mozilla/ipc/PBackgroundChild.h"
27 #ifdef XP_WIN
28 # include "WinWebAuthnManager.h"
29 #endif
31 using namespace mozilla::ipc;
33 namespace mozilla::dom {
35 /***********************************************************************
36 * Statics
37 **********************************************************************/
39 namespace {
40 static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
43 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(WebAuthnManager,
44 WebAuthnManagerBase)
46 NS_IMPL_CYCLE_COLLECTION_CLASS(WebAuthnManager)
48 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebAuthnManager,
49 WebAuthnManagerBase)
50 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction)
51 tmp->mTransaction.reset();
52 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
54 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager,
55 WebAuthnManagerBase)
56 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
57 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
59 /***********************************************************************
60 * Utility Functions
61 **********************************************************************/
63 static nsresult AssembleClientData(
64 const nsAString& aOrigin, const CryptoBuffer& aChallenge,
65 const nsAString& aType,
66 const AuthenticationExtensionsClientInputs& aExtensions,
67 /* out */ nsACString& aJsonOut) {
68 MOZ_ASSERT(NS_IsMainThread());
70 nsString challengeBase64;
71 nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
72 if (NS_WARN_IF(NS_FAILED(rv))) {
73 return NS_ERROR_FAILURE;
76 CollectedClientData clientDataObject;
77 clientDataObject.mType.Assign(aType);
78 clientDataObject.mChallenge.Assign(challengeBase64);
79 clientDataObject.mOrigin.Assign(aOrigin);
81 nsAutoString temp;
82 if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
83 return NS_ERROR_FAILURE;
86 aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
87 return NS_OK;
90 nsresult GetOrigin(nsPIDOMWindowInner* aParent,
91 /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost) {
92 MOZ_ASSERT(aParent);
93 nsCOMPtr<Document> doc = aParent->GetDoc();
94 MOZ_ASSERT(doc);
96 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
97 nsresult rv =
98 nsContentUtils::GetWebExposedOriginSerialization(principal, aOrigin);
99 if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(aOrigin.IsEmpty())) {
100 return NS_ERROR_FAILURE;
103 if (principal->GetIsIpAddress()) {
104 return NS_ERROR_DOM_SECURITY_ERR;
107 if (aOrigin.EqualsLiteral("null")) {
108 // 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a
109 // DOMException whose name is "NotAllowedError", and terminate this
110 // algorithm
111 MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
112 ("Rejecting due to opaque origin"));
113 return NS_ERROR_DOM_NOT_ALLOWED_ERR;
116 nsCOMPtr<nsIURI> originUri;
117 auto* basePrin = BasePrincipal::Cast(principal);
118 if (NS_FAILED(basePrin->GetURI(getter_AddRefs(originUri)))) {
119 return NS_ERROR_FAILURE;
121 if (NS_FAILED(originUri->GetAsciiHost(aHost))) {
122 return NS_ERROR_FAILURE;
125 return NS_OK;
128 nsresult RelaxSameOrigin(nsPIDOMWindowInner* aParent,
129 const nsAString& aInputRpId,
130 /* out */ nsACString& aRelaxedRpId) {
131 MOZ_ASSERT(aParent);
132 nsCOMPtr<Document> doc = aParent->GetDoc();
133 MOZ_ASSERT(doc);
135 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
136 auto* basePrin = BasePrincipal::Cast(principal);
137 nsCOMPtr<nsIURI> uri;
139 if (NS_FAILED(basePrin->GetURI(getter_AddRefs(uri)))) {
140 return NS_ERROR_FAILURE;
142 nsAutoCString originHost;
143 if (NS_FAILED(uri->GetAsciiHost(originHost))) {
144 return NS_ERROR_FAILURE;
146 nsCOMPtr<Document> document = aParent->GetDoc();
147 if (!document || !document->IsHTMLDocument()) {
148 return NS_ERROR_FAILURE;
150 nsHTMLDocument* html = document->AsHTMLDocument();
151 // See if the given RP ID is a valid domain string.
152 // (We use the document's URI here as a template so we don't have to come up
153 // with our own scheme, etc. If we can successfully set the host as the given
154 // RP ID, then it should be a valid domain string.)
155 nsCOMPtr<nsIURI> inputRpIdURI;
156 nsresult rv = NS_MutateURI(uri)
157 .SetHost(NS_ConvertUTF16toUTF8(aInputRpId))
158 .Finalize(inputRpIdURI);
159 if (NS_FAILED(rv)) {
160 return NS_ERROR_DOM_SECURITY_ERR;
162 nsAutoCString inputRpId;
163 if (NS_FAILED(inputRpIdURI->GetAsciiHost(inputRpId))) {
164 return NS_ERROR_FAILURE;
166 if (!html->IsRegistrableDomainSuffixOfOrEqualTo(
167 NS_ConvertUTF8toUTF16(inputRpId), originHost)) {
168 return NS_ERROR_DOM_SECURITY_ERR;
171 aRelaxedRpId.Assign(inputRpId);
172 return NS_OK;
175 /***********************************************************************
176 * WebAuthnManager Implementation
177 **********************************************************************/
179 void WebAuthnManager::ClearTransaction() {
180 if (mTransaction.isSome()) {
181 StopListeningForVisibilityEvents();
184 mTransaction.reset();
185 Unfollow();
188 void WebAuthnManager::CancelParent() {
189 if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
190 mChild->SendRequestCancel(mTransaction.ref().mId);
194 void WebAuthnManager::HandleVisibilityChange() {
195 if (mTransaction.isSome()) {
196 mTransaction.ref().mVisibilityChanged = true;
200 WebAuthnManager::~WebAuthnManager() {
201 MOZ_ASSERT(NS_IsMainThread());
203 if (mTransaction.isSome()) {
204 ClearTransaction();
207 if (mChild) {
208 RefPtr<WebAuthnTransactionChild> c;
209 mChild.swap(c);
210 c->Disconnect();
214 already_AddRefed<Promise> WebAuthnManager::MakeCredential(
215 const PublicKeyCredentialCreationOptions& aOptions,
216 const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) {
217 MOZ_ASSERT(NS_IsMainThread());
219 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
221 RefPtr<Promise> promise = Promise::Create(global, aError);
222 if (aError.Failed()) {
223 return nullptr;
226 if (mTransaction.isSome()) {
227 // If there hasn't been a visibility change during the current
228 // transaction, then let's let that one complete rather than
229 // cancelling it on a subsequent call.
230 if (!mTransaction.ref().mVisibilityChanged) {
231 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
232 return promise.forget();
235 // Otherwise, the user may well have clicked away, so let's
236 // abort the old transaction and take over control from here.
237 CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
240 nsString origin;
241 nsCString rpId;
242 nsresult rv = GetOrigin(mParent, origin, rpId);
243 if (NS_WARN_IF(NS_FAILED(rv))) {
244 promise->MaybeReject(rv);
245 return promise.forget();
248 // Enforce 5.4.3 User Account Parameters for Credential Generation
249 // When we add UX, we'll want to do more with this value, but for now
250 // we just have to verify its correctness.
252 CryptoBuffer userId;
253 userId.Assign(aOptions.mUser.mId);
254 if (userId.Length() > 64) {
255 promise->MaybeRejectWithTypeError("user.id is too long");
256 return promise.forget();
259 // If timeoutSeconds was specified, check if its value lies within a
260 // reasonable range as defined by the platform and if not, correct it to the
261 // closest value lying within that range.
263 uint32_t adjustedTimeout = 30000;
264 if (aOptions.mTimeout.WasPassed()) {
265 adjustedTimeout = aOptions.mTimeout.Value();
266 adjustedTimeout = std::max(15000u, adjustedTimeout);
267 adjustedTimeout = std::min(120000u, adjustedTimeout);
270 if (aOptions.mRp.mId.WasPassed()) {
271 // If rpId is specified, then invoke the procedure used for relaxing the
272 // same-origin restriction by setting the document.domain attribute, using
273 // rpId as the given value but without changing the current document’s
274 // domain. If no errors are thrown, set rpId to the value of host as
275 // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
276 // Otherwise, reject promise with a DOMException whose name is
277 // "SecurityError", and terminate this algorithm.
279 if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRp.mId.Value(), rpId))) {
280 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
281 return promise.forget();
285 // <https://w3c.github.io/webauthn/#sctn-appid-extension>
286 if (aOptions.mExtensions.mAppid.WasPassed()) {
287 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
288 return promise.forget();
291 // Process each element of mPubKeyCredParams using the following steps, to
292 // produce a new sequence of coseAlgos.
293 nsTArray<CoseAlg> coseAlgos;
294 // If pubKeyCredParams is empty, append ES256 and RS256
295 if (aOptions.mPubKeyCredParams.IsEmpty()) {
296 coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::ES256));
297 coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::RS256));
298 } else {
299 for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) {
300 // If current.type does not contain a PublicKeyCredentialType
301 // supported by this implementation, then stop processing current and move
302 // on to the next element in mPubKeyCredParams.
303 if (!aOptions.mPubKeyCredParams[a].mType.EqualsLiteral(
304 MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) {
305 continue;
308 coseAlgos.AppendElement(aOptions.mPubKeyCredParams[a].mAlg);
312 // If there are algorithms specified, but none are Public_key algorithms,
313 // reject the promise.
314 if (coseAlgos.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
315 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
316 return promise.forget();
319 // If excludeList is undefined, set it to the empty list.
321 // If extensions was specified, process any extensions supported by this
322 // client platform, to produce the extension data that needs to be sent to the
323 // authenticator. If an error is encountered while processing an extension,
324 // skip that extension and do not produce any extension data for it. Call the
325 // result of this processing clientExtensions.
327 // Currently no extensions are supported
329 // Use attestationChallenge, callerOrigin and rpId, along with the token
330 // binding key associated with callerOrigin (if any), to create a ClientData
331 // structure representing this request. Choose a hash algorithm for hashAlg
332 // and compute the clientDataJSON and clientDataHash.
334 CryptoBuffer challenge;
335 if (!challenge.Assign(aOptions.mChallenge)) {
336 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
337 return promise.forget();
340 nsAutoCString clientDataJSON;
341 nsresult srv = AssembleClientData(origin, challenge, u"webauthn.create"_ns,
342 aOptions.mExtensions, clientDataJSON);
343 if (NS_WARN_IF(NS_FAILED(srv))) {
344 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
345 return promise.forget();
348 nsTArray<WebAuthnScopedCredential> excludeList;
349 for (const auto& s : aOptions.mExcludeCredentials) {
350 WebAuthnScopedCredential c;
351 CryptoBuffer cb;
352 cb.Assign(s.mId);
353 c.id() = cb;
354 excludeList.AppendElement(c);
357 if (!MaybeCreateBackgroundActor()) {
358 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
359 return promise.forget();
362 // TODO: Add extension list building
363 nsTArray<WebAuthnExtension> extensions;
365 // <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension>
366 if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) {
367 bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value();
368 if (hmacCreateSecret) {
369 extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret));
373 if (aOptions.mExtensions.mCredProps.WasPassed()) {
374 bool credProps = aOptions.mExtensions.mCredProps.Value();
375 if (credProps) {
376 extensions.AppendElement(WebAuthnExtensionCredProps(credProps));
380 if (aOptions.mExtensions.mMinPinLength.WasPassed()) {
381 bool minPinLength = aOptions.mExtensions.mMinPinLength.Value();
382 if (minPinLength) {
383 extensions.AppendElement(WebAuthnExtensionMinPinLength(minPinLength));
387 const auto& selection = aOptions.mAuthenticatorSelection;
388 const auto& attachment = selection.mAuthenticatorAttachment;
389 const nsString& attestation = aOptions.mAttestation;
391 // Attachment
392 Maybe<nsString> authenticatorAttachment;
393 if (attachment.WasPassed()) {
394 authenticatorAttachment.emplace(attachment.Value());
397 // The residentKey field was added in WebAuthn level 2. It takes precedent
398 // over the requireResidentKey field if and only if it is present and it is a
399 // member of the ResidentKeyRequirement enum.
400 static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2);
401 bool useResidentKeyValue =
402 selection.mResidentKey.WasPassed() &&
403 (selection.mResidentKey.Value().EqualsLiteral(
404 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED) ||
405 selection.mResidentKey.Value().EqualsLiteral(
406 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED) ||
407 selection.mResidentKey.Value().EqualsLiteral(
408 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED));
410 nsString residentKey;
411 if (useResidentKeyValue) {
412 residentKey = selection.mResidentKey.Value();
413 } else {
414 // "If no value is given then the effective value is required if
415 // requireResidentKey is true or discouraged if it is false or absent."
416 if (selection.mRequireResidentKey) {
417 residentKey.AssignLiteral(MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED);
418 } else {
419 residentKey.AssignLiteral(
420 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED);
424 // Create and forward authenticator selection criteria.
425 WebAuthnAuthenticatorSelection authSelection(
426 residentKey, selection.mUserVerification, authenticatorAttachment);
428 WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName);
430 WebAuthnMakeCredentialUserInfo userInfo(userId, aOptions.mUser.mName,
431 aOptions.mUser.mDisplayName);
433 BrowsingContext* context = mParent->GetBrowsingContext();
434 if (!context) {
435 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
436 return promise.forget();
439 // Abort the request if aborted flag is already set.
440 if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
441 AutoJSAPI jsapi;
442 if (!jsapi.Init(global)) {
443 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
444 return promise.forget();
446 JSContext* cx = jsapi.cx();
447 JS::Rooted<JS::Value> reason(cx);
448 aSignal.Value().GetReason(cx, &reason);
449 promise->MaybeReject(reason);
450 return promise.forget();
453 WebAuthnMakeCredentialInfo info(
454 origin, NS_ConvertUTF8toUTF16(rpId), challenge, clientDataJSON,
455 adjustedTimeout, excludeList, rpInfo, userInfo, coseAlgos, extensions,
456 authSelection, attestation, context->Top()->Id());
458 // Set up the transaction state (including event listeners, etc). Fallible
459 // operations should not be performed below this line, as we must not leave
460 // the transaction state partially initialized. Once the transaction state is
461 // initialized the only valid ways to end the transaction are
462 // CancelTransaction, RejectTransaction, and FinishMakeCredential.
463 #ifdef XP_WIN
464 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
465 ListenForVisibilityEvents();
467 #else
468 ListenForVisibilityEvents();
469 #endif
471 AbortSignal* signal = nullptr;
472 if (aSignal.WasPassed()) {
473 signal = &aSignal.Value();
474 Follow(signal);
477 MOZ_ASSERT(mTransaction.isNothing());
478 mTransaction = Some(WebAuthnTransaction(promise));
479 mChild->SendRequestRegister(mTransaction.ref().mId, info);
481 return promise.forget();
484 const size_t MAX_ALLOWED_CREDENTIALS = 20;
486 already_AddRefed<Promise> WebAuthnManager::GetAssertion(
487 const PublicKeyCredentialRequestOptions& aOptions,
488 const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) {
489 MOZ_ASSERT(NS_IsMainThread());
491 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
493 RefPtr<Promise> promise = Promise::Create(global, aError);
494 if (aError.Failed()) {
495 return nullptr;
498 if (mTransaction.isSome()) {
499 // If there hasn't been a visibility change during the current
500 // transaction, then let's let that one complete rather than
501 // cancelling it on a subsequent call.
502 if (!mTransaction.ref().mVisibilityChanged) {
503 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
504 return promise.forget();
507 // Otherwise, the user may well have clicked away, so let's
508 // abort the old transaction and take over control from here.
509 CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
512 nsString origin;
513 nsCString rpId;
514 nsresult rv = GetOrigin(mParent, origin, rpId);
515 if (NS_WARN_IF(NS_FAILED(rv))) {
516 promise->MaybeReject(rv);
517 return promise.forget();
520 // If timeoutSeconds was specified, check if its value lies within a
521 // reasonable range as defined by the platform and if not, correct it to the
522 // closest value lying within that range.
524 uint32_t adjustedTimeout = 30000;
525 if (aOptions.mTimeout.WasPassed()) {
526 adjustedTimeout = aOptions.mTimeout.Value();
527 adjustedTimeout = std::max(15000u, adjustedTimeout);
528 adjustedTimeout = std::min(120000u, adjustedTimeout);
531 if (aOptions.mRpId.WasPassed()) {
532 // If rpId is specified, then invoke the procedure used for relaxing the
533 // same-origin restriction by setting the document.domain attribute, using
534 // rpId as the given value but without changing the current document’s
535 // domain. If no errors are thrown, set rpId to the value of host as
536 // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
537 // Otherwise, reject promise with a DOMException whose name is
538 // "SecurityError", and terminate this algorithm.
540 if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRpId.Value(), rpId))) {
541 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
542 return promise.forget();
546 // Abort the request if the allowCredentials set is too large
547 if (aOptions.mAllowCredentials.Length() > MAX_ALLOWED_CREDENTIALS) {
548 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
549 return promise.forget();
552 // Use assertionChallenge, callerOrigin and rpId, along with the token binding
553 // key associated with callerOrigin (if any), to create a ClientData structure
554 // representing this request. Choose a hash algorithm for hashAlg and compute
555 // the clientDataJSON and clientDataHash.
556 CryptoBuffer challenge;
557 if (!challenge.Assign(aOptions.mChallenge)) {
558 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
559 return promise.forget();
562 nsAutoCString clientDataJSON;
563 rv = AssembleClientData(origin, challenge, u"webauthn.get"_ns,
564 aOptions.mExtensions, clientDataJSON);
565 if (NS_WARN_IF(NS_FAILED(rv))) {
566 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
567 return promise.forget();
570 nsTArray<WebAuthnScopedCredential> allowList;
571 for (const auto& s : aOptions.mAllowCredentials) {
572 if (s.mType.EqualsLiteral(
573 MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) {
574 WebAuthnScopedCredential c;
575 CryptoBuffer cb;
576 cb.Assign(s.mId);
577 c.id() = cb;
579 // Serialize transports.
580 if (s.mTransports.WasPassed()) {
581 uint8_t transports = 0;
583 // We ignore unknown transports for forward-compatibility, but this
584 // needs to be reviewed if values are added to the
585 // AuthenticatorTransport enum.
586 static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2);
587 for (const nsAString& str : s.mTransports.Value()) {
588 if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_USB)) {
589 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB;
590 } else if (str.EqualsLiteral(
591 MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_NFC)) {
592 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC;
593 } else if (str.EqualsLiteral(
594 MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_BLE)) {
595 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE;
596 } else if (str.EqualsLiteral(
597 MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_INTERNAL)) {
598 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL;
601 c.transports() = transports;
604 allowList.AppendElement(c);
607 if (allowList.Length() == 0 && aOptions.mAllowCredentials.Length() != 0) {
608 promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
609 return promise.forget();
612 if (!MaybeCreateBackgroundActor()) {
613 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
614 return promise.forget();
617 // If extensions were specified, process any extensions supported by this
618 // client platform, to produce the extension data that needs to be sent to the
619 // authenticator. If an error is encountered while processing an extension,
620 // skip that extension and do not produce any extension data for it. Call the
621 // result of this processing clientExtensions.
622 nsTArray<WebAuthnExtension> extensions;
624 // credProps is only supported in MakeCredentials
625 if (aOptions.mExtensions.mCredProps.WasPassed()) {
626 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
627 return promise.forget();
630 // minPinLength is only supported in MakeCredentials
631 if (aOptions.mExtensions.mMinPinLength.WasPassed()) {
632 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
633 return promise.forget();
636 // <https://w3c.github.io/webauthn/#sctn-appid-extension>
637 if (aOptions.mExtensions.mAppid.WasPassed()) {
638 nsString appId(aOptions.mExtensions.mAppid.Value());
640 // Check that the appId value is allowed.
641 if (!EvaluateAppID(mParent, origin, appId)) {
642 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
643 return promise.forget();
646 // Append the hash and send it to the backend.
647 extensions.AppendElement(WebAuthnExtensionAppId(appId));
650 BrowsingContext* context = mParent->GetBrowsingContext();
651 if (!context) {
652 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
653 return promise.forget();
656 // Abort the request if aborted flag is already set.
657 if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
658 AutoJSAPI jsapi;
659 if (!jsapi.Init(global)) {
660 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
661 return promise.forget();
663 JSContext* cx = jsapi.cx();
664 JS::Rooted<JS::Value> reason(cx);
665 aSignal.Value().GetReason(cx, &reason);
666 promise->MaybeReject(reason);
667 return promise.forget();
670 WebAuthnGetAssertionInfo info(origin, NS_ConvertUTF8toUTF16(rpId), challenge,
671 clientDataJSON, adjustedTimeout, allowList,
672 extensions, aOptions.mUserVerification,
673 context->Top()->Id());
675 // Set up the transaction state (including event listeners, etc). Fallible
676 // operations should not be performed below this line, as we must not leave
677 // the transaction state partially initialized. Once the transaction state is
678 // initialized the only valid ways to end the transaction are
679 // CancelTransaction, RejectTransaction, and FinishGetAssertion.
680 #ifdef XP_WIN
681 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
682 ListenForVisibilityEvents();
684 #else
685 ListenForVisibilityEvents();
686 #endif
688 AbortSignal* signal = nullptr;
689 if (aSignal.WasPassed()) {
690 signal = &aSignal.Value();
691 Follow(signal);
694 MOZ_ASSERT(mTransaction.isNothing());
695 mTransaction = Some(WebAuthnTransaction(promise));
696 mChild->SendRequestSign(mTransaction.ref().mId, info);
698 return promise.forget();
701 already_AddRefed<Promise> WebAuthnManager::Store(const Credential& aCredential,
702 ErrorResult& aError) {
703 MOZ_ASSERT(NS_IsMainThread());
705 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
707 RefPtr<Promise> promise = Promise::Create(global, aError);
708 if (aError.Failed()) {
709 return nullptr;
712 if (mTransaction.isSome()) {
713 // If there hasn't been a visibility change during the current
714 // transaction, then let's let that one complete rather than
715 // cancelling it on a subsequent call.
716 if (!mTransaction.ref().mVisibilityChanged) {
717 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
718 return promise.forget();
721 // Otherwise, the user may well have clicked away, so let's
722 // abort the old transaction and take over control from here.
723 CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
726 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
727 return promise.forget();
730 void WebAuthnManager::FinishMakeCredential(
731 const uint64_t& aTransactionId,
732 const WebAuthnMakeCredentialResult& aResult) {
733 MOZ_ASSERT(NS_IsMainThread());
735 // Check for a valid transaction.
736 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
737 return;
740 nsAutoCString keyHandleBase64Url;
741 nsresult rv = Base64URLEncode(
742 aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(),
743 Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url);
744 if (NS_WARN_IF(NS_FAILED(rv))) {
745 RejectTransaction(rv);
746 return;
749 // Create a new PublicKeyCredential object and populate its fields with the
750 // values returned from the authenticator as well as the clientDataJSON
751 // computed earlier.
752 RefPtr<AuthenticatorAttestationResponse> attestation =
753 new AuthenticatorAttestationResponse(mParent);
754 attestation->SetClientDataJSON(aResult.ClientDataJSON());
755 attestation->SetAttestationObject(aResult.AttestationObject());
756 attestation->SetTransports(aResult.Transports());
758 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
759 credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
760 credential->SetType(u"public-key"_ns);
761 credential->SetRawId(aResult.KeyHandle());
762 credential->SetAttestationResponse(attestation);
763 credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
765 // Forward client extension results.
766 for (const auto& ext : aResult.Extensions()) {
767 if (ext.type() ==
768 WebAuthnExtensionResult::TWebAuthnExtensionResultCredProps) {
769 bool credPropsRk = ext.get_WebAuthnExtensionResultCredProps().rk();
770 credential->SetClientExtensionResultCredPropsRk(credPropsRk);
772 if (ext.type() ==
773 WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) {
774 bool hmacCreateSecret =
775 ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret();
776 credential->SetClientExtensionResultHmacSecret(hmacCreateSecret);
780 mTransaction.ref().mPromise->MaybeResolve(credential);
781 ClearTransaction();
784 void WebAuthnManager::FinishGetAssertion(
785 const uint64_t& aTransactionId, const WebAuthnGetAssertionResult& aResult) {
786 MOZ_ASSERT(NS_IsMainThread());
788 // Check for a valid transaction.
789 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
790 return;
793 nsAutoCString keyHandleBase64Url;
794 nsresult rv = Base64URLEncode(
795 aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(),
796 Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url);
797 if (NS_WARN_IF(NS_FAILED(rv))) {
798 RejectTransaction(rv);
799 return;
802 // Create a new PublicKeyCredential object named value and populate its fields
803 // with the values returned from the authenticator as well as the
804 // clientDataJSON computed earlier.
805 RefPtr<AuthenticatorAssertionResponse> assertion =
806 new AuthenticatorAssertionResponse(mParent);
807 assertion->SetClientDataJSON(aResult.ClientDataJSON());
808 assertion->SetAuthenticatorData(aResult.AuthenticatorData());
809 assertion->SetSignature(aResult.Signature());
810 assertion->SetUserHandle(aResult.UserHandle()); // may be empty
812 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
813 credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
814 credential->SetType(u"public-key"_ns);
815 credential->SetRawId(aResult.KeyHandle());
816 credential->SetAssertionResponse(assertion);
817 credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
819 // Forward client extension results.
820 for (const auto& ext : aResult.Extensions()) {
821 if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
822 bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
823 credential->SetClientExtensionResultAppId(appid);
827 mTransaction.ref().mPromise->MaybeResolve(credential);
828 ClearTransaction();
831 void WebAuthnManager::RequestAborted(const uint64_t& aTransactionId,
832 const nsresult& aError) {
833 MOZ_ASSERT(NS_IsMainThread());
835 if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
836 RejectTransaction(aError);
840 void WebAuthnManager::RunAbortAlgorithm() {
841 if (NS_WARN_IF(mTransaction.isNothing())) {
842 return;
845 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
847 AutoJSAPI jsapi;
848 if (!jsapi.Init(global)) {
849 CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
850 return;
852 JSContext* cx = jsapi.cx();
853 JS::Rooted<JS::Value> reason(cx);
854 Signal()->GetReason(cx, &reason);
855 CancelTransaction(reason);
858 } // namespace mozilla::dom