device/fido: add PIN support to makeCredential and getAssertion.

This change adds additional PIN logic to these flows, although the UI
isn't fully hooked up yet. This is guarded by a feature flag and the
intent is that there's no behaviour change without the feature flag
being set.

BUG=870892

Change-Id: Idd718c5b1ed76e1acd3d4041339b63f910fff41a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1493433
Commit-Queue: Martin Kreichgauer <martinkr@google.com>
Reviewed-by: Martin Kreichgauer <martinkr@google.com>
Reviewed-by: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#637502}
diff --git a/chrome/browser/ui/webauthn/sheet_models.cc b/chrome/browser/ui/webauthn/sheet_models.cc
index 6abee61..504584ca 100644
--- a/chrome/browser/ui/webauthn/sheet_models.cc
+++ b/chrome/browser/ui/webauthn/sheet_models.cc
@@ -751,12 +751,15 @@
   } else {
     // Submit PIN to authenticator for verification.
     DCHECK(mode_ == AuthenticatorClientPinEntrySheetModel::Mode::kPinEntry);
+    // TODO: use device::pin::IsValid instead.
     if (pin_code_.size() < kMinPinLength) {
       delegate_->ShowPinError(
           l10n_util::GetStringUTF16(IDS_WEBAUTHN_PIN_ENTRY_ERROR_TOO_SHORT));
       return;
     }
   }
-  // TODO(martinkr): Actually set the PIN/request the PIN token and continue
-  // the request.
+
+  if (dialog_model()) {
+    dialog_model()->OnHavePIN(base::UTF16ToUTF8(pin_code_));
+  }
 }
diff --git a/chrome/browser/webauthn/authenticator_request_dialog_model.cc b/chrome/browser/webauthn/authenticator_request_dialog_model.cc
index eb30aa4..b87c9d5 100644
--- a/chrome/browser/webauthn/authenticator_request_dialog_model.cc
+++ b/chrome/browser/webauthn/authenticator_request_dialog_model.cc
@@ -378,6 +378,14 @@
   SetCurrentStep(Step::kKeyAlreadyRegistered);
 }
 
+void AuthenticatorRequestDialogModel::OnSoftPINBlock() {
+  // TODO
+}
+
+void AuthenticatorRequestDialogModel::OnHardPINBlock() {
+  // TODO
+}
+
 void AuthenticatorRequestDialogModel::OnBluetoothPoweredStateChanged(
     bool powered) {
   transport_availability_.is_ble_powered = powered;
@@ -424,6 +432,18 @@
   ble_device_paired_callback_ = std::move(ble_device_paired_callback);
 }
 
+void AuthenticatorRequestDialogModel::SetPINCallback(
+    base::OnceCallback<void(std::string)> pin_callback) {
+  pin_callback_ = std::move(pin_callback);
+}
+
+void AuthenticatorRequestDialogModel::OnHavePIN(const std::string& pin) {
+  // TODO: disable the PIN submission action once activated. Otherwise
+  // |OnHavePIN| may be called twice because they'll be a delay between
+  // submitted the PIN and figuring out whether it's valid or not.
+  std::move(pin_callback_).Run(pin);
+}
+
 void AuthenticatorRequestDialogModel::AddAuthenticator(
     const device::FidoAuthenticator& authenticator) {
   if (!authenticator.AuthenticatorTransport()) {
diff --git a/chrome/browser/webauthn/authenticator_request_dialog_model.h b/chrome/browser/webauthn/authenticator_request_dialog_model.h
index fbd7957..5112777 100644
--- a/chrome/browser/webauthn/authenticator_request_dialog_model.h
+++ b/chrome/browser/webauthn/authenticator_request_dialog_model.h
@@ -263,6 +263,14 @@
   // one of excluded credentials (during a MakeCredential request).
   void OnActivatedKeyAlreadyRegistered();
 
+  // To be called when the selected authenticator cannot currently handle PIN
+  // requests because it needs a power-cycle due to too many failures.
+  void OnSoftPINBlock();
+
+  // To be called when the selected authenticator must be reset before
+  // performing any PIN operations because of too many failures.
+  void OnHardPINBlock();
+
   // To be called when the Bluetooth adapter powered state changes.
   void OnBluetoothPoweredStateChanged(bool powered);
 
@@ -276,6 +284,11 @@
   void SetBleDevicePairedCallback(
       BleDevicePairedCallback ble_device_paired_callback);
 
+  void SetPINCallback(base::OnceCallback<void(std::string)> pin_callback);
+
+  // OnHavePIN is called when the user enters a PIN in the UI.
+  void OnHavePIN(const std::string& pin);
+
   void UpdateAuthenticatorReferenceId(base::StringPiece old_authenticator_id,
                                       std::string new_authenticator_id);
   void AddAuthenticator(const device::FidoAuthenticator& authenticator);
@@ -334,6 +347,7 @@
   BlePairingCallback ble_pairing_callback_;
   base::RepeatingClosure bluetooth_adapter_power_on_callback_;
   BleDevicePairedCallback ble_device_paired_callback_;
+  base::OnceCallback<void(std::string)> pin_callback_;
 
   base::WeakPtrFactory<AuthenticatorRequestDialogModel> weak_factory_;
 
diff --git a/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc b/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
index a99083f..6a5bc018 100644
--- a/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
+++ b/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
@@ -134,6 +134,12 @@
     case InterestingFailureReason::kKeyAlreadyRegistered:
       weak_dialog_model_->OnActivatedKeyAlreadyRegistered();
       break;
+    case InterestingFailureReason::kSoftPINBlock:
+      weak_dialog_model_->OnSoftPINBlock();
+      break;
+    case InterestingFailureReason::kHardPINBlock:
+      weak_dialog_model_->OnHardPINBlock();
+      break;
   }
   return true;
 }
@@ -391,6 +397,29 @@
 
   weak_dialog_model_->OnBluetoothPoweredStateChanged(is_powered_on);
 }
+
+void ChromeAuthenticatorRequestDelegate::CollectPIN(
+    base::Optional<int> attempts,
+    base::OnceCallback<void(std::string)> provide_pin_cb) {
+  if (!weak_dialog_model_)
+    return;
+
+  weak_dialog_model_->SetPINCallback(std::move(provide_pin_cb));
+  if (attempts) {
+    weak_dialog_model_->SetCurrentStep(
+        AuthenticatorRequestDialogModel::Step::kClientPinEntry);
+  } else {
+    weak_dialog_model_->SetCurrentStep(
+        AuthenticatorRequestDialogModel::Step::kClientPinSetup);
+  }
+}
+
+void ChromeAuthenticatorRequestDelegate::FinishCollectPIN() {
+  // TODO: add a distinct step for this.
+  weak_dialog_model_->SetCurrentStep(
+      AuthenticatorRequestDialogModel::Step::kUsbInsertAndActivate);
+}
+
 void ChromeAuthenticatorRequestDelegate::OnModelDestroyed() {
   DCHECK(weak_dialog_model_);
   weak_dialog_model_ = nullptr;
diff --git a/chrome/browser/webauthn/chrome_authenticator_request_delegate.h b/chrome/browser/webauthn/chrome_authenticator_request_delegate.h
index 600e269..387ad00 100644
--- a/chrome/browser/webauthn/chrome_authenticator_request_delegate.h
+++ b/chrome/browser/webauthn/chrome_authenticator_request_delegate.h
@@ -100,6 +100,10 @@
   void FidoAuthenticatorPairingModeChanged(base::StringPiece authenticator_id,
                                            bool is_in_pairing_mode) override;
   void BluetoothAdapterPowerChanged(bool is_powered_on) override;
+  void CollectPIN(
+      base::Optional<int> attempts,
+      base::OnceCallback<void(std::string)> provide_pin_cb) override;
+  void FinishCollectPIN() override;
 
   // AuthenticatorRequestDialogModel::Observer:
   void OnModelDestroyed() override;
diff --git a/content/browser/webauth/authenticator_impl.cc b/content/browser/webauth/authenticator_impl.cc
index b70dff4..0c22c16 100644
--- a/content/browser/webauth/authenticator_impl.cc
+++ b/content/browser/webauth/authenticator_impl.cc
@@ -719,7 +719,7 @@
           request_->GetWeakPtr()) /* bluetooth_adapter_power_on_callback */,
       base::BindRepeating(
           &device::FidoRequestHandlerBase::InitiatePairingWithDevice,
-          request_->GetWeakPtr()) /* ble_pairing_callback*/);
+          request_->GetWeakPtr()) /* ble_pairing_callback */);
   request_->set_observer(request_delegate_.get());
 
   request_->SetPlatformAuthenticatorOrMarkUnavailable(
@@ -927,6 +927,8 @@
           AuthenticatorRequestClientDelegate::InterestingFailureReason::
               kKeyAlreadyRegistered);
       return;
+    case device::FidoReturnCode::kAuthenticatorRemovedDuringPINEntry:
+      [[fallthrough]];
     case device::FidoReturnCode::kAuthenticatorResponseInvalid:
       // The response from the authenticator was corrupted.
       InvokeCallbackAndCleanup(
@@ -944,6 +946,16 @@
           blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr,
           Focus::kDoCheck);
       return;
