buffet: Add RegistrationStatus::InvalidCredentials

If the device has been deleted from GCD then an oauth request will
return an error of invalid_grant. Make sure we handle this and set
the registration status to InvalidCredentials.

BUG=brillo:16
TEST=FEATURES=test emerge-${BOARD} buffet (I also tested this manually
by deregistering the device using
https://gcd.sandbox.google.com/clouddevices#) and
test_that <ipaddress> buffet_Registration buffet_BasicDBusAPI

Change-Id: I60aa68186d9c6f3a7ce812e4dddfda626ea2b42f
Reviewed-on: https://chromium-review.googlesource.com/254170
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Nathan Bullock <nathanbullock@google.com>
Tested-by: Nathan Bullock <nathanbullock@google.com>
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc
index e482d81..0b98cb7 100644
--- a/buffet/device_registration_info.cc
+++ b/buffet/device_registration_info.cc
@@ -66,27 +66,6 @@
   return {chromeos::http::request_header::kAuthorization, authorization};
 }
 
-std::unique_ptr<base::DictionaryValue> ParseOAuthResponse(
-    const chromeos::http::Response* response, chromeos::ErrorPtr* error) {
-  int code = 0;
-  auto resp = chromeos::http::ParseJsonResponse(response, &code, error);
-  if (resp && code >= chromeos::http::status_code::BadRequest) {
-    if (error) {
-      std::string error_code, error_message;
-      if (resp->GetString("error", &error_code) &&
-          resp->GetString("error_description", &error_message)) {
-        chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainOAuth2,
-                               error_code, error_message);
-      } else {
-        chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainOAuth2,
-                               "unexpected_response", "Unexpected OAuth error");
-      }
-    }
-    return std::unique_ptr<base::DictionaryValue>();
-  }
-  return resp;
-}
-
 inline void SetUnexpectedError(chromeos::ErrorPtr* error) {
   chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainGCD,
                          "unexpected_response", "Unexpected GCD error");
@@ -94,9 +73,6 @@
 
 void ParseGCDError(const base::DictionaryValue* json,
                    chromeos::ErrorPtr* error) {
-  if (!error)
-    return;
-
   const base::Value* list_value = nullptr;
   const base::ListValue* error_list = nullptr;
   if (!json->Get("error.errors", &list_value) ||
@@ -310,6 +286,31 @@
   return have_credentials;
 }
 
+std::unique_ptr<base::DictionaryValue>
+DeviceRegistrationInfo::ParseOAuthResponse(
+    const chromeos::http::Response* response, chromeos::ErrorPtr* error) {
+  int code = 0;
+  auto resp = chromeos::http::ParseJsonResponse(response, &code, error);
+  if (resp && code >= chromeos::http::status_code::BadRequest) {
+    std::string error_code, error_message;
+    if (!resp->GetString("error", &error_code)) {
+      error_code = "unexpected_response";
+    }
+    if (error_code == "invalid_grant") {
+      LOG(INFO) << "The device's registration has been revoked.";
+      SetRegistrationStatus(RegistrationStatus::kInvalidCredentials);
+    }
+    // I have never actually seen an error_description returned.
+    if (!resp->GetString("error_description", &error_message)) {
+      error_message = "Unexpected OAuth error";
+    }
+    chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainOAuth2,
+                           error_code, error_message);
+    return std::unique_ptr<base::DictionaryValue>();
+  }
+  return resp;
+}
+
 bool DeviceRegistrationInfo::ValidateAndRefreshAccessToken(
     chromeos::ErrorPtr* error) {
   LOG(INFO) << "Checking access token expiration.";
diff --git a/buffet/device_registration_info.h b/buffet/device_registration_info.h
index 95c8b0d..7b2e3da 100644
--- a/buffet/device_registration_info.h
+++ b/buffet/device_registration_info.h
@@ -150,6 +150,11 @@
   // Makes sure the access token is available and up-to-date.
   bool ValidateAndRefreshAccessToken(chromeos::ErrorPtr* error);
 
+  // Parse the OAuth response, and sets registration status to
+  // kInvalidCredentials if our registration is no longer valid.
+  std::unique_ptr<base::DictionaryValue> ParseOAuthResponse(
+      const chromeos::http::Response* response, chromeos::ErrorPtr* error);
+
   // This attempts to open the XMPP channel. The XMPP channel needs to be
   // restarted anytime the access_token is refreshed.
   void StartXmpp();
diff --git a/buffet/device_registration_info_unittest.cc b/buffet/device_registration_info_unittest.cc
index dee5614..2912fb6 100644
--- a/buffet/device_registration_info_unittest.cc
+++ b/buffet/device_registration_info_unittest.cc
@@ -124,6 +124,28 @@
   response->ReplyJson(chromeos::http::status_code::Ok, &json);
 }
 
