[DBSC] Bind refresh token only when the server requests it

The server is adding a new "eligible_for_token_binding" parameter to the
DICE SIGNIN response header. When this parameter is not set, Chrome
shouldn't attempt to bind the refresh token.

When "eligible_for_token_binding" is set, its value will contain a list
of supported algorithms separated by space:

eligible_for_token_binding=ES256 RS256

Fixed: b:356938919
Change-Id: I0b0988219c17110deb11d7eb8c6005498d469f5e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5816854
Commit-Queue: Alex Ilin <alexilin@chromium.org>
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: David Roger <droger@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1351695}
diff --git a/chrome/browser/signin/dice_browsertest.cc b/chrome/browser/signin/dice_browsertest.cc
index ee8c686..f9fc7c38 100644
--- a/chrome/browser/signin/dice_browsertest.cc
+++ b/chrome/browser/signin/dice_browsertest.cc
@@ -240,7 +240,8 @@
       http_response->AddCustomHeader(
           kDiceResponseHeader,
           base::StringPrintf(
-              "action=SIGNIN,authuser=1,id=%s,email=%s,authorization_code=%s",
+              "action=SIGNIN,authuser=1,id=%s,email=%s,authorization_code=%s,"
+              "eligible_for_token_binding=ES256 RS256",
               signin::GetTestGaiaIdForEmail(main_email).c_str(),
               main_email.c_str(), kAuthorizationCode));
     }
@@ -817,8 +818,10 @@
   SendRefreshTokenResponse();
   EXPECT_TRUE(
       GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
-  // TODO(http://b/274463812): verify that the refresh token is bound once the
-  // binding status is propagated to `IdentityManager`.
+  EXPECT_FALSE(
+      GetIdentityManager()
+          ->GetWrappedBindingKeyOfRefreshTokenForAccount(GetMainAccountID())
+          .empty());
 }
 #endif  // BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
 
diff --git a/chrome/browser/signin/dice_response_handler.cc b/chrome/browser/signin/dice_response_handler.cc
index 21854f18..6c9adc6 100644
--- a/chrome/browser/signin/dice_response_handler.cc
+++ b/chrome/browser/signin/dice_response_handler.cc
@@ -368,7 +368,9 @@
           dice_params.signin_info->account_info;
       ProcessDiceSigninHeader(
           info.gaia_id, info.email, dice_params.signin_info->authorization_code,
-          dice_params.signin_info->no_authorization_code, std::move(delegate));
+          dice_params.signin_info->no_authorization_code,
+          dice_params.signin_info->supported_algorithms_for_token_binding,
+          std::move(delegate));
       return;
     }
     case signin::DiceAction::ENABLE_SYNC: {
@@ -415,6 +417,7 @@
     const std::string& email,
     const std::string& authorization_code,
     bool no_authorization_code,
+    const std::string& supported_algorithms_for_token_binding,
     std::unique_ptr<ProcessDiceHeaderDelegate> delegate) {
   if (no_authorization_code) {
     lock_ = std::make_unique<AccountReconcilor::Lock>(account_reconcilor_);
@@ -464,9 +467,12 @@
 
 #if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
   if (!registration_token_helper_factory_.is_null() &&
-      !registration_token_helper_) {
+      !registration_token_helper_ &&
+      !supported_algorithms_for_token_binding.empty()) {
     CHECK(switches::IsChromeRefreshTokenBindingEnabled(
         signin_client_->GetPrefs()));
+    // TODO(crbug.com/362480455): pass the list of supported algorithms to the
+    // helper factory once supported.
     registration_token_helper_ = registration_token_helper_factory_.Run(
         GetWrappedBindingKeyToReuse(*identity_manager_));
   }
@@ -476,7 +482,13 @@
       gaia_id, email, authorization_code, signin_client_, account_reconcilor_,
       std::move(delegate),
 #if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
-      registration_token_helper_.get(),
+      // It's important to check `supported_algorithms_for_token_binding` here
+      // in addition to the factory call above because
+      // `registration_token_helper_` might be shared between several
+      // `DiceTokenFetcher`s.
+      supported_algorithms_for_token_binding.empty()
+          ? nullptr
+          : registration_token_helper_.get(),
 #endif  // BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
       this));
 }