+    case device::FidoReturnCode::kSoftPINBlock:
+      SignalFailureToRequestDelegate(
+          AuthenticatorRequestClientDelegate::InterestingFailureReason::
+              kSoftPINBlock);
+      return;
+    case device::FidoReturnCode::kHardPINBlock:
+      SignalFailureToRequestDelegate(
+          AuthenticatorRequestClientDelegate::InterestingFailureReason::
+              kHardPINBlock);
+      return;
     case device::FidoReturnCode::kSuccess:
       DCHECK(response_data.has_value());
 
@@ -1079,6 +1091,8 @@
           AuthenticatorRequestClientDelegate::InterestingFailureReason::
               kKeyNotRegistered);
       return;
+    case device::FidoReturnCode::kAuthenticatorRemovedDuringPINEntry:
+      [[fallthrough]];
     case device::FidoReturnCode::kAuthenticatorResponseInvalid:
       // The response from the authenticator was corrupted.
       InvokeCallbackAndCleanup(
@@ -1094,6 +1108,16 @@
           std::move(get_assertion_response_callback_),
           blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr);
       return;
+    case device::FidoReturnCode::kSoftPINBlock:
+      SignalFailureToRequestDelegate(
+          AuthenticatorRequestClientDelegate::InterestingFailureReason::
+              kSoftPINBlock);
+      return;
+    case device::FidoReturnCode::kHardPINBlock:
+      SignalFailureToRequestDelegate(
+          AuthenticatorRequestClientDelegate::InterestingFailureReason::
+              kHardPINBlock);
+      return;
     case device::FidoReturnCode::kSuccess:
       DCHECK(response_data.has_value());
       if (transport_used) {
@@ -1133,6 +1157,13 @@
     case AuthenticatorRequestClientDelegate::InterestingFailureReason::kTimeout:
       status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
       break;
+    case AuthenticatorRequestClientDelegate::InterestingFailureReason::
+        kSoftPINBlock:
+      status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
+      break;
+    case AuthenticatorRequestClientDelegate::InterestingFailureReason::
+        kHardPINBlock:
+      status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
   }
 
   error_awaiting_user_acknowledgement_ = status;
diff --git a/content/browser/webauth/authenticator_impl_unittest.cc b/content/browser/webauth/authenticator_impl_unittest.cc
index 8924c79..65cf700 100644
--- a/content/browser/webauth/authenticator_impl_unittest.cc
+++ b/content/browser/webauth/authenticator_impl_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "content/browser/webauth/authenticator_impl.h"
 
+#include <list>
 #include <memory>
 #include <string>
 #include <utility>
@@ -2448,4 +2449,256 @@
   }
 }
 
+// PINList is a list of expected |attempts| values and the PIN to answer with.
+using PINList = std::list<std::pair<base::Optional<int>, std::string>>;
+
+class PINTestAuthenticatorRequestDelegate
+    : public AuthenticatorRequestClientDelegate {
+ public:
+  explicit PINTestAuthenticatorRequestDelegate(
+      const PINList& pins,
+      base::Optional<InterestingFailureReason>* failure_reason)
+      : expected_(pins), failure_reason_(failure_reason) {}
+  ~PINTestAuthenticatorRequestDelegate() override { DCHECK(expected_.empty()); }
+
+  void CollectPIN(
+      base::Optional<int> attempts,
+      base::OnceCallback<void(std::string)> provide_pin_cb) override {
+    DCHECK(!expected_.empty());
+    DCHECK(attempts == expected_.front().first)
+        << "got: " << attempts.value_or(-1)
+        << " expected: " << expected_.front().first.value_or(-1);
+    std::string pin = std::move(expected_.front().second);
+    expected_.pop_front();
+
+    base::SequencedTaskRunnerHandle::Get()->PostTask(
+        FROM_HERE, base::BindOnce(std::move(provide_pin_cb), std::move(pin)));
+  }
+
+  bool DoesBlockRequestOnFailure(InterestingFailureReason reason) override {
+    *failure_reason_ = reason;
+    return AuthenticatorRequestClientDelegate::DoesBlockRequestOnFailure(
+        reason);
+  }
+
+ private:
+  PINList expected_;
+  base::Optional<InterestingFailureReason>* const failure_reason_;
+  DISALLOW_COPY_AND_ASSIGN(PINTestAuthenticatorRequestDelegate);
+};
+
+class PINTestAuthenticatorContentBrowserClient : public ContentBrowserClient {
+ public:
+  std::unique_ptr<AuthenticatorRequestClientDelegate>
+  GetWebAuthenticationRequestDelegate(
+      RenderFrameHost* render_frame_host) override {
+    return std::make_unique<PINTestAuthenticatorRequestDelegate>(
+        expected, &failure_reason);
+  }
+
+  PINList expected;
+  base::Optional<InterestingFailureReason> failure_reason;
+};
+
+class PINAuthenticatorContentBrowserClientTest : public AuthenticatorImplTest {
+ public:
+  PINAuthenticatorContentBrowserClientTest() = default;
+
+  void SetUp() override {
+    scoped_feature_list_.InitAndEnableFeature(device::kWebAuthPINSupport);
+
+    AuthenticatorImplTest::SetUp();
+    old_client_ = SetBrowserClientForTesting(&test_client_);
+    virtual_device_.EnablePINSupport();
+    NavigateAndCommit(GURL(kTestOrigin1));
+  }
+
+  void TearDown() override {
+    SetBrowserClientForTesting(old_client_);
+    AuthenticatorImplTest::TearDown();
+  }
+
+ protected:
+  static PublicKeyCredentialCreationOptionsPtr make_credential_options() {
+    PublicKeyCredentialCreationOptionsPtr options =
+        GetTestPublicKeyCredentialCreationOptions();
+    options->authenticator_selection->user_verification =
+        blink::mojom::UserVerificationRequirement::REQUIRED;
+    return options;
+  }
+
+  static PublicKeyCredentialRequestOptionsPtr get_credential_options() {
+    PublicKeyCredentialRequestOptionsPtr options =
+        GetTestPublicKeyCredentialRequestOptions();
+    options->user_verification =
+        blink::mojom::UserVerificationRequirement::REQUIRED;
+    return options;
+  }
+
+  static bool HasUV(const TestMakeCredentialCallback& callback) {
+    DCHECK_EQ(AuthenticatorStatus::SUCCESS, callback.status());
+    base::Optional<Value> attestation_value =
+        Reader::Read(callback.value()->attestation_object);
+    DCHECK(attestation_value);
+    DCHECK(attestation_value->is_map());
+    const auto& attestation = attestation_value->GetMap();
+
+    const auto auth_data_it = attestation.find(Value("authData"));
+    DCHECK(auth_data_it != attestation.end() &&
+           auth_data_it->second.is_bytestring());
+    base::Optional<device::AuthenticatorData> auth_data =
+        device::AuthenticatorData::DecodeAuthenticatorData(
+            auth_data_it->second.GetBytestring());
+    return auth_data->obtained_user_verification();
+  }
+
+  static bool HasUV(const TestGetAssertionCallback& callback) {
+    DCHECK_EQ(AuthenticatorStatus::SUCCESS, callback.status());
+    base::Optional<device::AuthenticatorData> auth_data =
+        device::AuthenticatorData::DecodeAuthenticatorData(
+            callback.value()->authenticator_data);
+    return auth_data->obtained_user_verification();
+  }
+
+  PINTestAuthenticatorContentBrowserClient test_client_;
+  device::test::ScopedVirtualFidoDevice virtual_device_;
+
+ private:
+  ContentBrowserClient* old_client_ = nullptr;
+  base::test::ScopedFeatureList scoped_feature_list_;
+
+  DISALLOW_COPY_AND_ASSIGN(PINAuthenticatorContentBrowserClientTest);
+};
+
+static constexpr char kTestPIN[] = "1234";
+
+TEST_F(PINAuthenticatorContentBrowserClientTest, MakeCredentialSet) {
+  TestServiceManagerContext smc;
+  test_client_.expected = {{base::nullopt, kTestPIN}};
+
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestMakeCredentialCallback callback_receiver;
+  authenticator->MakeCredential(make_credential_options(),
+                                callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::SUCCESS, callback_receiver.status());
+  EXPECT_EQ(kTestPIN, virtual_device_.mutable_state()->pin);
+  EXPECT_TRUE(HasUV(callback_receiver));
+}
+
+TEST_F(PINAuthenticatorContentBrowserClientTest, MakeCredentialUse) {
+  TestServiceManagerContext smc;
+  virtual_device_.mutable_state()->pin = kTestPIN;
+  virtual_device_.mutable_state()->retries = 8;
+
+  test_client_.expected = {{8, "wrong"}, {7, kTestPIN}};
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestMakeCredentialCallback callback_receiver;
+  authenticator->MakeCredential(make_credential_options(),
+                                callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::SUCCESS, callback_receiver.status());
+  EXPECT_EQ(8, virtual_device_.mutable_state()->retries);
+  EXPECT_TRUE(HasUV(callback_receiver));
+}
+
+TEST_F(PINAuthenticatorContentBrowserClientTest, MakeCredentialSoftLock) {
+  TestServiceManagerContext smc;
+  virtual_device_.mutable_state()->pin = kTestPIN;
+  virtual_device_.mutable_state()->retries = 8;
+
+  test_client_.expected = {{8, "wrong"}, {7, "wrong"}, {6, "wrong"}};
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestMakeCredentialCallback callback_receiver;
+  authenticator->MakeCredential(make_credential_options(),
+                                callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status());
+  EXPECT_EQ(5, virtual_device_.mutable_state()->retries);
+  EXPECT_TRUE(virtual_device_.mutable_state()->soft_locked);
+  ASSERT_TRUE(test_client_.failure_reason.has_value());
+  EXPECT_EQ(InterestingFailureReason::kSoftPINBlock,
+            *test_client_.failure_reason);
+}
+
+TEST_F(PINAuthenticatorContentBrowserClientTest, MakeCredentialHardLock) {
+  TestServiceManagerContext smc;
+  virtual_device_.mutable_state()->pin = kTestPIN;
+  virtual_device_.mutable_state()->retries = 1;
+
+  test_client_.expected = {{1, "wrong"}};
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestMakeCredentialCallback callback_receiver;
+  authenticator->MakeCredential(make_credential_options(),
+                                callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status());
+  EXPECT_EQ(0, virtual_device_.mutable_state()->retries);
+  ASSERT_TRUE(test_client_.failure_reason.has_value());
+  EXPECT_EQ(InterestingFailureReason::kHardPINBlock,
+            *test_client_.failure_reason);
+}
+
+TEST_F(PINAuthenticatorContentBrowserClientTest, GetAssertion) {
+  TestServiceManagerContext smc;
+  virtual_device_.mutable_state()->pin = kTestPIN;
+  virtual_device_.mutable_state()->retries = 8;
+
+  PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
+  ASSERT_TRUE(virtual_device_.mutable_state()->InjectRegistration(
+      options->allow_credentials[0]->id, kTestRelyingPartyId));
+
+  test_client_.expected = {{8, "wrong"}, {7, kTestPIN}};
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestGetAssertionCallback callback_receiver;
+  authenticator->GetAssertion(std::move(options), callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::SUCCESS, callback_receiver.status());
+  EXPECT_EQ(8, virtual_device_.mutable_state()->retries);
+  EXPECT_TRUE(HasUV(callback_receiver));
+}
+
+TEST_F(PINAuthenticatorContentBrowserClientTest, GetAssertionSoftLock) {
+  TestServiceManagerContext smc;
+  virtual_device_.mutable_state()->pin = kTestPIN;
+  virtual_device_.mutable_state()->retries = 8;
+
+  PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
+  ASSERT_TRUE(virtual_device_.mutable_state()->InjectRegistration(
+      options->allow_credentials[0]->id, kTestRelyingPartyId));
+
+  test_client_.expected = {{8, "wrong"}, {7, "wrong"}, {6, "wrong"}};
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestGetAssertionCallback callback_receiver;
+  authenticator->GetAssertion(std::move(options), callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status());
+  EXPECT_EQ(5, virtual_device_.mutable_state()->retries);
+  EXPECT_TRUE(virtual_device_.mutable_state()->soft_locked);
+  ASSERT_TRUE(test_client_.failure_reason.has_value());
+  EXPECT_EQ(InterestingFailureReason::kSoftPINBlock,
+            *test_client_.failure_reason);
+}
+
+TEST_F(PINAuthenticatorContentBrowserClientTest, GetAssertionHardLock) {
+  TestServiceManagerContext smc;
+  virtual_device_.mutable_state()->pin = kTestPIN;
+  virtual_device_.mutable_state()->retries = 1;
+
+  PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
+  ASSERT_TRUE(virtual_device_.mutable_state()->InjectRegistration(
+      options->allow_credentials[0]->id, kTestRelyingPartyId));
+
+  test_client_.expected = {{1, "wrong"}};
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestGetAssertionCallback callback_receiver;
+  authenticator->GetAssertion(std::move(options), callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status());
+  EXPECT_EQ(0, virtual_device_.mutable_state()->retries);
+  ASSERT_TRUE(test_client_.failure_reason.has_value());
+  EXPECT_EQ(InterestingFailureReason::kHardPINBlock,
+            *test_client_.failure_reason);
+}
+
 }  // namespace content