+void OAuth2HandlerFail(const ServerRequest& request,
+                       ServerResponse* response) {
+  base::DictionaryValue json;
+  EXPECT_EQ("refresh_token", request.GetFormField("grant_type"));
+  EXPECT_EQ(test_data::kRefreshToken, request.GetFormField("refresh_token"));
+  EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id"));
+  EXPECT_EQ(test_data::kClientSecret, request.GetFormField("client_secret"));
+  json.SetString("error", "unable_to_authenticate");
+  response->ReplyJson(chromeos::http::status_code::BadRequest, &json);
+}
+
+void OAuth2HandlerDeregister(const ServerRequest& request,
+                             ServerResponse* response) {
+  base::DictionaryValue json;
+  EXPECT_EQ("refresh_token", request.GetFormField("grant_type"));
+  EXPECT_EQ(test_data::kRefreshToken, request.GetFormField("refresh_token"));
+  EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id"));
+  EXPECT_EQ(test_data::kClientSecret, request.GetFormField("client_secret"));
+  json.SetString("error", "invalid_grant");
+  response->ReplyJson(chromeos::http::status_code::BadRequest, &json);
+}
+
 void DeviceInfoHandler(const ServerRequest& request, ServerResponse* response) {
   std::string auth = "Bearer ";
   auth += test_data::kAccessToken;
@@ -293,6 +315,46 @@
   EXPECT_EQ(1, transport_->GetRequestCount());
 }
 
+TEST_F(DeviceRegistrationInfoTest, CheckAuthenticationFailure) {
+  SetDefaultDeviceRegistration(&data_);
+  storage_->Save(&data_);
+  EXPECT_TRUE(dev_reg_->Load());
+  EXPECT_EQ(RegistrationStatus::kOffline,
+            dev_reg_->GetRegistrationStatus());
+
+  transport_->AddHandler(dev_reg_->GetOAuthURL("token"),
+                         chromeos::http::request_type::kPost,
+                         base::Bind(OAuth2HandlerFail));
+  transport_->ResetRequestCount();
+  chromeos::ErrorPtr error;
+  EXPECT_FALSE(dev_reg_->CheckRegistration(&error));
+  EXPECT_EQ(1, transport_->GetRequestCount());
+  EXPECT_TRUE(error->HasError(buffet::kErrorDomainOAuth2,
+                              "unable_to_authenticate"));
+  EXPECT_EQ(RegistrationStatus::kOffline,
+            dev_reg_->GetRegistrationStatus());
+}
+
+TEST_F(DeviceRegistrationInfoTest, CheckDeregistration) {
+  SetDefaultDeviceRegistration(&data_);
+  storage_->Save(&data_);
+  EXPECT_TRUE(dev_reg_->Load());
+  EXPECT_EQ(RegistrationStatus::kOffline,
+            dev_reg_->GetRegistrationStatus());
+
+  transport_->AddHandler(dev_reg_->GetOAuthURL("token"),
+                         chromeos::http::request_type::kPost,
+                         base::Bind(OAuth2HandlerDeregister));
+  transport_->ResetRequestCount();
+  chromeos::ErrorPtr error;
+  EXPECT_FALSE(dev_reg_->CheckRegistration(&error));
+  EXPECT_EQ(1, transport_->GetRequestCount());
+  EXPECT_TRUE(error->HasError(buffet::kErrorDomainOAuth2,
+                              "invalid_grant"));
+  EXPECT_EQ(RegistrationStatus::kInvalidCredentials,
+            dev_reg_->GetRegistrationStatus());
+}
+
 TEST_F(DeviceRegistrationInfoTest, GetDeviceInfo) {
   SetDefaultDeviceRegistration(&data_);
   storage_->Save(&data_);
diff --git a/buffet/registration_status.cc b/buffet/registration_status.cc
index 4fb1721..3b44ae3 100644
--- a/buffet/registration_status.cc
+++ b/buffet/registration_status.cc
@@ -18,6 +18,8 @@
       return "registering";
     case RegistrationStatus::kRegistered:
       return "registered";
+    case RegistrationStatus::kInvalidCredentials:
+      return "invalid_credentials";
   }
   return "unknown";
 }
diff --git a/buffet/registration_status.h b/buffet/registration_status.h
index fa2e2cb..eee0c81 100644
--- a/buffet/registration_status.h
+++ b/buffet/registration_status.h
@@ -16,6 +16,7 @@
   kUnregistered,  // We have no credentials.
   kRegistering,  // We've just been given credentials.
   kRegistered,  // We're registered and online.
+  kInvalidCredentials,  // Our registration has been revoked.
 };
 
 std::string StatusToString(RegistrationStatus status);