diff --git a/chrome/browser/signin/dice_response_handler.h b/chrome/browser/signin/dice_response_handler.h
index 96f7b57..52cde41 100644
--- a/chrome/browser/signin/dice_response_handler.h
+++ b/chrome/browser/signin/dice_response_handler.h
@@ -204,6 +204,7 @@
       const std::string& email,
       const std::string& authorization_code,
       bool no_authorization_code,
+      const std::string& supported_algorithms_for_token_binding,
       std::unique_ptr<ProcessDiceHeaderDelegate> delegate);
 
   // Process the Dice enable sync action.
diff --git a/chrome/browser/signin/dice_response_handler_unittest.cc b/chrome/browser/signin/dice_response_handler_unittest.cc
index 7d0800c..737e44be 100644
--- a/chrome/browser/signin/dice_response_handler_unittest.cc
+++ b/chrome/browser/signin/dice_response_handler_unittest.cc
@@ -61,6 +61,7 @@
 namespace {
 
 const char kAuthorizationCode[] = "authorization_code";
+const char kEligibleForTokenBinding[] = "ES256 RS256";
 const char kEmail[] = "test@email.com";
 const int kSessionIndex = 42;
 
@@ -212,6 +213,8 @@
             std::make_unique<DiceResponseParams::SigninInfo>();
         dice_params.signin_info->account_info = account_info;
         dice_params.signin_info->authorization_code = kAuthorizationCode;
+        dice_params.signin_info->supported_algorithms_for_token_binding =
+            kEligibleForTokenBinding;
         break;
       case DiceAction::ENABLE_SYNC:
         dice_params.enable_sync_info =
@@ -499,6 +502,36 @@
   EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
 }
 
+// Checks that no token binding attempt is made when an account is ineligible
+// for token binding.
+TEST_F(DiceResponseHandlerTest, SigninIneligibleForTokenBinding) {
+  EnableRegistrationTokenHelperFactory();
+  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+  dice_params.signin_info->supported_algorithms_for_token_binding.clear();
+  const auto& account_info = dice_params.signin_info->account_info;
+  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+      account_info.gaia_id, account_info.email);
+  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+
+  dice_response_handler_->ProcessDiceHeader(
+      dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+  // Check that a GaiaAuthFetcher has been created immediately.
+  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+  ASSERT_THAT(consumer, testing::NotNull());
+  // Simulate GaiaAuthFetcher success.
+  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+      "refresh_token", "access_token", 10, /*is_child_account=*/false,
+      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
+  // Check that the token has been inserted in the token service and it is
+  // unbound.
+  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+  EXPECT_TRUE(identity_manager()
+                  ->GetWrappedBindingKeyOfRefreshTokenForAccount(account_id)
+                  .empty());
+  EXPECT_TRUE(auth_error_email_.empty());
+  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
+}
+
 TEST_F(DiceResponseHandlerTest, ReuseBindingKeyOtherTokenIsBound) {
   EnableRegistrationTokenHelperFactory();
   const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
@@ -629,6 +662,47 @@
             kWrappedKey);
 }
 