diff --git a/content/public/browser/authenticator_request_client_delegate.cc b/content/public/browser/authenticator_request_client_delegate.cc
index dffc975..07d3039 100644
--- a/content/public/browser/authenticator_request_client_delegate.cc
+++ b/content/public/browser/authenticator_request_client_delegate.cc
@@ -83,4 +83,10 @@
     base::StringPiece authenticator_id,
     bool is_in_pairing_mode) {}
 
+void AuthenticatorRequestClientDelegate::CollectPIN(
+    base::Optional<int> attempts,
+    base::OnceCallback<void(std::string)> provide_pin_cb) {}
+
+void AuthenticatorRequestClientDelegate::FinishCollectPIN() {}
+
 }  // namespace content
diff --git a/content/public/browser/authenticator_request_client_delegate.h b/content/public/browser/authenticator_request_client_delegate.h
index c73ae20..3c6a600 100644
--- a/content/public/browser/authenticator_request_client_delegate.h
+++ b/content/public/browser/authenticator_request_client_delegate.h
@@ -39,6 +39,8 @@
     kTimeout,
     kKeyNotRegistered,
     kKeyAlreadyRegistered,
+    kSoftPINBlock,
+    kHardPINBlock,
   };
 
   AuthenticatorRequestClientDelegate();
@@ -131,6 +133,10 @@
                                   std::string new_authenticator_id) override;
   void FidoAuthenticatorPairingModeChanged(base::StringPiece authenticator_id,
                                            bool is_in_pairing_mode) override;
+  void CollectPIN(
+      base::Optional<int> attempts,
+      base::OnceCallback<void(std::string)> provide_pin_cb) override;
+  void FinishCollectPIN() override;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(AuthenticatorRequestClientDelegate);
diff --git a/device/fido/ble_adapter_manager_unittest.cc b/device/fido/ble_adapter_manager_unittest.cc
index f91ba0d..1c8d773 100644
--- a/device/fido/ble_adapter_manager_unittest.cc
+++ b/device/fido/ble_adapter_manager_unittest.cc
@@ -48,6 +48,10 @@
                     std::string new_authenticator_id));
   MOCK_METHOD2(FidoAuthenticatorPairingModeChanged,
                void(base::StringPiece, bool));
+  MOCK_METHOD2(CollectPIN,
+               void(base::Optional<int>,
+                    base::OnceCallback<void(std::string)>));
+  MOCK_METHOD0(FinishCollectPIN, void());
 
  private:
   DISALLOW_COPY_AND_ASSIGN(MockObserver);
diff --git a/device/fido/ctap2_device_operation.h b/device/fido/ctap2_device_operation.h
index 1cba8f9..ad9edd54a 100644
--- a/device/fido/ctap2_device_operation.h
+++ b/device/fido/ctap2_device_operation.h
@@ -35,11 +35,11 @@
       base::OnceCallback<base::Optional<Response>(base::span<const uint8_t>)>;
 
   Ctap2DeviceOperation(FidoDevice* device,
-                       const Request& request,
+                       Request request,
                        DeviceResponseCallback callback,
                        DeviceResponseParser device_response_parser)
       : DeviceOperation<Request, Response>(device,
-                                           request,
+                                           std::move(request),
                                            std::move(callback)),
         device_response_parser_(std::move(device_response_parser)),
         weak_factory_(this) {}
diff --git a/device/fido/device_operation.h b/device/fido/device_operation.h
index 21db99f..70901ae 100644
--- a/device/fido/device_operation.h
+++ b/device/fido/device_operation.h
@@ -38,9 +38,11 @@
   // Represents a per device logic that is owned by FidoTask. Thus,
   // DeviceOperation does not outlive |request|.
   DeviceOperation(FidoDevice* device,
-                  const Request& request,
+                  Request request,
                   DeviceResponseCallback callback)
-      : device_(device), request_(request), callback_(std::move(callback)) {}
+      : device_(device),
+        request_(std::move(request)),
+        callback_(std::move(callback)) {}
 
   virtual ~DeviceOperation() = default;
 
@@ -65,7 +67,7 @@
 
  private:
   FidoDevice* const device_ = nullptr;
-  const Request& request_;
+  Request request_;
   DeviceResponseCallback callback_;
 
   DISALLOW_COPY_AND_ASSIGN(DeviceOperation);
diff --git a/device/fido/fido_authenticator.cc b/device/fido/fido_authenticator.cc
index feb8705..093fe35 100644
--- a/device/fido/fido_authenticator.cc
+++ b/device/fido/fido_authenticator.cc
@@ -33,7 +33,7 @@
 }
 
 void FidoAuthenticator::SetPIN(const std::string& pin,
-                               pin::KeyAgreementResponse& peer_key,
+                               const pin::KeyAgreementResponse& peer_key,
                                FidoAuthenticator::SetPINCallback callback) {
   NOTREACHED();
 }
