Add an experimental identity API for platform apps.

This adds a way for platform apps to access the user's signed into chrome account and generate an OAuth2 token:
 - chrome.experimental.identity.getAuthToken

BUG=none
TEST=ExtensionApiTest.Identity


Review URL: http://codereview.chromium.org/9474005

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@128591 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/common/common_resources.grd b/chrome/common/common_resources.grd
index 1c829ff7..b1e17d57 100644
--- a/chrome/common/common_resources.grd
+++ b/chrome/common/common_resources.grd
@@ -31,6 +31,7 @@
       <include name="IDR_EXTENSION_API_JSON_EXPERIMENTAL_DOWNLOADS" file="extensions\api\experimental.downloads.json" type="BINDATA" />
       <include name="IDR_EXTENSION_API_JSON_EXPERIMENTAL_EXTENSION" file="extensions\api\experimental.extension.json" type="BINDATA" />
       <include name="IDR_EXTENSION_API_JSON_EXPERIMENTAL_FONTSSETTINGS" file="extensions\api\experimental.fontSettings.json" type="BINDATA" />
+      <include name="IDR_EXTENSION_API_JSON_EXPERIMENTAL_IDENTITY" file="extensions\api\experimental.identity.json" type="BINDATA" />
       <include name="IDR_EXTENSION_API_JSON_EXPERIMENTAL_INFOBARS" file="extensions\api\experimental.infobars.json" type="BINDATA" />
       <include name="IDR_EXTENSION_API_JSON_EXPERIMENTAL_INPUT_UI" file="extensions\api\experimental.input.ui.json" type="BINDATA" />
       <include name="IDR_EXTENSION_API_JSON_EXPERIMENTAL_INPUT_VIRTUALKEYBOARD" file="extensions\api\experimental.input.virtualKeyboard.json" type="BINDATA" />
diff --git a/chrome/common/extensions/api/_manifest_features.json b/chrome/common/extensions/api/_manifest_features.json
index bad73fd8..9823c27 100644
--- a/chrome/common/extensions/api/_manifest_features.json
+++ b/chrome/common/extensions/api/_manifest_features.json
@@ -107,6 +107,9 @@
       "extension", "packaged_app", "hosted_app", "platform_app"
     ]
   },
+  "oauth2": {
+    "extension_types": ["platform_app"]
+  },
   "page_action": {
     "extension_types": ["extension"]
   },