+TEST_F(DiceResponseHandlerTest, TwoFetchersOneEligible) {
+  EnableRegistrationTokenHelperFactory();
+  auto authorization_code = [&](const DiceResponseParams& dice_params) {
+    return dice_params.signin_info->authorization_code;
+  };
+
+  DiceResponseParams eligible_dice_params_ = MakeDiceParams(DiceAction::SIGNIN);
+  DiceResponseParams ineligible_dice_params =
+      MakeDiceParams(DiceAction::SIGNIN);
+  ineligible_dice_params.signin_info->account_info =
+      GetDiceResponseParamsAccountInfo("other@email.com");
+  ineligible_dice_params.signin_info->authorization_code =
+      "other_authorization_code";
+  ineligible_dice_params.signin_info->supported_algorithms_for_token_binding
+      .clear();
+  ExpectRegistrationTokenHelperCreated(
+      {authorization_code(eligible_dice_params_)},
+      /*expected_wrapped_binding_key=*/{});
+
+  dice_response_handler_->ProcessDiceHeader(
+      eligible_dice_params_,
+      std::make_unique<TestProcessDiceHeaderDelegate>(this));
+  // Token fetch should be blocked on the binding registration token generation.
+  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());
+
+  dice_response_handler_->ProcessDiceHeader(
+      ineligible_dice_params,
+      std::make_unique<TestProcessDiceHeaderDelegate>(this));
+  // Token fetch should start immediately for ineligible account.
+  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull());
+
+  // Simulate successful token generation and check that GaiaAuthFetcher has
+  // been created.
+  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
+  SimulateRegistrationTokenHelperResult(
+      authorization_code(eligible_dice_params_),
+      RegistrationTokenHelper::Result(unexportable_keys::UnexportableKeyId(),
+                                      kWrappedKey, "test_registration_token"));
+  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull());
+}
+
 TEST_F(DiceResponseHandlerTest,
        NewRegistrationTokenHelperCreatedForConsecutiveFetchers) {
   EnableRegistrationTokenHelperFactory();
diff --git a/components/signin/core/browser/dice_header_helper.cc b/components/signin/core/browser/dice_header_helper.cc
index 2031493..530f695 100644
--- a/components/signin/core/browser/dice_header_helper.cc
+++ b/components/signin/core/browser/dice_header_helper.cc
@@ -30,6 +30,8 @@
 const char kSigninNoAuthorizationCodeAttrName[] = "no_authorization_code";
 const char kSigninEmailAttrName[] = "email";
 const char kSigninIdAttrName[] = "id";
+const char kSigninEligibleForTokenBindingAttrName[] =
+    "eligible_for_token_binding";
 
 // Signout response parameters.
 const char kSignoutEmailAttrName[] = "email";
@@ -110,6 +112,13 @@
         DLOG(WARNING)
             << "No authorization code header expected only with SIGNIN action";
       }
+    } else if (key_name == kSigninEligibleForTokenBindingAttrName) {
+      if (params.signin_info) {
+        params.signin_info->supported_algorithms_for_token_binding = value;
+      } else {
+        DLOG(WARNING) << "Eligible for token binding attribute expected only "
+                         "with SIGNIN action";
+      }
     } else {
       DLOG(WARNING) << "Unexpected Gaia header attribute '" << key_name << "'.";
     }
diff --git a/components/signin/core/browser/signin_header_helper.h b/components/signin/core/browser/signin_header_helper.h
index 5f4994d..fccd542 100644
--- a/components/signin/core/browser/signin_header_helper.h
+++ b/components/signin/core/browser/signin_header_helper.h
@@ -129,6 +129,9 @@
     // Whether Dice response contains the 'no_authorization_code' header value.
     // If true then LSO was unavailable for provision of auth code.
     bool no_authorization_code = false;
+    // If the account is eligible for token binding, this string is non-empty
+    // and contains a list of supported binding algorithms separated by space.
+    std::string supported_algorithms_for_token_binding;
   };
 
   // Parameters for the SIGNOUT action.
diff --git a/components/signin/core/browser/signin_header_helper_unittest.cc b/components/signin/core/browser/signin_header_helper_unittest.cc
index 81bc44d9..ae0ffcd 100644
--- a/components/signin/core/browser/signin_header_helper_unittest.cc
+++ b/components/signin/core/browser/signin_header_helper_unittest.cc
@@ -548,6 +548,7 @@
   const char kAuthorizationCode[] = "authorization_code";
   const char kEmail[] = "foo@example.com";
   const char kGaiaID[] = "gaia_id";