diff --git a/device/fido/fido_authenticator.h b/device/fido/fido_authenticator.h
index 634f7c81..bfef7d1 100644
--- a/device/fido/fido_authenticator.h
+++ b/device/fido/fido_authenticator.h
@@ -90,7 +90,7 @@
   // only valid to call this method if |Options| indicates that the
   // authenticator supports PINs.
   virtual void SetPIN(const std::string& pin,
-                      pin::KeyAgreementResponse& peer_key,
+                      const pin::KeyAgreementResponse& peer_key,
                       SetPINCallback callback);
   // ChangePIN alters the PIN on a device that already has a PIN set. The
   // length of |pin| must respect |pin::kMinLength| and |pin::kMaxLength|. It is
diff --git a/device/fido/fido_constants.h b/device/fido/fido_constants.h
index 85cfa30..0c38e214 100644
--- a/device/fido/fido_constants.h
+++ b/device/fido/fido_constants.h
@@ -29,6 +29,9 @@
   kUserConsentButCredentialNotRecognized,
   // The user explicitly refused to provide consent.
   kUserConsentDenied,
+  kAuthenticatorRemovedDuringPINEntry,
+  kSoftPINBlock,
+  kHardPINBlock,
 };
 
 enum class ProtocolVersion {
diff --git a/device/fido/fido_device_authenticator.cc b/device/fido/fido_device_authenticator.cc
index d2807351..fae86f8 100644
--- a/device/fido/fido_device_authenticator.cc
+++ b/device/fido/fido_device_authenticator.cc
@@ -7,11 +7,13 @@
 #include <utility>
 
 #include "base/bind.h"
+#include "base/feature_list.h"
 #include "base/logging.h"
 #include "base/threading/sequenced_task_runner_handle.h"
 #include "device/fido/authenticator_supported_options.h"
 #include "device/fido/ctap_get_assertion_request.h"
 #include "device/fido/ctap_make_credential_request.h"
+#include "device/fido/features.h"
 #include "device/fido/fido_device.h"
 #include "device/fido/get_assertion_task.h"
 #include "device/fido/make_credential_task.h"
@@ -55,19 +57,22 @@
 
 void FidoDeviceAuthenticator::MakeCredential(CtapMakeCredentialRequest request,
                                              MakeCredentialCallback callback) {
-  DCHECK(!task_);
   DCHECK(device_->SupportedProtocolIsInitialized())
       << "InitializeAuthenticator() must be called first.";
   DCHECK(Options());
 
-  // Update the request to the "effective" user verification requirement.
-  // https://w3c.github.io/webauthn/#effective-user-verification-requirement-for-credential-creation
-  if (Options()->user_verification_availability ==
-      AuthenticatorSupportedOptions::UserVerificationAvailability::
-          kSupportedAndConfigured) {
-    request.SetUserVerification(UserVerificationRequirement::kRequired);
-  } else {
-    request.SetUserVerification(UserVerificationRequirement::kDiscouraged);
+  // When PIN support is enabled, the mapping from Webauthn's ternary user-
+  // verification preference to CTAP2's binary option is done inside the request
+  // handler instead.
+  if (!base::FeatureList::IsEnabled(device::kWebAuthPINSupport) ||
+      request.user_verification() == UserVerificationRequirement::kPreferred) {
+    if (Options()->user_verification_availability ==
+        AuthenticatorSupportedOptions::UserVerificationAvailability::
+            kSupportedAndConfigured) {
+      request.SetUserVerification(UserVerificationRequirement::kRequired);
+    } else {
+      request.SetUserVerification(UserVerificationRequirement::kDiscouraged);
+    }
   }
 
   // TODO(martinkr): Change FidoTasks to take all request parameters by const
@@ -78,19 +83,26 @@
 
 void FidoDeviceAuthenticator::GetAssertion(CtapGetAssertionRequest request,
                                            GetAssertionCallback callback) {
-  DCHECK(!task_);
   DCHECK(device_->SupportedProtocolIsInitialized())
       << "InitializeAuthenticator() must be called first.";
   DCHECK(Options());
+  const bool pin_support =
+      base::FeatureList::IsEnabled(device::kWebAuthPINSupport);
 
   // Update the request to the "effective" user verification requirement.
   // https://w3c.github.io/webauthn/#effective-user-verification-requirement-for-assertion
-  if (Options()->user_verification_availability ==
-      AuthenticatorSupportedOptions::UserVerificationAvailability::
-          kSupportedAndConfigured) {
-    request.SetUserVerification(UserVerificationRequirement::kRequired);
-  } else {
-    request.SetUserVerification(UserVerificationRequirement::kDiscouraged);
+  if (!pin_support ||
+      request.user_verification() == UserVerificationRequirement::kPreferred) {
+    if (Options()->user_verification_availability ==
+            AuthenticatorSupportedOptions::UserVerificationAvailability::
+                kSupportedAndConfigured ||
+        (pin_support && Options()->client_pin_availability ==
+                            AuthenticatorSupportedOptions::
+                                ClientPinAvailability::kSupportedAndPinSet)) {
+      request.SetUserVerification(UserVerificationRequirement::kRequired);
+    } else {
+      request.SetUserVerification(UserVerificationRequirement::kDiscouraged);
+    }
   }
 
   task_ = std::make_unique<GetAssertionTask>(device_.get(), std::move(request),
@@ -203,7 +215,7 @@
 }
 
 void FidoDeviceAuthenticator::SetPIN(const std::string& pin,
-                                     pin::KeyAgreementResponse& peer_key,
+                                     const pin::KeyAgreementResponse& peer_key,
                                      SetPINCallback callback) {
   DCHECK(device_->SupportedProtocolIsInitialized())
       << "InitializeAuthenticator() must be called first.";
diff --git a/device/fido/fido_device_authenticator.h b/device/fido/fido_device_authenticator.h
index 31cf8aa..fff40969 100644
--- a/device/fido/fido_device_authenticator.h
+++ b/device/fido/fido_device_authenticator.h
@@ -52,7 +52,7 @@
                    const pin::KeyAgreementResponse& peer_key,
                    GetPINTokenCallback callback) override;
   void SetPIN(const std::string& pin,
-              pin::KeyAgreementResponse& peer_key,
+              const pin::KeyAgreementResponse& peer_key,
               SetPINCallback callback) override;
   void ChangePIN(const std::string& old_pin,
                  const std::string& new_pin,
diff --git a/device/fido/fido_request_handler.h b/device/fido/fido_request_handler.h
index f54b79b..646cde0b 100644
--- a/device/fido/fido_request_handler.h
+++ b/device/fido/fido_request_handler.h
@@ -78,6 +78,8 @@
              authenticator->AuthenticatorTransport());
   }
 
+  CompletionCallback completion_callback_;
+
  private:
   static base::Optional<FidoReturnCode>
   ConvertDeviceResponseCodeToFidoReturnCode(
@@ -120,8 +122,6 @@
     }
   }
 
-  CompletionCallback completion_callback_;
-
   DISALLOW_COPY_AND_ASSIGN(FidoRequestHandler);
 };
 
diff --git a/device/fido/fido_request_handler_base.h b/device/fido/fido_request_handler_base.h
index 1de5d5e..93525cd 100644
--- a/device/fido/fido_request_handler_base.h
+++ b/device/fido/fido_request_handler_base.h
@@ -140,6 +140,17 @@
     virtual void FidoAuthenticatorPairingModeChanged(
         base::StringPiece authenticator_id,
         bool is_in_pairing_mode) = 0;
+
+    // CollectPIN is called when a PIN is needed to complete a request. The
+    // |retries| parameter is either |nullopt| to indicate that the user needs
+    // to set a PIN, or contains the number of PIN attempts remaining before a
+    // hard lock.
+    virtual void CollectPIN(
+        base::Optional<int> attempts,
+        base::OnceCallback<void(std::string)> provide_pin_cb) = 0;
+
+    // CollectClientPin is guaranteed to have been called previously.
+    virtual void FinishCollectPIN() = 0;
   };
 
   // TODO(https://crbug.com/769631): Remove the dependency on Connector once
diff --git a/device/fido/fido_request_handler_unittest.cc b/device/fido/fido_request_handler_unittest.cc
index 32c38b5..521d0e7 100644
--- a/device/fido/fido_request_handler_unittest.cc
+++ b/device/fido/fido_request_handler_unittest.cc
@@ -126,6 +126,14 @@
   void FidoAuthenticatorPairingModeChanged(base::StringPiece authenticator_id,
                                            bool is_in_pairing_mode) override {}
 
+  void CollectPIN(
+      base::Optional<int> attempts,
+      base::OnceCallback<void(std::string)> provide_pin_cb) override {
+    NOTREACHED();
+  }
+
+  void FinishCollectPIN() override { NOTREACHED(); }
+
  private:
   TransportAvailabilityNotificationReceiver
       transport_availability_notification_receiver_;
diff --git a/device/fido/get_assertion_request_handler.cc b/device/fido/get_assertion_request_handler.cc
index a10bcf4..bc70bf2f 100644
--- a/device/fido/get_assertion_request_handler.cc
+++ b/device/fido/get_assertion_request_handler.cc
@@ -6,15 +6,19 @@
 
 #include <algorithm>
 #include <set>
+#include <string>
 #include <utility>
 
 #include "base/bind.h"
+#include "base/feature_list.h"
 #include "base/stl_util.h"
 #include "device/fido/authenticator_get_assertion_response.h"
 #include "device/fido/cable/fido_cable_discovery.h"
+#include "device/fido/features.h"
 #include "device/fido/fido_authenticator.h"
 #include "device/fido/fido_discovery_factory.h"
 #include "device/fido/get_assertion_task.h"
+#include "device/fido/pin.h"
 
 namespace device {
 
@@ -102,7 +106,8 @@
 // Checks UserVerificationRequirement enum passed from the relying party is
 // compatible with the authenticator.
 bool CheckUserVerificationCompatible(FidoAuthenticator* authenticator,
-                                     const CtapGetAssertionRequest& request) {
+                                     const CtapGetAssertionRequest& request,
+                                     bool have_observer) {
   const auto& opt_options = authenticator->Options();
   if (!opt_options) {
     // This authenticator doesn't know its capabilities yet, so we need
@@ -111,11 +116,16 @@
     return true;
   }
 
+  const bool pin_support =
+      base::FeatureList::IsEnabled(device::kWebAuthPINSupport) && have_observer;
   return request.user_verification() !=
              UserVerificationRequirement::kRequired ||
          opt_options->user_verification_availability ==
              AuthenticatorSupportedOptions::UserVerificationAvailability::
-                 kSupportedAndConfigured;
+                 kSupportedAndConfigured ||
+         (pin_support && opt_options->client_pin_availability ==
+                             AuthenticatorSupportedOptions::
+                                 ClientPinAvailability::kSupportedAndPinSet);
 }
 
 base::flat_set<FidoTransportProtocol> GetTransportsAllowedByRP(
@@ -187,20 +197,97 @@
 
 GetAssertionRequestHandler::~GetAssertionRequestHandler() = default;
 
+static bool WillNeedPIN(const CtapGetAssertionRequest& request,
+                        const FidoAuthenticator* authenticator) {
+  const auto& opt_options = authenticator->Options();
+  if (request.user_verification() ==
+          UserVerificationRequirement::kDiscouraged ||
+      !opt_options ||
+      opt_options->user_verification_availability ==
+          AuthenticatorSupportedOptions::UserVerificationAvailability::
+              kSupportedAndConfigured) {
+    return false;
+  }
+
+  return opt_options->client_pin_availability ==
+         AuthenticatorSupportedOptions::ClientPinAvailability::
+             kSupportedAndPinSet;
+}
+
 void GetAssertionRequestHandler::DispatchRequest(
     FidoAuthenticator* authenticator) {
-  if (!CheckUserVerificationCompatible(authenticator, request_))
-    return;
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
 
-  authenticator->GetAssertion(
-      request_, base::BindOnce(&GetAssertionRequestHandler::HandleResponse,
-                               weak_factory_.GetWeakPtr(), authenticator));
+  if (state_ != State::kWaitingForTouch ||
+      !CheckUserVerificationCompatible(authenticator, request_, observer())) {
+    return;
+  }
+
+  if (base::FeatureList::IsEnabled(device::kWebAuthPINSupport) &&
+      WillNeedPIN(request_, authenticator)) {
+    CtapGetAssertionRequest request(request_);
+    // Set empty pinAuth to trigger just a touch.
+    request.SetPinAuth({});
+    authenticator->GetAssertion(
+        request, base::BindOnce(&GetAssertionRequestHandler::HandleResponse,
+                                weak_factory_.GetWeakPtr(), authenticator));
+  } else {
+    authenticator->GetAssertion(
+        request_, base::BindOnce(&GetAssertionRequestHandler::HandleResponse,
+                                 weak_factory_.GetWeakPtr(), authenticator));
+  }
+}
+
+void GetAssertionRequestHandler::AuthenticatorRemoved(
+    FidoDiscoveryBase* discovery,
+    FidoAuthenticator* authenticator) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+  FidoRequestHandlerBase::AuthenticatorRemoved(discovery, authenticator);
+
+  if (authenticator == authenticator_) {
+    authenticator_ = nullptr;
+    if (state_ == State::kWaitingForPIN ||
+        state_ == State::kWaitingForSecondTouch) {
+      state_ = State::kFinished;
+      std::move(completion_callback_)
+          .Run(FidoReturnCode::kAuthenticatorRemovedDuringPINEntry,
+               base::nullopt, base::nullopt);
+    }
+  }
 }
 
 void GetAssertionRequestHandler::HandleResponse(
     FidoAuthenticator* authenticator,
     CtapDeviceResponseCode response_code,
     base::Optional<AuthenticatorGetAssertionResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+  if (state_ != State::kWaitingForTouch &&
+      state_ != State::kWaitingForSecondTouch) {
+    return;
+  }
+
+  if (state_ == State::kWaitingForTouch &&
+      base::FeatureList::IsEnabled(device::kWebAuthPINSupport) &&
+      WillNeedPIN(request_, authenticator)) {
+    if (response_code != CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid &&
+        response_code != CtapDeviceResponseCode::kCtap2ErrPinInvalid) {
+      VLOG(1) << "Expected invalid pinAuth error but got "
+              << static_cast<int>(response_code);
+      return;
+    }
+    DCHECK(observer());
+    CancelActiveAuthenticators(authenticator->GetId());
+    state_ = State::kGettingRetries;
+    authenticator_ = authenticator;
+    authenticator_->GetRetries(
+        base::BindOnce(&GetAssertionRequestHandler::OnRetriesResponse,
+                       weak_factory_.GetWeakPtr()));
+    return;
+  }
+
+  state_ = State::kFinished;
   if (response_code != CtapDeviceResponseCode::kSuccess) {
     OnAuthenticatorResponse(authenticator, response_code, base::nullopt);
     return;
@@ -219,4 +306,120 @@
   OnAuthenticatorResponse(authenticator, response_code, std::move(response));
 }
 
+void GetAssertionRequestHandler::OnRetriesResponse(
+    CtapDeviceResponseCode status,
+    base::Optional<pin::RetriesResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK_EQ(state_, State::kGettingRetries);
+
+  if (status == CtapDeviceResponseCode::kSuccess && !response) {
+    status = CtapDeviceResponseCode::kCtap2ErrInvalidCBOR;
+  }
+
+  if (status != CtapDeviceResponseCode::kSuccess) {
+    state_ = State::kFinished;
+    FidoReturnCode ret = FidoReturnCode::kAuthenticatorResponseInvalid;
+    if (status == CtapDeviceResponseCode::kCtap2ErrPinBlocked) {
+      ret = FidoReturnCode::kHardPINBlock;
+    }
+    std::move(completion_callback_).Run(ret, base::nullopt, base::nullopt);
+    return;
+  }
+
+  state_ = State::kWaitingForPIN;
+  observer()->CollectPIN(response->retries,
+                         base::BindOnce(&GetAssertionRequestHandler::OnHavePIN,
+                                        weak_factory_.GetWeakPtr()));
+}
+
+void GetAssertionRequestHandler::OnHavePIN(std::string pin) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK_EQ(State::kWaitingForPIN, state_);
+  DCHECK(pin::IsValid(pin));
+
+  if (authenticator_ == nullptr) {
+    // Authenticator was detached. The request will already have been canceled
+    // but this callback may have been waiting in a queue.
+    DCHECK(!completion_callback_);
+    return;
+  }
+
+  state_ = State::kGetEphemeralKey;
+  authenticator_->GetEphemeralKey(
+      base::BindOnce(&GetAssertionRequestHandler::OnHaveEphemeralKey,
+                     weak_factory_.GetWeakPtr(), std::move(pin)));
+}
+
+void GetAssertionRequestHandler::OnHaveEphemeralKey(
+    std::string pin,
+    CtapDeviceResponseCode status,
+    base::Optional<pin::KeyAgreementResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK_EQ(State::kGetEphemeralKey, state_);
+
+  if (status == CtapDeviceResponseCode::kSuccess && !response) {
+    status = CtapDeviceResponseCode::kCtap2ErrInvalidCBOR;
+  }
+
+  if (status != CtapDeviceResponseCode::kSuccess) {
+    state_ = State::kFinished;
+    std::move(completion_callback_)
+        .Run(FidoReturnCode::kAuthenticatorResponseInvalid, base::nullopt,
+             base::nullopt);
+    return;
+  }
+
+  state_ = State::kRequestWithPIN;
+  authenticator_->GetPINToken(
+      std::move(pin), *response,
+      base::BindOnce(&GetAssertionRequestHandler::OnHavePINToken,
+                     weak_factory_.GetWeakPtr()));
+}
+
+void GetAssertionRequestHandler::OnHavePINToken(
+    CtapDeviceResponseCode status,
+    base::Optional<pin::TokenResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK_EQ(state_, State::kRequestWithPIN);
+
+  if (status == CtapDeviceResponseCode::kCtap2ErrPinInvalid) {
+    state_ = State::kGettingRetries;
+    authenticator_->GetRetries(
+        base::BindOnce(&GetAssertionRequestHandler::OnRetriesResponse,
+                       weak_factory_.GetWeakPtr()));
+    return;
+  }
+
+  if (status == CtapDeviceResponseCode::kSuccess && !response) {
+    status = CtapDeviceResponseCode::kCtap2ErrInvalidCBOR;
+  }
+
+  if (status != CtapDeviceResponseCode::kSuccess) {
+    state_ = State::kFinished;
+    FidoReturnCode ret;
+    switch (status) {
+      case CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked:
+        ret = FidoReturnCode::kSoftPINBlock;
+        break;
+      case CtapDeviceResponseCode::kCtap2ErrPinBlocked:
+        ret = FidoReturnCode::kHardPINBlock;
+        break;
+      default:
+        ret = FidoReturnCode::kAuthenticatorResponseInvalid;
+        break;
+    }
+    std::move(completion_callback_).Run(ret, base::nullopt, base::nullopt);
+    return;
+  }
+
+  observer()->FinishCollectPIN();
+  state_ = State::kWaitingForSecondTouch;
+  request_.SetPinAuth(response->PinAuth(request_.client_data_hash()));
+  request_.SetPinProtocol(pin::kProtocolVersion);
+
+  authenticator_->GetAssertion(
+      request_, base::BindOnce(&GetAssertionRequestHandler::HandleResponse,
+                               weak_factory_.GetWeakPtr(), authenticator_));
+}
+
 }  // namespace device
diff --git a/device/fido/get_assertion_request_handler.h b/device/fido/get_assertion_request_handler.h
index e561de8..c52efca 100644
--- a/device/fido/get_assertion_request_handler.h
+++ b/device/fido/get_assertion_request_handler.h
@@ -6,6 +6,7 @@
 #define DEVICE_FIDO_GET_ASSERTION_REQUEST_HANDLER_H_
 
 #include <memory>
+#include <string>
 
 #include "base/callback.h"
 #include "base/macros.h"
@@ -36,15 +37,43 @@
   ~GetAssertionRequestHandler() override;
 
  private:
+  enum class State {
+    kWaitingForTouch,
+    kWaitingForSecondTouch,
+    kGettingRetries,
+    kWaitingForPIN,
+    kGetEphemeralKey,
+    kRequestWithPIN,
+    kFinished,
+  };
+
   // FidoRequestHandlerBase:
   void DispatchRequest(FidoAuthenticator* authenticator) override;
+  void AuthenticatorRemoved(FidoDiscoveryBase* discovery,
+                            FidoAuthenticator* authenticator) override;
 
   void HandleResponse(
       FidoAuthenticator* authenticator,
       CtapDeviceResponseCode response_code,
       base::Optional<AuthenticatorGetAssertionResponse> response);
+  void OnRetriesResponse(CtapDeviceResponseCode status,
+                         base::Optional<pin::RetriesResponse> response);
+  void OnHavePIN(std::string pin);
+  void OnHaveEphemeralKey(std::string pin,
+                          CtapDeviceResponseCode status,
+                          base::Optional<pin::KeyAgreementResponse> response);
+  void OnHavePINToken(CtapDeviceResponseCode status,
+                      base::Optional<pin::TokenResponse> response);
 
+  State state_ = State::kWaitingForTouch;
   CtapGetAssertionRequest request_;
+  // authenticator_ points to the authenticator that will be used for this
+  // operation. It's only set after the user touches an authenticator to select
+  // it, after which point that authenticator will be used exclusively through
+  // requesting PIN etc. The object is owned by the underlying discovery object
+  // and this pointer is cleared if it's removed during processing.
+  FidoAuthenticator* authenticator_ = nullptr;
+  SEQUENCE_CHECKER(my_sequence_checker_);
   base::WeakPtrFactory<GetAssertionRequestHandler> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(GetAssertionRequestHandler);
diff --git a/device/fido/make_credential_request_handler.cc b/device/fido/make_credential_request_handler.cc
index 0522cd1..fa54a9f 100644
--- a/device/fido/make_credential_request_handler.cc
+++ b/device/fido/make_credential_request_handler.cc
@@ -5,26 +5,36 @@
 #include "device/fido/make_credential_request_handler.h"
 
 #include <set>
+#include <string>
 #include <utility>
 
 #include "base/bind.h"
+#include "base/feature_list.h"
 #include "base/stl_util.h"
 #include "device/fido/authenticator_make_credential_response.h"
+#include "device/fido/features.h"
 #include "device/fido/fido_authenticator.h"
 #include "device/fido/fido_parsing_utils.h"
 #include "device/fido/fido_transport_protocol.h"
 #include "device/fido/make_credential_task.h"
+#include "device/fido/pin.h"
 #include "services/service_manager/public/cpp/connector.h"
 
 namespace device {
 
+using ClientPinAvailability =
+    AuthenticatorSupportedOptions::ClientPinAvailability;
+
 namespace {
 
 bool CheckIfAuthenticatorSelectionCriteriaAreSatisfied(
     FidoAuthenticator* authenticator,
-    const AuthenticatorSelectionCriteria& authenticator_selection_criteria) {
+    const AuthenticatorSelectionCriteria& authenticator_selection_criteria,
+    bool have_observer) {
   using UvAvailability =
       AuthenticatorSupportedOptions::UserVerificationAvailability;
+  const bool pin_support =
+      base::FeatureList::IsEnabled(device::kWebAuthPINSupport) && have_observer;
 
   const auto& opt_options = authenticator->Options();
   if (!opt_options) {
@@ -39,17 +49,25 @@
        !opt_options->is_platform_device) ||
       (authenticator_selection_criteria.authenticator_attachment() ==
            AuthenticatorAttachment::kCrossPlatform &&
-       opt_options->is_platform_device))
+       opt_options->is_platform_device)) {
     return false;
+  }
 
   if (authenticator_selection_criteria.require_resident_key() &&
-      !opt_options->supports_resident_key)
+      !opt_options->supports_resident_key) {
     return false;
+  }
 