diff --git a/chrome/common/extensions/api/experimental.identity.json b/chrome/common/extensions/api/experimental.identity.json
new file mode 100644
index 0000000..14fb881
--- /dev/null
+++ b/chrome/common/extensions/api/experimental.identity.json
@@ -0,0 +1,28 @@
+[
+  {
+    "namespace": "experimental.identity",
+    "nodoc": true,
+    "types": [],
+    "events": [],
+    "functions": [
+      {
+        "name": "getAuthToken",
+        "type": "function",
+        "description": "Gets an OAuth2 access token as specified by the manifest.",
+        "parameters": [
+          {
+            "name": "callback",
+            "type": "function",
+            "parameters": [
+               {
+                "name": "token",
+                "type": "string",
+                "description": "OAuth2 access token as specified by the manifest."
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/chrome/common/extensions/api/extension_api.cc b/chrome/common/extensions/api/extension_api.cc
index 8571417..22f6793 100644
--- a/chrome/common/extensions/api/extension_api.cc
+++ b/chrome/common/extensions/api/extension_api.cc
@@ -158,6 +158,8 @@
       IDR_EXTENSION_API_JSON_EXPERIMENTAL_EXTENSION);
   unloaded_schemas_["experimental.fontSettings"] = ReadFromResource(
       IDR_EXTENSION_API_JSON_EXPERIMENTAL_FONTSSETTINGS);
+  unloaded_schemas_["experimental.identity"] = ReadFromResource(
+      IDR_EXTENSION_API_JSON_EXPERIMENTAL_IDENTITY);
   unloaded_schemas_["experimental.infobars"] = ReadFromResource(
       IDR_EXTENSION_API_JSON_EXPERIMENTAL_INFOBARS);
   unloaded_schemas_["experimental.input.ui"] = ReadFromResource(
diff --git a/chrome/common/extensions/docs/js/api_page_generator.js b/chrome/common/extensions/docs/js/api_page_generator.js
index 44708af1..8c8322f5 100644
--- a/chrome/common/extensions/docs/js/api_page_generator.js
+++ b/chrome/common/extensions/docs/js/api_page_generator.js
@@ -37,6 +37,7 @@
   '../api/experimental.downloads.json',
   '../api/experimental.extension.json',
   '../api/experimental.fontSettings.json',
+  '../api/experimental.identity.json',
   '../api/experimental.infobars.json',
   '../api/experimental.input.ui.json',
   '../api/experimental.input.virtualKeyboard.json',
diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc
index 484b731d..549566d 100644
--- a/chrome/common/extensions/extension.cc
+++ b/chrome/common/extensions/extension.cc
@@ -238,6 +238,9 @@
 Extension::TtsVoice::TtsVoice() {}
 Extension::TtsVoice::~TtsVoice() {}
 
+Extension::OAuth2Info::OAuth2Info() {}
+Extension::OAuth2Info::~OAuth2Info() {}
+
 Extension::ExtensionKeybinding::ExtensionKeybinding() {}
 Extension::ExtensionKeybinding::~ExtensionKeybinding() {}
 
@@ -1029,12 +1032,41 @@
   if (!LoadExtent(keys::kWebURLs, &extent_,
                   errors::kInvalidWebURLs, errors::kInvalidWebURL, error) ||
       !LoadLaunchURL(error) ||
-      !LoadLaunchContainer(error))
+      !LoadLaunchContainer(error) ||
+      !LoadOAuth2Info(error))
     return false;
 
   return true;
 }
 
+bool Extension::LoadOAuth2Info(string16* error) {
+  if (!manifest_->HasKey(keys::kOAuth2))
+    return true;
+
+  if (!manifest_->GetString(keys::kOAuth2ClientId, &oauth2_info_.client_id) ||
+      oauth2_info_.client_id.empty()) {
+    *error = ASCIIToUTF16(errors::kInvalidOAuth2ClientId);
+    return false;
+  }
+
+  ListValue* list = NULL;
+  if (!manifest_->GetList(keys::kOAuth2Scopes, &list)) {
+    *error = ASCIIToUTF16(errors::kInvalidOAuth2Scopes);
+    return false;
+  }
+
+  for (size_t i = 0; i < list->GetSize(); ++i) {
+    std::string scope;
+    if (!list->GetString(i, &scope)) {
+      *error = ASCIIToUTF16(errors::kInvalidOAuth2Scopes);
+      return false;
+    }
+    oauth2_info_.scopes.push_back(scope);
+  }
+
+  return true;
+}
+
 bool Extension::LoadExtent(const char* key,
                            URLPatternSet* extent,
                            const char* list_error,
diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h
index 039a446..cf6a3fd 100644
--- a/chrome/common/extensions/extension.h
+++ b/chrome/common/extensions/extension.h
@@ -183,6 +183,15 @@
     std::set<std::string> event_types;
   };
 
+  // OAuth2 info included in the extension.
+  struct OAuth2Info {
+    OAuth2Info();
+    ~OAuth2Info();
+
+    std::string client_id;
+    std::vector<std::string> scopes;
+  };
+
   enum InitFromValueFlags {
     NO_FLAGS = 0,
 
@@ -591,6 +600,7 @@
   bool incognito_split_mode() const { return incognito_split_mode_; }
   bool offline_enabled() const { return offline_enabled_; }
   const std::vector<TtsVoice>& tts_voices() const { return tts_voices_; }
+  const OAuth2Info& oauth2_info() const { return oauth2_info_; }
   const std::vector<webkit_glue::WebIntentServiceData>&
       intents_services() const {
     return intents_services_;
@@ -802,6 +812,9 @@
   ExtensionAction* LoadExtensionActionHelper(
       const base::DictionaryValue* extension_action, string16* error);
 
+  // Helper method that loads the OAuth2 info from the 'oauth2' manifest key.
+  bool LoadOAuth2Info(string16* error);
+
   // Returns true if the extension has more than one "UI surface". For example,
   // an extension that has a browser action and a page action.
   bool HasMultipleUISurfaces() const;
@@ -1002,6 +1015,9 @@
   // List of text-to-speech voices that this extension provides, if any.
   std::vector<TtsVoice> tts_voices_;
 
+  // The OAuth2 client id and scopes, if specified by the extension.
+  OAuth2Info oauth2_info_;
+
   // List of intent services that this extension provides, if any.
   std::vector<webkit_glue::WebIntentServiceData> intents_services_;
 
diff --git a/chrome/common/extensions/extension_manifest_constants.cc b/chrome/common/extensions/extension_manifest_constants.cc
index 24691f7..d1fa89e 100644
--- a/chrome/common/extensions/extension_manifest_constants.cc
+++ b/chrome/common/extensions/extension_manifest_constants.cc
@@ -67,6 +67,9 @@
 const char kNaClModules[] = "nacl_modules";
 const char kNaClModulesMIMEType[] = "mime_type";
 const char kNaClModulesPath[] = "path";
+const char kOAuth2[] = "oauth2";
+const char kOAuth2ClientId[] = "oauth2.client_id";
+const char kOAuth2Scopes[] = "oauth2.scopes";
 const char kOfflineEnabled[] = "offline_enabled";
 const char kOmnibox[] = "omnibox";
 const char kOmniboxKeyword[] = "omnibox.keyword";
@@ -324,6 +327,10 @@
     "Invalid value for 'nacl_modules[*].path'.";
 const char kInvalidNaClModulesMIMEType[] =
     "Invalid value for 'nacl_modules[*].mime_type'.";
+const char kInvalidOAuth2ClientId[] =
+    "Invalid value for 'oauth2.client_id'.";
+const char kInvalidOAuth2Scopes[] =
+    "Invalid value for 'oauth2.scopes'.";
 const char kInvalidOfflineEnabled[] =
     "Invalid value for 'offline_enabled'.";
 const char kInvalidOmniboxKeyword[] =
@@ -455,4 +462,3 @@
     "Extensions cannot install plugins on Chrome OS";
 #endif
 }  // namespace extension_manifest_errors
-
diff --git a/chrome/common/extensions/extension_manifest_constants.h b/chrome/common/extensions/extension_manifest_constants.h
index f68c0e9c..8392858 100644
--- a/chrome/common/extensions/extension_manifest_constants.h
+++ b/chrome/common/extensions/extension_manifest_constants.h
@@ -75,6 +75,9 @@
   extern const char kNaClModulesMIMEType[];
   extern const char kNaClModulesPath[];
   extern const char kName[];
+  extern const char kOAuth2[];
+  extern const char kOAuth2ClientId[];
+  extern const char kOAuth2Scopes[];
   extern const char kOfflineEnabled[];
   extern const char kOmnibox[];
   extern const char kOmniboxKeyword[];
@@ -237,6 +240,8 @@
   extern const char kInvalidNaClModulesMIMEType[];
   extern const char kInvalidNaClModulesPath[];
   extern const char kInvalidName[];
+  extern const char kInvalidOAuth2ClientId[];
+  extern const char kInvalidOAuth2Scopes[];
   extern const char kInvalidOfflineEnabled[];
   extern const char kInvalidOmniboxKeyword[];
   extern const char kInvalidOptionsPage[];
diff --git a/chrome/common/extensions/extension_permission_set.cc b/chrome/common/extensions/extension_permission_set.cc
index 0c90e477..c2cd6c3 100644
--- a/chrome/common/extensions/extension_permission_set.cc
+++ b/chrome/common/extensions/extension_permission_set.cc
@@ -198,7 +198,6 @@
       kUnlimitedStorage, "unlimitedStorage", 0,
       ExtensionPermissionMessage::kNone, kFlagCannotBeOptional);
 
-
   // Register hosted and packaged app permissions.
   info->RegisterPermission(
       kAppNotifications, "appNotifications", 0,
diff --git a/chrome/common/net/gaia/google_service_auth_error.cc b/chrome/common/net/gaia/google_service_auth_error.cc
index 78595eb..20f3d1a6 100644
--- a/chrome/common/net/gaia/google_service_auth_error.cc
+++ b/chrome/common/net/gaia/google_service_auth_error.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -7,6 +7,7 @@
 #include <string>
 
 #include "base/logging.h"
+#include "base/stringprintf.h"
 #include "base/values.h"
 #include "net/base/net_errors.h"
 
@@ -96,6 +97,37 @@
   return value;
 }
 
+std::string GoogleServiceAuthError::ToString() const {
+  switch (state_) {
+    case NONE:
+      return "";
+    case INVALID_GAIA_CREDENTIALS:
+      return "Invalid credentials.";
+    case USER_NOT_SIGNED_UP:
+      return "Not authorized.";
+    case CONNECTION_FAILED:
+      return base::StringPrintf("Connection failed (%d).", network_error_);
+    case CAPTCHA_REQUIRED:
+      return base::StringPrintf("CAPTCHA required (%s).",
+                                captcha_.token.c_str());
+    case ACCOUNT_DELETED:
+      return "Account deleted.";
+    case ACCOUNT_DISABLED:
+      return "Account disabled.";
+    case SERVICE_UNAVAILABLE:
+      return "Service unavailable; try again later.";
+    case TWO_FACTOR:
+      return "2-step verification required.";
+    case REQUEST_CANCELED:
+      return "Request canceled.";
+    case HOSTED_NOT_ALLOWED:
+      return "Google account required.";
+    default:
+      NOTREACHED();
+      return std::string();
+  }
+}
+
 GoogleServiceAuthError::GoogleServiceAuthError(State s, int error)
     : state_(s),
       captcha_("", GURL(), GURL()),
diff --git a/chrome/common/net/gaia/google_service_auth_error.h b/chrome/common/net/gaia/google_service_auth_error.h
index 74cc11d6..da67bf4 100644
--- a/chrome/common/net/gaia/google_service_auth_error.h
+++ b/chrome/common/net/gaia/google_service_auth_error.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -118,6 +118,9 @@
   // ownership of returned dictionary.
   base::DictionaryValue* ToValue() const;
 
+  // Returns a message describing the error.
+  std::string ToString() const;
+
  private:
   GoogleServiceAuthError(State s, int error);
 
diff --git a/chrome/common/net/gaia/oauth2_mint_token_flow.cc b/chrome/common/net/gaia/oauth2_mint_token_flow.cc
index 4e7029db..feef388 100644
--- a/chrome/common/net/gaia/oauth2_mint_token_flow.cc
+++ b/chrome/common/net/gaia/oauth2_mint_token_flow.cc
@@ -8,10 +8,29 @@
 #include <vector>
 
 #include "base/basictypes.h"
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/message_loop.h"
+#include "chrome/common/chrome_switches.h"
 #include "chrome/common/net/gaia/gaia_urls.h"
+#include "chrome/common/net/gaia/google_service_auth_error.h"
 
 using net::URLRequestContextGetter;
 
+namespace {
+
+OAuth2MintTokenFlow::InterceptorForTests* g_interceptor_for_tests = NULL;
+
+}  // namespace
+
+// static
+void OAuth2MintTokenFlow::SetInterceptorForTests(
+    OAuth2MintTokenFlow::InterceptorForTests* interceptor) {
+  CHECK(CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType));
+  CHECK(NULL == g_interceptor_for_tests);  // Only one at a time.
+  g_interceptor_for_tests = interceptor;
+}
+
 OAuth2MintTokenFlow::OAuth2MintTokenFlow(
     URLRequestContextGetter* context,
     Delegate* delegate)
@@ -32,6 +51,27 @@
   client_id_ = client_id;
   scopes_ = scopes;
 
+  if (g_interceptor_for_tests) {
+    std::string auth_token;
+    GoogleServiceAuthError error = GoogleServiceAuthError::None();
+
+    // We use PostTask, instead of calling the delegate directly, because the
+    // message loop will run a few times before we notify the delegate in the
+    // real implementation.
+    if (g_interceptor_for_tests->DoIntercept(this, &auth_token, &error)) {
+      MessageLoop::current()->PostTask(
+          FROM_HERE,
+          base::Bind(&OAuth2MintTokenFlow::Delegate::OnMintTokenSuccess,
+                     base::Unretained(delegate_), auth_token));
+    } else {
+      MessageLoop::current()->PostTask(
+          FROM_HERE,
+          base::Bind(&OAuth2MintTokenFlow::Delegate::OnMintTokenFailure,
+                     base::Unretained(delegate_), error));
+    }
+    return;
+  }
+
   BeginGetLoginAccessToken();
 }
 
@@ -40,6 +80,7 @@
   login_access_token_ = access_token;
   EndGetLoginAccessToken(NULL);
 }
+
 void OAuth2MintTokenFlow::OnGetTokenFailure(
     const GoogleServiceAuthError& error) {
   EndGetLoginAccessToken(&error);
diff --git a/chrome/common/net/gaia/oauth2_mint_token_flow.h b/chrome/common/net/gaia/oauth2_mint_token_flow.h
index 31d1958..ee06145 100644
--- a/chrome/common/net/gaia/oauth2_mint_token_flow.h
+++ b/chrome/common/net/gaia/oauth2_mint_token_flow.h
@@ -35,6 +35,17 @@
     virtual void OnMintTokenFailure(const GoogleServiceAuthError& error) { }
   };
 
+  // An interceptor for tests.
+  class InterceptorForTests {
+   public:
+    // Returns true if the success callback should be called and false for
+    // failures.
+    virtual bool DoIntercept(const OAuth2MintTokenFlow* flow,
+                             std::string* access_token,
+                             GoogleServiceAuthError* error) = 0;
+  };
+  static void SetInterceptorForTests(InterceptorForTests* interceptor);
+
   OAuth2MintTokenFlow(net::URLRequestContextGetter* context,
                       Delegate* delegate);
   virtual ~OAuth2MintTokenFlow();
@@ -57,11 +68,11 @@
   const std::string& client_id() const { return client_id_; }
 
  protected:
-  // Helper to create an instnace of access token fetcher.
+  // Helper to create an instance of access token fetcher.
   // Caller owns the returned instance.
   virtual OAuth2AccessTokenFetcher* CreateAccessTokenFetcher();
 
-  // Helper to create an instnace of mint token fetcher.
+  // Helper to create an instance of mint token fetcher.
   // Caller owns the returned instance.
   virtual OAuth2MintTokenFetcher* CreateMintTokenFetcher();