+  const char kSupportedTokenBindingAlgorithms[] = "ES256 RS256";
   const int kSessionIndex = 42;
 
   {
@@ -555,14 +556,18 @@
     base::HistogramTester histogram_tester;
     DiceResponseParams params =
         BuildDiceSigninResponseParams(base::StringPrintf(
-            "action=SIGNIN,id=%s,email=%s,authuser=%i,authorization_code=%s",
-            kGaiaID, kEmail, kSessionIndex, kAuthorizationCode));
+            "action=SIGNIN,id=%s,email=%s,authuser=%i,authorization_code=%s,"
+            "eligible_for_token_binding=%s",
+            kGaiaID, kEmail, kSessionIndex, kAuthorizationCode,
+            kSupportedTokenBindingAlgorithms));
     EXPECT_EQ(DiceAction::SIGNIN, params.user_intention);
     ASSERT_TRUE(params.signin_info);
     EXPECT_EQ(kGaiaID, params.signin_info->account_info.gaia_id);
     EXPECT_EQ(kEmail, params.signin_info->account_info.email);
     EXPECT_EQ(kSessionIndex, params.signin_info->account_info.session_index);
     EXPECT_EQ(kAuthorizationCode, params.signin_info->authorization_code);
+    EXPECT_EQ(kSupportedTokenBindingAlgorithms,
+              params.signin_info->supported_algorithms_for_token_binding);
     histogram_tester.ExpectUniqueSample("Signin.DiceAuthorizationCode", true,
                                         1);
   }
@@ -667,6 +672,47 @@
   }
 }
 
+TEST_F(SigninHeaderHelperTest,
+       BuildDiceSigninResponseParamsNotEligibleForTokenBinding) {
+  const char kAuthorizationCode[] = "authorization_code";
+  const char kEmail[] = "foo@example.com";
+  const char kGaiaID[] = "gaia_id";
+  const int kSessionIndex = 42;
+
+  // "eligible_for_token_binding" is missing.
+  DiceResponseParams params = BuildDiceSigninResponseParams(base::StringPrintf(
+      "action=SIGNIN,id=%s,email=%s,authuser=%i,authorization_code=%s", kGaiaID,
+      kEmail, kSessionIndex, kAuthorizationCode));
+  EXPECT_EQ(DiceAction::SIGNIN, params.user_intention);
+  ASSERT_TRUE(params.signin_info);
+  EXPECT_TRUE(
+      params.signin_info->supported_algorithms_for_token_binding.empty());
+}
+
+// Mainly tests that whitespace characters in the middle of a header don't break
+// parsing.
+TEST_F(SigninHeaderHelperTest, BuildDiceSigninResponseParamsMixedOrder) {
+  const char kAuthorizationCode[] = "authorization_code";
+  const char kEmail[] = "foo@example.com";
+  const char kGaiaID[] = "gaia_id";
+  const char kSupportedTokenBindingAlgorithms[] = "ES256 RS256";
+  const int kSessionIndex = 42;
+
+  DiceResponseParams params = BuildDiceSigninResponseParams(base::StringPrintf(
+      "id=%s,action=SIGNIN,authuser=%i,eligible_for_token_binding=%s,email=%s,"
+      "authorization_code=%s",
+      kGaiaID, kSessionIndex, kSupportedTokenBindingAlgorithms, kEmail,
+      kAuthorizationCode));
+  EXPECT_EQ(DiceAction::SIGNIN, params.user_intention);
+  ASSERT_TRUE(params.signin_info);
+  EXPECT_EQ(kGaiaID, params.signin_info->account_info.gaia_id);
+  EXPECT_EQ(kEmail, params.signin_info->account_info.email);
+  EXPECT_EQ(kSessionIndex, params.signin_info->account_info.session_index);
+  EXPECT_EQ(kAuthorizationCode, params.signin_info->authorization_code);
+  EXPECT_EQ(kSupportedTokenBindingAlgorithms,
+            params.signin_info->supported_algorithms_for_token_binding);
+}
+
 #endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)
 
 // Tests that the Mirror header request is returned normally when the redirect