-  return authenticator_selection_criteria.user_verification_requirement() !=
-             UserVerificationRequirement::kRequired ||
-         opt_options->user_verification_availability ==
-             UvAvailability::kSupportedAndConfigured;
+  if (authenticator_selection_criteria.user_verification_requirement() ==
+          UserVerificationRequirement::kRequired &&
+      opt_options->user_verification_availability !=
+          UvAvailability::kSupportedAndConfigured &&
+      (!pin_support || opt_options->client_pin_availability ==
+                           ClientPinAvailability::kNotSupported)) {
+    return false;
+  }
+
+  return true;
 }
 
 base::flat_set<FidoTransportProtocol> GetTransportsAllowedByRP(
@@ -102,11 +120,35 @@
 
 MakeCredentialRequestHandler::~MakeCredentialRequestHandler() = default;
 
+// WillNeedPIN returns what type of PIN intervention will be needed to serve the
+// given request on the given authenticator.
+//   |kNotSupported|: no PIN involved.
+//   |kSupportedButPinNotSet|: will need to set a new PIN.
+//   |kSupportedAndPinSet|: will need to prompt for an existing PIN.
+static ClientPinAvailability WillNeedPIN(
+    const CtapMakeCredentialRequest& request,
+    const FidoAuthenticator* authenticator) {
+  const auto& opt_options = authenticator->Options();
+  if (request.user_verification() != UserVerificationRequirement::kRequired ||
+      !opt_options ||
+      opt_options->user_verification_availability ==
+          AuthenticatorSupportedOptions::UserVerificationAvailability::
+              kSupportedAndConfigured) {
+    return ClientPinAvailability::kNotSupported;
+  }
+
+  return opt_options->client_pin_availability;
+}
+
 void MakeCredentialRequestHandler::DispatchRequest(
     FidoAuthenticator* authenticator) {
-  if (!CheckIfAuthenticatorSelectionCriteriaAreSatisfied(
-          authenticator, authenticator_selection_criteria_))
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+  if (state_ != State::kWaitingForTouch ||
+      !CheckIfAuthenticatorSelectionCriteriaAreSatisfied(
+          authenticator, authenticator_selection_criteria_, observer())) {
     return;
+  }
 
   // Set the rk, uv and attachment fields, which were only initialized to
   // default values up to here.  TODO(martinkr): Initialize these fields earlier
@@ -119,16 +161,98 @@
   request_parameter_.SetAuthenticatorAttachment(
       authenticator_selection_criteria_.authenticator_attachment());
 
-  authenticator->MakeCredential(
-      request_parameter_,
-      base::BindOnce(&MakeCredentialRequestHandler::HandleResponse,
-                     weak_factory_.GetWeakPtr(), authenticator));
+  if (base::FeatureList::IsEnabled(device::kWebAuthPINSupport) &&
+      WillNeedPIN(request_parameter_, authenticator) !=
+          ClientPinAvailability::kNotSupported) {
+    // Set an empty pinAuth parameter to wait for a touch and then report an
+    // error.
+    CtapMakeCredentialRequest request(request_parameter_);
+    request.SetPinAuth({});
+    authenticator->MakeCredential(
+        request, base::BindOnce(&MakeCredentialRequestHandler::HandleResponse,
+                                weak_factory_.GetWeakPtr(), authenticator));
+  } else {
+    authenticator->MakeCredential(
+        request_parameter_,
+        base::BindOnce(&MakeCredentialRequestHandler::HandleResponse,
+                       weak_factory_.GetWeakPtr(), authenticator));
+  }
+}
+
+void MakeCredentialRequestHandler::AuthenticatorRemoved(
+    FidoDiscoveryBase* discovery,
+    FidoAuthenticator* authenticator) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+  FidoRequestHandlerBase::AuthenticatorRemoved(discovery, authenticator);
+
+  if (authenticator == authenticator_) {
+    authenticator_ = nullptr;
+    if (state_ == State::kWaitingForPIN || state_ == State::kWaitingForNewPIN ||
+        state_ == State::kWaitingForSecondTouch) {
+      state_ = State::kFinished;
+      std::move(completion_callback_)
+          .Run(FidoReturnCode::kAuthenticatorRemovedDuringPINEntry,
+               base::nullopt, base::nullopt);
+    }
+  }
 }
 
 void MakeCredentialRequestHandler::HandleResponse(
     FidoAuthenticator* authenticator,
     CtapDeviceResponseCode response_code,
     base::Optional<AuthenticatorMakeCredentialResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+  if (state_ != State::kWaitingForTouch &&
+      state_ != State::kWaitingForSecondTouch) {
+    return;
+  }
+
+  if (base::FeatureList::IsEnabled(device::kWebAuthPINSupport) &&
+      state_ == State::kWaitingForTouch) {
+    switch (WillNeedPIN(request_parameter_, authenticator)) {
+      case ClientPinAvailability::kSupportedAndPinSet:
+        // Will need to get PIN to handle this request.
+        if (response_code != CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid &&
+            response_code != CtapDeviceResponseCode::kCtap2ErrPinInvalid) {
+          VLOG(1) << "Expected invalid pinAuth error but got "
+                  << static_cast<int>(response_code);
+          return;
+        }
+        DCHECK(observer());
+        CancelActiveAuthenticators(authenticator->GetId());
+        state_ = State::kGettingRetries;
+        authenticator_ = authenticator;
+        authenticator_->GetRetries(
+            base::BindOnce(&MakeCredentialRequestHandler::OnRetriesResponse,
+                           weak_factory_.GetWeakPtr()));
+        return;
+
+      case ClientPinAvailability::kSupportedButPinNotSet:
+        // Will need to set a PIN to handle this request.
+        if (response_code != CtapDeviceResponseCode::kCtap2ErrPinNotSet) {
+          VLOG(1) << "Expected PIN-not-set error but got "
+                  << static_cast<int>(response_code);
+          return;
+        }
+        DCHECK(observer());
+        CancelActiveAuthenticators(authenticator->GetId());
+        state_ = State::kWaitingForNewPIN;
+        authenticator_ = authenticator;
+        observer()->CollectPIN(
+            base::nullopt,
+            base::BindOnce(&MakeCredentialRequestHandler::OnHavePIN,
+                           weak_factory_.GetWeakPtr()));
+        return;
+
+      case ClientPinAvailability::kNotSupported:
+        // No PIN needed for this request.
+        break;
+    }
+  }
+
+  state_ = State::kFinished;
   if (response_code != CtapDeviceResponseCode::kSuccess) {
     OnAuthenticatorResponse(authenticator, response_code, base::nullopt);
     return;
@@ -146,8 +270,161 @@
   OnAuthenticatorResponse(authenticator, response_code, std::move(response));
 }
 
+void MakeCredentialRequestHandler::OnHavePIN(std::string pin) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK(state_ == State::kWaitingForPIN || state_ == State::kWaitingForNewPIN);
+  DCHECK(pin::IsValid(pin));
+
+  if (authenticator_ == nullptr) {
+    // Authenticator was detached. The request will already have been canceled
+    // but this callback may have been waiting in a queue.
+    DCHECK(!completion_callback_);
+    return;
+  }
+
+  if (state_ == State::kWaitingForPIN) {
+    state_ = State::kGetEphemeralKey;
+  } else {
+    DCHECK_EQ(state_, State::kWaitingForNewPIN);
+    state_ = State::kGetEphemeralKeyForNewPIN;
+  }
+
+  authenticator_->GetEphemeralKey(
+      base::BindOnce(&MakeCredentialRequestHandler::OnHaveEphemeralKey,
+                     weak_factory_.GetWeakPtr(), std::move(pin)));
+}
+
+void MakeCredentialRequestHandler::OnRetriesResponse(
+    CtapDeviceResponseCode status,
+    base::Optional<pin::RetriesResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK_EQ(state_, State::kGettingRetries);
+
+  if (status == CtapDeviceResponseCode::kSuccess && !response) {
+    status = CtapDeviceResponseCode::kCtap2ErrInvalidCBOR;
+  }
+
+  if (status != CtapDeviceResponseCode::kSuccess) {
+    state_ = State::kFinished;
+    FidoReturnCode ret = FidoReturnCode::kAuthenticatorResponseInvalid;
+    if (status == CtapDeviceResponseCode::kCtap2ErrPinBlocked) {
+      ret = FidoReturnCode::kHardPINBlock;
+    }
+    std::move(completion_callback_).Run(ret, base::nullopt, base::nullopt);
+    return;
+  }
+
+  state_ = State::kWaitingForPIN;
+  observer()->CollectPIN(
+      response->retries,
+      base::BindOnce(&MakeCredentialRequestHandler::OnHavePIN,
+                     weak_factory_.GetWeakPtr()));
+}
+
+void MakeCredentialRequestHandler::OnHaveEphemeralKey(
+    std::string pin,
+    CtapDeviceResponseCode status,
+    base::Optional<pin::KeyAgreementResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK(state_ == State::kGetEphemeralKey ||
+         state_ == State::kGetEphemeralKeyForNewPIN);
+
+  if (status == CtapDeviceResponseCode::kSuccess && !response) {
+    status = CtapDeviceResponseCode::kCtap2ErrInvalidCBOR;
+  }
+
+  if (status != CtapDeviceResponseCode::kSuccess) {
+    state_ = State::kFinished;
+    std::move(completion_callback_)
+        .Run(FidoReturnCode::kAuthenticatorResponseInvalid, base::nullopt,
+             base::nullopt);
+    return;
+  }
+
+  if (state_ == State::kGetEphemeralKey) {
+    state_ = State::kRequestWithPIN;
+    authenticator_->GetPINToken(
+        std::move(pin), *response,
+        base::BindOnce(&MakeCredentialRequestHandler::OnHavePINToken,
+                       weak_factory_.GetWeakPtr()));
+  } else {
+    DCHECK_EQ(state_, State::kGetEphemeralKeyForNewPIN);
+    state_ = State::kSettingPIN;
+    authenticator_->SetPIN(
+        pin, *response,
+        base::BindOnce(&MakeCredentialRequestHandler::OnHaveSetPIN,
+                       weak_factory_.GetWeakPtr(), pin, *response));
+  }
+}
+
+void MakeCredentialRequestHandler::OnHaveSetPIN(
+    std::string pin,
+    pin::KeyAgreementResponse key_agreement,
+    CtapDeviceResponseCode status,
+    base::Optional<pin::EmptyResponse> response) {
+  DCHECK_EQ(state_, State::kSettingPIN);
+
+  // Having just set the PIN, we need to immediately turn around and use it to
+  // get a PIN token.
+  state_ = State::kRequestWithPIN;
+  authenticator_->GetPINToken(
+      std::move(pin), key_agreement,
+      base::BindOnce(&MakeCredentialRequestHandler::OnHavePINToken,
+                     weak_factory_.GetWeakPtr()));
+}
+
+void MakeCredentialRequestHandler::OnHavePINToken(
+    CtapDeviceResponseCode status,
+    base::Optional<pin::TokenResponse> response) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+  DCHECK_EQ(state_, State::kRequestWithPIN);
+
+  if (status == CtapDeviceResponseCode::kCtap2ErrPinInvalid) {
+    state_ = State::kGettingRetries;
+    authenticator_->GetRetries(
+        base::BindOnce(&MakeCredentialRequestHandler::OnRetriesResponse,
+                       weak_factory_.GetWeakPtr()));
+    return;
+  }
+
+  if (status == CtapDeviceResponseCode::kSuccess && !response) {
+    status = CtapDeviceResponseCode::kCtap2ErrInvalidCBOR;
+  }
+
+  if (status != CtapDeviceResponseCode::kSuccess) {
+    state_ = State::kFinished;
+    FidoReturnCode ret;
+    switch (status) {
+      case CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked:
+        ret = FidoReturnCode::kSoftPINBlock;
+        break;
+      case CtapDeviceResponseCode::kCtap2ErrPinBlocked:
+        ret = FidoReturnCode::kHardPINBlock;
+        break;
+      default:
+        ret = FidoReturnCode::kAuthenticatorResponseInvalid;
+        break;
+    }
+    std::move(completion_callback_).Run(ret, base::nullopt, base::nullopt);
+    return;
+  }
+
+  observer()->FinishCollectPIN();
+  state_ = State::kWaitingForSecondTouch;
+  request_parameter_.SetPinAuth(
+      response->PinAuth(request_parameter_.client_data_hash()));
+  request_parameter_.SetPinProtocol(pin::kProtocolVersion);
+
+  authenticator_->MakeCredential(
+      request_parameter_,
+      base::BindOnce(&MakeCredentialRequestHandler::HandleResponse,
+                     weak_factory_.GetWeakPtr(), authenticator_));
+}
+
 void MakeCredentialRequestHandler::SetPlatformAuthenticatorOrMarkUnavailable(
     base::Optional<PlatformAuthenticatorInfo> platform_authenticator_info) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
   if (platform_authenticator_info) {
     // TODO(crbug.com/873710): In the case of a request with
     // AuthenticatorAttachment::kAny and when there is no embedder-provided
diff --git a/device/fido/make_credential_request_handler.h b/device/fido/make_credential_request_handler.h
index 20a3ae17..241a755e 100644
--- a/device/fido/make_credential_request_handler.h
+++ b/device/fido/make_credential_request_handler.h
@@ -6,12 +6,14 @@
 #define DEVICE_FIDO_MAKE_CREDENTIAL_REQUEST_HANDLER_H_
 
 #include <memory>
+#include <string>
 
 #include "base/callback.h"
 #include "base/component_export.h"
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "base/optional.h"
+#include "base/sequence_checker.h"
 #include "device/fido/authenticator_selection_criteria.h"
 #include "device/fido/ctap_make_credential_request.h"
 #include "device/fido/fido_constants.h"
@@ -44,16 +46,51 @@
       override;
 
  private:
+  enum class State {
+    kWaitingForTouch,
+    kWaitingForSecondTouch,
+    kGettingRetries,
+    kWaitingForPIN,
+    kWaitingForNewPIN,
+    kGetEphemeralKey,
+    kGetEphemeralKeyForNewPIN,
+    kSettingPIN,
+    kRequestWithPIN,
+    kFinished,
+  };
+
   // FidoRequestHandlerBase:
   void DispatchRequest(FidoAuthenticator* authenticator) override;
+  void AuthenticatorRemoved(FidoDiscoveryBase* discovery,
+                            FidoAuthenticator* authenticator) override;
 
   void HandleResponse(
       FidoAuthenticator* authenticator,
       CtapDeviceResponseCode response_code,
       base::Optional<AuthenticatorMakeCredentialResponse> response);
+  void OnHavePIN(std::string pin);
+  void OnRetriesResponse(CtapDeviceResponseCode status,
+                         base::Optional<pin::RetriesResponse> response);
+  void OnHaveEphemeralKey(std::string pin,
+                          CtapDeviceResponseCode status,
+                          base::Optional<pin::KeyAgreementResponse> response);
+  void OnHaveSetPIN(std::string pin,
+                    pin::KeyAgreementResponse key_agreement,
+                    CtapDeviceResponseCode status,
+                    base::Optional<pin::EmptyResponse> response);
+  void OnHavePINToken(CtapDeviceResponseCode status,
+                      base::Optional<pin::TokenResponse> response);
 
+  State state_ = State::kWaitingForTouch;
   CtapMakeCredentialRequest request_parameter_;
   AuthenticatorSelectionCriteria authenticator_selection_criteria_;
+  // authenticator_ points to the authenticator that will be used for this
+  // operation. It's only set after the user touches an authenticator to select
+  // it, after which point that authenticator will be used exclusively through
+  // requesting PIN etc. The object is owned by the underlying discovery object
+  // and this pointer is cleared if it's removed during processing.
+  FidoAuthenticator* authenticator_ = nullptr;
+  SEQUENCE_CHECKER(my_sequence_checker_);
   base::WeakPtrFactory<MakeCredentialRequestHandler> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(MakeCredentialRequestHandler);
diff --git a/device/fido/make_credential_task.cc b/device/fido/make_credential_task.cc
index c24efb3..218049b9 100644
--- a/device/fido/make_credential_task.cc
+++ b/device/fido/make_credential_task.cc
@@ -72,7 +72,7 @@
 void MakeCredentialTask::MakeCredential() {
   register_operation_ = std::make_unique<Ctap2DeviceOperation<
       CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
-      device(), request_parameter_, std::move(callback_),
+      device(), std::move(request_parameter_), std::move(callback_),
       base::BindOnce(&ReadCTAPMakeCredentialResponse,
                      device()->DeviceTransport()));
   register_operation_->Start();
@@ -87,7 +87,7 @@
 
   DCHECK_EQ(ProtocolVersion::kU2f, device()->supported_protocol());
   register_operation_ = std::make_unique<U2fRegisterOperation>(
-      device(), request_parameter_,
+      device(), std::move(request_parameter_),
       base::BindOnce(&MakeCredentialTask::MaybeRevertU2fFallback,
                      weak_factory_.GetWeakPtr()));
   register_operation_->Start();
diff --git a/device/fido/pin.cc b/device/fido/pin.cc
index 60041407..2d12008 100644
--- a/device/fido/pin.cc
+++ b/device/fido/pin.cc
@@ -90,8 +90,13 @@
     return base::nullopt;
   }
 
