AuthenticatorManager.cpp [plain text]
#include "config.h"
#include "AuthenticatorManager.h"
#if ENABLE(WEB_AUTHN)
#include "APIUIClient.h"
#include "APIWebAuthenticationPanel.h"
#include "APIWebAuthenticationPanelClient.h"
#include "AuthenticatorPresenterCoordinator.h"
#include "LocalService.h"
#include "NfcService.h"
#include "WebPageProxy.h"
#include "WebPreferencesKeys.h"
#include "WebProcessProxy.h"
#include <WebCore/AuthenticatorAssertionResponse.h>
#include <WebCore/AuthenticatorTransport.h>
#include <WebCore/PublicKeyCredentialCreationOptions.h>
#include <WebCore/WebAuthenticationConstants.h>
#include <wtf/MonotonicTime.h>
namespace WebKit {
using namespace WebCore;
namespace {
const unsigned maxTimeOutValue = 120000;
static AuthenticatorManager::TransportSet collectTransports(const Optional<PublicKeyCredentialCreationOptions::AuthenticatorSelectionCriteria>& authenticatorSelection)
{
AuthenticatorManager::TransportSet result;
if (!authenticatorSelection || !authenticatorSelection->authenticatorAttachment) {
auto addResult = result.add(AuthenticatorTransport::Internal);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
addResult = result.add(AuthenticatorTransport::Usb);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
addResult = result.add(AuthenticatorTransport::Nfc);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
return result;
}
if (authenticatorSelection->authenticatorAttachment == PublicKeyCredentialCreationOptions::AuthenticatorAttachment::Platform) {
auto addResult = result.add(AuthenticatorTransport::Internal);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
return result;
}
if (authenticatorSelection->authenticatorAttachment == PublicKeyCredentialCreationOptions::AuthenticatorAttachment::CrossPlatform) {
auto addResult = result.add(AuthenticatorTransport::Usb);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
addResult = result.add(AuthenticatorTransport::Nfc);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
return result;
}
ASSERT_NOT_REACHED();
return result;
}
static AuthenticatorManager::TransportSet collectTransports(const Vector<PublicKeyCredentialDescriptor>& allowCredentials)
{
AuthenticatorManager::TransportSet result;
if (allowCredentials.isEmpty()) {
auto addResult = result.add(AuthenticatorTransport::Internal);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
addResult = result.add(AuthenticatorTransport::Usb);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
addResult = result.add(AuthenticatorTransport::Nfc);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
return result;
}
for (auto& allowCredential : allowCredentials) {
if (allowCredential.transports.isEmpty()) {
result.add(AuthenticatorTransport::Internal);
result.add(AuthenticatorTransport::Usb);
result.add(AuthenticatorTransport::Nfc);
return result;
}
for (const auto& transport : allowCredential.transports) {
if (transport == AuthenticatorTransport::Ble)
continue;
result.add(transport);
if (result.size() >= AuthenticatorManager::maxTransportNumber)
return result;
}
}
ASSERT(result.size() < AuthenticatorManager::maxTransportNumber);
return result;
}
static void processGoogleLegacyAppIdSupportExtension(const Optional<AuthenticationExtensionsClientInputs>& extensions, AuthenticatorManager::TransportSet& transports)
{
if (!extensions) {
ASSERT_NOT_REACHED();
return;
}
if (!extensions->googleLegacyAppidSupport)
return;
transports.remove(AuthenticatorTransport::Internal);
}
static String getRpId(const Variant<PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions>& options)
{
if (WTF::holds_alternative<PublicKeyCredentialCreationOptions>(options))
return WTF::get<PublicKeyCredentialCreationOptions>(options).rp.id;
return WTF::get<PublicKeyCredentialRequestOptions>(options).rpId;
}
}
const size_t AuthenticatorManager::maxTransportNumber = 3;
AuthenticatorManager::AuthenticatorManager()
: m_requestTimeOutTimer(RunLoop::main(), this, &AuthenticatorManager::timeOutTimerFired)
{
}
void AuthenticatorManager::handleRequest(WebAuthenticationRequestData&& data, Callback&& callback)
{
if (m_pendingCompletionHandler) {
invokePendingCompletionHandler(ExceptionData { NotAllowedError, "This request has been cancelled by a new request."_s });
m_requestTimeOutTimer.stop();
}
clearState();
m_pendingRequestData = WTFMove(data);
m_pendingCompletionHandler = WTFMove(callback);
initTimeOutTimer();
if (m_mode == Mode::Compatible) {
runPanel();
return;
}
runPresenter();
}
void AuthenticatorManager::cancelRequest(const PageIdentifier& pageID, const Optional<FrameIdentifier>& frameID)
{
if (!m_pendingCompletionHandler)
return;
if (auto pendingFrameID = m_pendingRequestData.frameID) {
if (pendingFrameID->pageID != pageID)
return;
if (frameID && frameID != pendingFrameID->frameID)
return;
}
cancelRequest();
}
void AuthenticatorManager::cancelRequest(const API::WebAuthenticationPanel& panel)
{
RELEASE_ASSERT(RunLoop::isMain());
if (!m_pendingCompletionHandler || m_pendingRequestData.panel.get() != &panel)
return;
cancelRequest();
}
void AuthenticatorManager::cancel()
{
RELEASE_ASSERT(RunLoop::isMain());
if (!m_pendingCompletionHandler)
return;
cancelRequest();
}
void AuthenticatorManager::enableModernWebAuthentication()
{
m_mode = Mode::Modern;
}
void AuthenticatorManager::enableNativeSupport()
{
m_mode = Mode::Native;
}
void AuthenticatorManager::clearStateAsync()
{
RunLoop::main().dispatch([weakThis = makeWeakPtr(*this)] {
if (!weakThis)
return;
weakThis->clearState();
});
}
void AuthenticatorManager::clearState()
{
if (m_pendingCompletionHandler)
return;
m_authenticators.clear();
m_services.clear();
m_pendingRequestData = { };
m_presenter = nullptr;
}
void AuthenticatorManager::authenticatorAdded(Ref<Authenticator>&& authenticator)
{
ASSERT(RunLoop::isMain());
authenticator->setObserver(*this);
authenticator->handleRequest(m_pendingRequestData);
authenticator->setWebAuthenticationModernEnabled(m_mode != Mode::Compatible);
auto addResult = m_authenticators.add(WTFMove(authenticator));
ASSERT_UNUSED(addResult, addResult.isNewEntry);
}
void AuthenticatorManager::serviceStatusUpdated(WebAuthenticationStatus status)
{
if (m_presenter) {
m_presenter->updatePresenter(status);
return;
}
dispatchPanelClientCall([status] (const API::WebAuthenticationPanel& panel) {
panel.client().updatePanel(status);
});
}
void AuthenticatorManager::respondReceived(Respond&& respond)
{
ASSERT(RunLoop::isMain());
if (!m_requestTimeOutTimer.isActive())
return;
ASSERT(m_pendingCompletionHandler);
auto shouldComplete = WTF::holds_alternative<Ref<AuthenticatorResponse>>(respond);
if (!shouldComplete)
shouldComplete = WTF::get<ExceptionData>(respond).code == InvalidStateError;
if (shouldComplete) {
invokePendingCompletionHandler(WTFMove(respond));
clearStateAsync();
m_requestTimeOutTimer.stop();
return;
}
respondReceivedInternal(WTFMove(respond));
restartDiscovery();
}
void AuthenticatorManager::downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator)
{
RunLoop::main().dispatch([weakThis = makeWeakPtr(*this), id] {
if (!weakThis)
return;
auto removed = weakThis->m_authenticators.remove(id);
ASSERT_UNUSED(removed, removed);
});
authenticatorAdded(WTFMove(downgradedAuthenticator));
}
void AuthenticatorManager::authenticatorStatusUpdated(WebAuthenticationStatus status)
{
m_pendingRequestData.cachedPin = String();
if (m_presenter) {
m_presenter->updatePresenter(status);
return;
}
dispatchPanelClientCall([status] (const API::WebAuthenticationPanel& panel) {
panel.client().updatePanel(status);
});
}
void AuthenticatorManager::requestPin(uint64_t retries, CompletionHandler<void(const WTF::String&)>&& completionHandler)
{
if (!m_pendingRequestData.cachedPin.isNull()) {
completionHandler(m_pendingRequestData.cachedPin);
m_pendingRequestData.cachedPin = String();
return;
}
auto callback = [weakThis = makeWeakPtr(*this), this, completionHandler = WTFMove(completionHandler)] (const WTF::String& pin) mutable {
if (!weakThis)
return;
m_pendingRequestData.cachedPin = pin;
completionHandler(pin);
};
if (m_presenter) {
m_presenter->requestPin(retries, WTFMove(callback));
return;
}
dispatchPanelClientCall([retries, callback = WTFMove(callback)] (const API::WebAuthenticationPanel& panel) mutable {
panel.client().requestPin(retries, WTFMove(callback));
});
}
void AuthenticatorManager::selectAssertionResponse(Vector<Ref<WebCore::AuthenticatorAssertionResponse>>&& responses, WebAuthenticationSource source, CompletionHandler<void(AuthenticatorAssertionResponse*)>&& completionHandler)
{
if (m_presenter) {
m_presenter->selectAssertionResponse(WTFMove(responses), source, WTFMove(completionHandler));
return;
}
dispatchPanelClientCall([responses = WTFMove(responses), source, completionHandler = WTFMove(completionHandler)] (const API::WebAuthenticationPanel& panel) mutable {
panel.client().selectAssertionResponse(WTFMove(responses), source, WTFMove(completionHandler));
});
}
void AuthenticatorManager::decidePolicyForLocalAuthenticator(CompletionHandler<void(LocalAuthenticatorPolicy)>&& completionHandler)
{
dispatchPanelClientCall([completionHandler = WTFMove(completionHandler)] (const API::WebAuthenticationPanel& panel) mutable {
panel.client().decidePolicyForLocalAuthenticator(WTFMove(completionHandler));
});
}
void AuthenticatorManager::requestLAContextForUserVerification(CompletionHandler<void(LAContext *)>&& completionHandler)
{
if (m_presenter) {
m_presenter->requestLAContextForUserVerification(WTFMove(completionHandler));
return;
}
dispatchPanelClientCall([completionHandler = WTFMove(completionHandler)] (const API::WebAuthenticationPanel& panel) mutable {
panel.client().requestLAContextForUserVerification(WTFMove(completionHandler));
});
}
void AuthenticatorManager::cancelRequest()
{
invokePendingCompletionHandler(ExceptionData { NotAllowedError, "This request has been cancelled by the user."_s });
clearState();
m_requestTimeOutTimer.stop();
}
UniqueRef<AuthenticatorTransportService> AuthenticatorManager::createService(AuthenticatorTransport transport, AuthenticatorTransportService::Observer& observer) const
{
return AuthenticatorTransportService::create(transport, observer);
}
void AuthenticatorManager::filterTransports(TransportSet& transports) const
{
if (!NfcService::isAvailable())
transports.remove(AuthenticatorTransport::Nfc);
if (!LocalService::isAvailable())
transports.remove(AuthenticatorTransport::Internal);
if (!m_pendingRequestData.processingUserGesture)
transports.clear();
}
void AuthenticatorManager::startDiscovery(const TransportSet& transports)
{
ASSERT(RunLoop::isMain());
ASSERT(m_services.isEmpty() && transports.size() <= maxTransportNumber);
for (auto& transport : transports) {
auto service = createService(transport, *this);
service->startDiscovery();
m_services.append(WTFMove(service));
}
}
void AuthenticatorManager::initTimeOutTimer()
{
Optional<unsigned> timeOutInMs;
WTF::switchOn(m_pendingRequestData.options, [&](const PublicKeyCredentialCreationOptions& options) {
timeOutInMs = options.timeout;
}, [&](const PublicKeyCredentialRequestOptions& options) {
timeOutInMs = options.timeout;
});
unsigned timeOutInMsValue = std::min(maxTimeOutValue, timeOutInMs.valueOr(maxTimeOutValue));
m_requestTimeOutTimer.startOneShot(Seconds::fromMilliseconds(timeOutInMsValue));
}
void AuthenticatorManager::timeOutTimerFired()
{
invokePendingCompletionHandler((ExceptionData { NotAllowedError, "Operation timed out."_s }));
clearState();
}
void AuthenticatorManager::runPanel()
{
auto* page = m_pendingRequestData.page.get();
if (!page)
return;
ASSERT(m_pendingRequestData.frameID && page->webPageID() == m_pendingRequestData.frameID->pageID);
auto* frame = page->process().webFrame(m_pendingRequestData.frameID->frameID);
if (!frame)
return;
auto& options = m_pendingRequestData.options;
auto transports = getTransports();
if (transports.isEmpty()) {
cancel();
return;
}
m_pendingRequestData.panel = API::WebAuthenticationPanel::create(*this, getRpId(options), transports, getClientDataType(options));
auto& panel = *m_pendingRequestData.panel;
page->uiClient().runWebAuthenticationPanel(*page, panel, *frame, FrameInfoData { m_pendingRequestData.frameInfo }, [transports = WTFMove(transports), weakPanel = makeWeakPtr(panel), weakThis = makeWeakPtr(*this), this] (WebAuthenticationPanelResult result) {
if (!weakThis || !weakPanel
|| (result == WebAuthenticationPanelResult::DidNotPresent)
|| (weakPanel.get() != m_pendingRequestData.panel.get()))
return;
startDiscovery(transports);
});
}
void AuthenticatorManager::runPresenter()
{
auto transports = getTransports();
if (transports.isEmpty()) {
cancel();
return;
}
startDiscovery(transports);
if (m_mode == Mode::Native)
return;
auto& options = m_pendingRequestData.options;
m_presenter = makeUnique<AuthenticatorPresenterCoordinator>(*this, getRpId(options), transports, getClientDataType(options));
}
void AuthenticatorManager::invokePendingCompletionHandler(Respond&& respond)
{
auto result = WTF::holds_alternative<Ref<AuthenticatorResponse>>(respond) ? WebAuthenticationResult::Succeeded : WebAuthenticationResult::Failed;
if (m_presenter)
m_presenter->dimissPresenter(result);
else {
dispatchPanelClientCall([result] (const API::WebAuthenticationPanel& panel) {
panel.client().dismissPanel(result);
});
}
m_pendingCompletionHandler(WTFMove(respond));
}
void AuthenticatorManager::restartDiscovery()
{
for (auto& service : m_services)
service->restartDiscovery();
}
auto AuthenticatorManager::getTransports() const -> TransportSet
{
TransportSet transports;
WTF::switchOn(m_pendingRequestData.options, [&](const PublicKeyCredentialCreationOptions& options) {
transports = collectTransports(options.authenticatorSelection);
processGoogleLegacyAppIdSupportExtension(options.extensions, transports);
}, [&](const PublicKeyCredentialRequestOptions& options) {
transports = collectTransports(options.allowCredentials);
});
filterTransports(transports);
return transports;
}
void AuthenticatorManager::dispatchPanelClientCall(Function<void(const API::WebAuthenticationPanel&)>&& call) const
{
auto weakPanel = m_pendingRequestData.weakPanel;
if (!weakPanel)
weakPanel = makeWeakPtr(m_pendingRequestData.panel.get());
if (!weakPanel)
return;
RunLoop::main().dispatch([weakPanel = WTFMove(weakPanel), call = WTFMove(call)] () {
if (!weakPanel)
return;
call(*weakPanel);
});
}
}
#endif // ENABLE(WEB_AUTHN)