AuthenticatorCoordinator.cpp [plain text]
#include "config.h"
#include "AuthenticatorCoordinator.h"
#if ENABLE(WEB_AUTHN)
#include "AbortSignal.h"
#include "AuthenticatorAssertionResponse.h"
#include "AuthenticatorAttestationResponse.h"
#include "AuthenticatorCoordinatorClient.h"
#include "AuthenticatorResponseData.h"
#include "Document.h"
#include "JSBasicCredential.h"
#include "JSDOMPromiseDeferred.h"
#include "PublicKeyCredential.h"
#include "PublicKeyCredentialCreationOptions.h"
#include "PublicKeyCredentialRequestOptions.h"
#include "RegistrableDomain.h"
#include "LegacySchemeRegistry.h"
#include "SecurityOrigin.h"
#include "WebAuthenticationConstants.h"
#include <pal/crypto/CryptoDigest.h>
#include <wtf/JSONValues.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/text/Base64.h>
namespace WebCore {
namespace AuthenticatorCoordinatorInternal {
static Ref<ArrayBuffer> produceClientDataJson(ClientDataType type, const BufferSource& challenge, const SecurityOrigin& origin)
{
auto object = JSON::Object::create();
switch (type) {
case ClientDataType::Create:
object->setString("type"_s, "webauthn.create"_s);
break;
case ClientDataType::Get:
object->setString("type"_s, "webauthn.get"_s);
break;
}
object->setString("challenge"_s, WTF::base64URLEncode(challenge.data(), challenge.length()));
object->setString("origin"_s, origin.toRawString());
auto utf8JSONString = object->toJSONString().utf8();
return ArrayBuffer::create(utf8JSONString.data(), utf8JSONString.length());
}
static Vector<uint8_t> produceClientDataJsonHash(const ArrayBuffer& clientDataJson)
{
auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
crypto->addBytes(clientDataJson.data(), clientDataJson.byteLength());
return crypto->computeHash();
}
static bool needsAppIdQuirks(const String& host, const String& appId)
{
if (equalLettersIgnoringASCIICase(host, "google.com") || host.endsWithIgnoringASCIICase(".google.com"))
return (appId == "https://www.gstatic.com/securitykey/origins.json"_s) || (appId == "https://www.gstatic.com/securitykey/a/google.com/origins.json"_s);
return false;
}
static String processAppIdExtension(const SecurityOrigin& facetId, const String& appId)
{
ASSERT(LegacySchemeRegistry::shouldTreatURLSchemeAsSecure(facetId.protocol()));
if (appId.isEmpty())
return facetId.toString();
URL appIdURL(URL(), appId);
if (!appIdURL.isValid() || facetId.protocol() != appIdURL.protocol() || (RegistrableDomain(appIdURL) != RegistrableDomain::uncheckedCreateFromHost(facetId.host()) && !needsAppIdQuirks(facetId.host(), appId)))
return String();
return appId;
}
static bool processGoogleLegacyAppIdSupportExtension(const Optional<AuthenticationExtensionsClientInputs>& extensions, const String& rpId)
{
if (rpId != "google.com"_s)
return false;
if (!extensions)
return true;
return extensions->googleLegacyAppidSupport;
}
}
AuthenticatorCoordinator::AuthenticatorCoordinator(std::unique_ptr<AuthenticatorCoordinatorClient>&& client)
: m_client(WTFMove(client))
{
}
void AuthenticatorCoordinator::setClient(std::unique_ptr<AuthenticatorCoordinatorClient>&& client)
{
m_client = WTFMove(client);
}
void AuthenticatorCoordinator::create(const Document& document, const PublicKeyCredentialCreationOptions& options, bool sameOriginWithAncestors, RefPtr<AbortSignal>&& abortSignal, CredentialPromise&& promise) const
{
using namespace AuthenticatorCoordinatorInternal;
const auto& callerOrigin = document.securityOrigin();
auto* frame = document.frame();
ASSERT(frame);
if (!sameOriginWithAncestors) {
promise.reject(Exception { NotAllowedError, "The origin of the document is not the same as its ancestors."_s });
return;
}
if (URL::hostIsIPAddress(callerOrigin.domain())) {
promise.reject(Exception { SecurityError, "The effective domain of the document is not a valid domain."_s });
return;
}
if (!options.rp.id.isEmpty() && !callerOrigin.isMatchingRegistrableDomainSuffix(options.rp.id)) {
promise.reject(Exception { SecurityError, "The provided RP ID is not a registrable domain suffix of the effective domain of the document."_s });
return;
}
if (options.rp.id.isEmpty())
options.rp.id = callerOrigin.domain();
if (options.pubKeyCredParams.isEmpty()) {
promise.reject(Exception { NotSupportedError, "No desired properties of the to be created credential are provided."_s });
return;
}
options.extensions = AuthenticationExtensionsClientInputs { String(), processGoogleLegacyAppIdSupportExtension(options.extensions, options.rp.id) };
auto clientDataJson = produceClientDataJson(ClientDataType::Create, options.challenge, callerOrigin);
auto clientDataJsonHash = produceClientDataJsonHash(clientDataJson);
if (!m_client) {
promise.reject(Exception { UnknownError, "Unknown internal error."_s });
return;
}
auto callback = [clientDataJson = WTFMove(clientDataJson), promise = WTFMove(promise), abortSignal = WTFMove(abortSignal)] (AuthenticatorResponseData&& data, ExceptionData&& exception) mutable {
if (abortSignal && abortSignal->aborted()) {
promise.reject(Exception { AbortError, "Aborted by AbortSignal."_s });
return;
}
if (auto response = AuthenticatorResponse::tryCreate(WTFMove(data))) {
response->setClientDataJSON(WTFMove(clientDataJson));
promise.resolve(PublicKeyCredential::create(response.releaseNonNull()).ptr());
return;
}
ASSERT(!exception.message.isNull());
promise.reject(exception.toException());
};
m_client->makeCredential(*frame, callerOrigin, clientDataJsonHash, options, WTFMove(callback));
}
void AuthenticatorCoordinator::discoverFromExternalSource(const Document& document, const PublicKeyCredentialRequestOptions& options, bool sameOriginWithAncestors, RefPtr<AbortSignal>&& abortSignal, CredentialPromise&& promise) const
{
using namespace AuthenticatorCoordinatorInternal;
auto& callerOrigin = document.securityOrigin();
auto* frame = document.frame();
ASSERT(frame);
if (!sameOriginWithAncestors) {
promise.reject(Exception { NotAllowedError, "The origin of the document is not the same as its ancestors."_s });
return;
}
if (URL::hostIsIPAddress(callerOrigin.domain())) {
promise.reject(Exception { SecurityError, "The effective domain of the document is not a valid domain."_s });
return;
}
if (!options.rpId.isEmpty() && !callerOrigin.isMatchingRegistrableDomainSuffix(options.rpId)) {
promise.reject(Exception { SecurityError, "The provided RP ID is not a registrable domain suffix of the effective domain of the document."_s });
return;
}
if (options.rpId.isEmpty())
options.rpId = callerOrigin.domain();
if (options.extensions && !options.extensions->appid.isNull()) {
auto appid = processAppIdExtension(callerOrigin, options.extensions->appid);
if (!appid) {
promise.reject(Exception { SecurityError, "The origin of the document is not authorized for the provided App ID."_s });
return;
}
options.extensions->appid = appid;
}
auto clientDataJson = produceClientDataJson(ClientDataType::Get, options.challenge, callerOrigin);
auto clientDataJsonHash = produceClientDataJsonHash(clientDataJson);
if (!m_client) {
promise.reject(Exception { UnknownError, "Unknown internal error."_s });
return;
}
auto callback = [clientDataJson = WTFMove(clientDataJson), promise = WTFMove(promise), abortSignal = WTFMove(abortSignal)] (AuthenticatorResponseData&& data, ExceptionData&& exception) mutable {
if (abortSignal && abortSignal->aborted()) {
promise.reject(Exception { AbortError, "Aborted by AbortSignal."_s });
return;
}
if (auto response = AuthenticatorResponse::tryCreate(WTFMove(data))) {
response->setClientDataJSON(WTFMove(clientDataJson));
promise.resolve(PublicKeyCredential::create(response.releaseNonNull()).ptr());
return;
}
ASSERT(!exception.message.isNull());
promise.reject(exception.toException());
};
m_client->getAssertion(*frame, callerOrigin, clientDataJsonHash, options, WTFMove(callback));
}
void AuthenticatorCoordinator::isUserVerifyingPlatformAuthenticatorAvailable(DOMPromiseDeferred<IDLBoolean>&& promise) const
{
if (!m_client) {
promise.reject(Exception { UnknownError, "Unknown internal error."_s });
return;
}
auto completionHandler = [promise = WTFMove(promise)] (bool result) mutable {
promise.resolve(result);
};
m_client->isUserVerifyingPlatformAuthenticatorAvailable(WTFMove(completionHandler));
}
void AuthenticatorCoordinator::resetUserGestureRequirement()
{
m_client->resetUserGestureRequirement();
}
}
#endif // ENABLE(WEB_AUTHN)