+  const int64_t retries = it->second.GetUnsigned();
+  if (retries > INT_MAX) {
+    return base::nullopt;
+  }
+
   RetriesResponse ret;
-  ret.retries = it->second.GetUnsigned();
+  ret.retries = static_cast<int>(retries);
   return ret;
 }
 
@@ -399,6 +404,8 @@
 
 TokenRequest::~TokenRequest() = default;
 
+TokenRequest::TokenRequest(TokenRequest&& other) = default;
+
 const std::array<uint8_t, 32>& TokenRequest::shared_key() const {
   return shared_key_;
 }
diff --git a/device/fido/pin.h b/device/fido/pin.h
index 352639e8..0d13397 100644
--- a/device/fido/pin.h
+++ b/device/fido/pin.h
@@ -22,6 +22,10 @@
 namespace device {
 namespace pin {
 
+// kProtocolVersion is the version of the PIN protocol that this code
+// implements.
+constexpr int kProtocolVersion = 1;
+
 // IsValid returns true if |pin|, which must be UTF-8, is a syntactically valid
 // PIN.
 bool IsValid(const std::string& pin);
@@ -48,7 +52,7 @@
 
   // retries is the number of PIN attempts remaining before the authenticator
   // locks.
-  int64_t retries;
+  int retries;
 
  private:
   RetriesResponse();
@@ -129,6 +133,7 @@
  public:
   TokenRequest(const std::string& pin, const KeyAgreementResponse& peer_key);
   ~TokenRequest();
+  TokenRequest(TokenRequest&&);
   TokenRequest(const TokenRequest&) = delete;
 
   // shared_key returns the shared ECDH key that was used to encrypt the PIN.
@@ -139,7 +144,7 @@
 
  private:
   std::array<uint8_t, 32> shared_key_;
-  const cbor::Value::MapValue cose_key_;
+  cbor::Value::MapValue cose_key_;
   uint8_t pin_hash_[16];
 };
 
diff --git a/device/fido/pin_internal.h b/device/fido/pin_internal.h
index 870c6bb..105b85d 100644
--- a/device/fido/pin_internal.h
+++ b/device/fido/pin_internal.h
@@ -19,10 +19,6 @@
 namespace device {
 namespace pin {
 
-// kProtocolVersion is the version of the PIN protocol that this code
-// implements.
-constexpr int kProtocolVersion = 1;
-
 // Subcommand enumerates the subcommands to the main |authenticatorClientPIN|
 // command. See
 // https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#authenticatorClientPIN
diff --git a/device/fido/u2f_register_operation.h b/device/fido/u2f_register_operation.h
index e5dab24..cf423f9 100644
--- a/device/fido/u2f_register_operation.h
+++ b/device/fido/u2f_register_operation.h
@@ -13,12 +13,12 @@
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "base/optional.h"
+#include "device/fido/ctap_make_credential_request.h"
 #include "device/fido/device_operation.h"
 
 namespace device {
 
 class FidoDevice;
-class CtapMakeCredentialRequest;
 class AuthenticatorMakeCredentialResponse;
 class PublicKeyCredentialDescriptor;
 
diff --git a/device/fido/u2f_sign_operation.h b/device/fido/u2f_sign_operation.h
index 70271a48..99e10b9b 100644
--- a/device/fido/u2f_sign_operation.h
+++ b/device/fido/u2f_sign_operation.h
@@ -15,13 +15,13 @@
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "base/optional.h"
+#include "device/fido/ctap_get_assertion_request.h"
 #include "device/fido/device_operation.h"
 #include "device/fido/fido_constants.h"
 
 namespace device {
 
 class FidoDevice;
-class CtapGetAssertionRequest;
 class AuthenticatorGetAssertionResponse;
 
 // Represents per device authentication logic for U2F tokens. Handles iterating