[identity] Copy google_auth_provider from Topaz
This is step 1 of 4 in a soft transition to migrate the Auth Provider
between repos. It creates a new component but the component is not yet
included in any configurations or used.
Change-Id: I3b115347b16129b0be72d7ff1c59310d4105573d
diff --git a/src/identity/BUILD.gn b/src/identity/BUILD.gn
index 37ffb68..74b1828 100644
--- a/src/identity/BUILD.gn
+++ b/src/identity/BUILD.gn
@@ -6,6 +6,7 @@
testonly = true
deps = [
":tests",
+ "bin",
"lib",
]
}
@@ -14,6 +15,7 @@
testonly = true
data_deps = [
+ "bin:tests",
"lib:tests",
]
}
diff --git a/src/identity/bin/BUILD.gn b/src/identity/bin/BUILD.gn
new file mode 100644
index 0000000..2ca63fc0
--- /dev/null
+++ b/src/identity/bin/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/test/test_package.gni")
+import("//build/testing/environments.gni")
+
+group("bin") {
+ testonly = true
+ deps = [
+ ":identity_bin_unittests",
+ ]
+}
+
+group("tests") {
+ testonly = true
+ data_deps = [
+ ":identity_bin_unittests",
+ ]
+}
+
+test_package("identity_bin_unittests") {
+ deps = [
+ "google_auth_provider:google_auth_provider_unittests",
+ ]
+
+ tests = [
+ {
+ name = "google_auth_provider_unittests"
+ environments = basic_envs
+ },
+ ]
+}
diff --git a/src/identity/bin/google_auth_provider/BUILD.gn b/src/identity/bin/google_auth_provider/BUILD.gn
new file mode 100644
index 0000000..0f226bb
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/BUILD.gn
@@ -0,0 +1,108 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/package.gni")
+
+# TODO(jsankey): Complete soft transition to migrate users away from
+# the conflicting target in Topaz then remove the _2.
+package("google_auth_provider_2") {
+ deps = [
+ ":bin",
+ ]
+
+ binary = "google_auth_provider_2"
+
+ meta = [
+ {
+ path = rebase_path("meta/google_auth_provider_2.cmx")
+ dest = "google_auth_provider_2.cmx"
+ },
+ ]
+}
+
+executable("bin") {
+ output_name = "google_auth_provider_2"
+
+ sources = [
+ "main.cc",
+ ]
+
+ deps = [
+ ":lib",
+ "//garnet/public/lib/fsl",
+ "//garnet/public/lib/network_wrapper",
+ "//sdk/fidl/fuchsia.auth",
+ "//sdk/lib/fidl/cpp",
+ "//sdk/lib/sys/cpp",
+ "//src/lib/fxl",
+ "//zircon/public/lib/async-loop-cpp",
+ "//zircon/public/lib/fit",
+ "//zircon/public/lib/trace-provider",
+ ]
+}
+
+executable("google_auth_provider_unittests") {
+ testonly = true
+
+ deps = [
+ ":unittests",
+ "//third_party/googletest:gtest_main",
+ ]
+}
+
+source_set("unittests") {
+ testonly = true
+ output_name = "google_auth_provider_unittests"
+
+ sources = [
+ "factory_impl_unittest.cc",
+ "google_auth_provider_impl_unittest.cc",
+ ]
+
+ deps = [
+ ":lib",
+ "//garnet/public/lib/gtest:gtest",
+ "//garnet/public/lib/network_wrapper:fake",
+ "//peridot/lib/rapidjson",
+ "//sdk/fidl/fuchsia.auth",
+ "//sdk/lib/fidl/cpp",
+ "//src/lib/fxl:printers",
+ "//third_party/googletest:gtest_main",
+ ]
+}
+
+source_set("lib") {
+ sources = [
+ "constants.h",
+ "factory_impl.cc",
+ "factory_impl.h",
+ "google_auth_provider_impl.cc",
+ "google_auth_provider_impl.h",
+ "settings.h",
+ ]
+
+ deps = [
+ "//garnet/public/lib/rapidjson_utils",
+ "//peridot/lib/rapidjson",
+ "//sdk/lib/ui/scenic/cpp",
+ "//src/identity/lib/oauth:lib",
+ "//src/lib/fxl",
+ ]
+
+ public_deps = [
+ "//garnet/public/lib/callback",
+ "//garnet/public/lib/network_wrapper",
+ "//sdk/fidl/fuchsia.auth",
+ "//sdk/fidl/fuchsia.auth.testing",
+ "//sdk/fidl/fuchsia.net.oldhttp",
+ "//sdk/fidl/fuchsia.ui.views",
+ "//sdk/fidl/fuchsia.web",
+ "//sdk/lib/fidl/cpp",
+ "//sdk/lib/sys/cpp",
+ "//zircon/public/lib/async-default",
+ "//zircon/public/lib/fit",
+ "//zircon/public/lib/syslog",
+ "//zircon/public/lib/zx",
+ ]
+}
diff --git a/src/identity/bin/google_auth_provider/constants.h b/src/identity/bin/google_auth_provider/constants.h
new file mode 100644
index 0000000..7072d27
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/constants.h
@@ -0,0 +1,41 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <initializer_list>
+
+namespace google_auth_provider {
+
+constexpr char kFuchsiaClientId[] =
+ "934259141868-rejmm4ollj1bs7th1vg2ur6antpbug79.apps.googleusercontent.com";
+constexpr char kGoogleOAuthAuthEndpoint[] =
+ "https://accounts.google.com/o/oauth2/v2/auth";
+constexpr char kGoogleFuchsiaEndpoint[] =
+ "https://accounts.google.com/embedded/setup/fuchsia";
+constexpr char kGoogleOAuthTokenEndpoint[] =
+ "https://www.googleapis.com/oauth2/v4/token";
+constexpr char kGoogleRevokeTokenEndpoint[] =
+ "https://accounts.google.com/o/oauth2/revoke";
+constexpr char kGoogleUserInfoEndpoint[] =
+ "https://www.googleapis.com/oauth2/v3/userinfo";
+constexpr char kFirebaseAuthEndpoint[] =
+ "https://www.googleapis.com/identitytoolkit/v3/relyingparty/"
+ "verifyAssertion";
+constexpr char kRedirectUri[] = "https://localhost/fuchsiaoauth2redirect";
+constexpr char kWebViewUrl[] = "web_view";
+
+constexpr auto kScopes = {
+ // Used by google_auth_provider for retrieving unique user profile id.
+ "openid",
+ // Used by a variety of client components.
+ "email",
+ // Used by google_auth_provider for retrieving user profile attributes,
+ // specifically display name, profile url, and profile image.
+ "profile",
+ // Used by components outside this repository.
+ "https://www.googleapis.com/auth/assistant",
+ // Used by components outside this repository.
+ "https://www.googleapis.com/auth/youtube.readonly",
+ // Used by software update
+ "https://www.googleapis.com/auth/devstorage.read_write"};
+} // namespace google_auth_provider
diff --git a/src/identity/bin/google_auth_provider/factory_impl.cc b/src/identity/bin/google_auth_provider/factory_impl.cc
new file mode 100644
index 0000000..83907a6
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/factory_impl.cc
@@ -0,0 +1,38 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/bin/google_auth_provider/factory_impl.h"
+
+#include "src/identity/bin/google_auth_provider/settings.h"
+
+namespace google_auth_provider {
+
+FactoryImpl::FactoryImpl(async_dispatcher_t* main_dispatcher,
+ sys::ComponentContext* context,
+ network_wrapper::NetworkWrapper* network_wrapper,
+ Settings settings)
+ : main_dispatcher_(main_dispatcher),
+ context_(context),
+ network_wrapper_(network_wrapper),
+ settings_(std::move(settings)) {
+ FXL_DCHECK(context_);
+ FXL_DCHECK(network_wrapper_);
+}
+
+FactoryImpl::~FactoryImpl() {}
+
+void FactoryImpl::Bind(
+ fidl::InterfaceRequest<fuchsia::auth::AuthProviderFactory> request) {
+ factory_bindings_.AddBinding(this, std::move(request));
+}
+
+void FactoryImpl::GetAuthProvider(
+ fidl::InterfaceRequest<fuchsia::auth::AuthProvider> auth_provider,
+ GetAuthProviderCallback callback) {
+ providers_.emplace(main_dispatcher_, context_, network_wrapper_, settings_,
+ std::move(auth_provider));
+ callback(fuchsia::auth::AuthProviderStatus::OK);
+}
+
+} // namespace google_auth_provider
diff --git a/src/identity/bin/google_auth_provider/factory_impl.h b/src/identity/bin/google_auth_provider/factory_impl.h
new file mode 100644
index 0000000..c3d335d
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/factory_impl.h
@@ -0,0 +1,50 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_FACTORY_IMPL_H_
+#define SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_FACTORY_IMPL_H_
+
+#include <fuchsia/auth/cpp/fidl.h>
+#include <lib/async/dispatcher.h>
+
+#include "lib/callback/auto_cleanable.h"
+#include "lib/network_wrapper/network_wrapper.h"
+#include "src/identity/bin/google_auth_provider/google_auth_provider_impl.h"
+#include "src/identity/bin/google_auth_provider/settings.h"
+#include "src/lib/fxl/macros.h"
+
+namespace google_auth_provider {
+
+class FactoryImpl : public fuchsia::auth::AuthProviderFactory {
+ public:
+ FactoryImpl(async_dispatcher_t* main_dispatcher,
+ sys::ComponentContext* context,
+ network_wrapper::NetworkWrapper* network_wrapper,
+ Settings settings);
+
+ ~FactoryImpl() override;
+
+ void Bind(fidl::InterfaceRequest<fuchsia::auth::AuthProviderFactory> request);
+
+ private:
+ // Factory:
+ void GetAuthProvider(
+ fidl::InterfaceRequest<fuchsia::auth::AuthProvider> auth_provider,
+ GetAuthProviderCallback callback) override;
+
+ async_dispatcher_t* const main_dispatcher_;
+ sys::ComponentContext* const context_;
+ network_wrapper::NetworkWrapper* const network_wrapper_;
+ const Settings settings_;
+
+ callback::AutoCleanableSet<GoogleAuthProviderImpl> providers_;
+
+ fidl::BindingSet<fuchsia::auth::AuthProviderFactory> factory_bindings_;
+
+ FXL_DISALLOW_COPY_AND_ASSIGN(FactoryImpl);
+};
+
+} // namespace google_auth_provider
+
+#endif // SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_FACTORY_IMPL_H_
diff --git a/src/identity/bin/google_auth_provider/factory_impl_unittest.cc b/src/identity/bin/google_auth_provider/factory_impl_unittest.cc
new file mode 100644
index 0000000..5748051
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/factory_impl_unittest.cc
@@ -0,0 +1,52 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/bin/google_auth_provider/factory_impl.h"
+
+#include "lib/callback/capture.h"
+#include "lib/callback/set_when_called.h"
+#include "lib/fidl/cpp/binding.h"
+#include "lib/gtest/test_loop_fixture.h"
+#include "lib/network_wrapper/fake_network_wrapper.h"
+#include "src/identity/bin/google_auth_provider/settings.h"
+#include "src/lib/fxl/macros.h"
+
+namespace google_auth_provider {
+
+class GoogleFactoryImplTest : public gtest::TestLoopFixture {
+ public:
+ GoogleFactoryImplTest()
+ : network_wrapper_(dispatcher()),
+ context_(sys::ComponentContext::Create().get()),
+ factory_impl_(dispatcher(), context_, &network_wrapper_, {}) {
+ factory_impl_.Bind(factory_.NewRequest());
+ }
+
+ ~GoogleFactoryImplTest() override {}
+
+ protected:
+ network_wrapper::FakeNetworkWrapper network_wrapper_;
+ sys::ComponentContext* context_;
+ fuchsia::auth::AuthProviderPtr auth_provider_;
+ fuchsia::auth::AuthProviderFactoryPtr factory_;
+
+ FactoryImpl factory_impl_;
+
+ private:
+ FXL_DISALLOW_COPY_AND_ASSIGN(GoogleFactoryImplTest);
+};
+
+TEST_F(GoogleFactoryImplTest, GetAuthProvider) {
+ fuchsia::auth::AuthProviderStatus status;
+ auth_provider_.Unbind();
+ bool callback_called = false;
+ factory_->GetAuthProvider(
+ auth_provider_.NewRequest(),
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status));
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(fuchsia::auth::AuthProviderStatus::OK, status);
+}
+
+} // namespace google_auth_provider
diff --git a/src/identity/bin/google_auth_provider/google_auth_provider_impl.cc b/src/identity/bin/google_auth_provider/google_auth_provider_impl.cc
new file mode 100644
index 0000000..2a96cfb
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/google_auth_provider_impl.cc
@@ -0,0 +1,683 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/bin/google_auth_provider/google_auth_provider_impl.h"
+
+#include <fuchsia/net/oldhttp/cpp/fidl.h>
+#include <lib/async/dispatcher.h>
+#include <lib/fdio/directory.h>
+#include <lib/fidl/cpp/interface_request.h>
+#include <lib/fit/function.h>
+#include <lib/sys/cpp/component_context.h>
+#include <lib/syslog/global.h>
+#include <lib/ui/scenic/cpp/view_token_pair.h>
+#include <lib/vfs/cpp/service.h>
+
+#include "garnet/public/lib/rapidjson_utils/rapidjson_validation.h"
+#include "peridot/lib/rapidjson/rapidjson.h"
+#include "rapidjson/document.h"
+#include "rapidjson/schema.h"
+#include "src/identity/bin/google_auth_provider/constants.h"
+#include "src/identity/lib/oauth/oauth_request_builder.h"
+#include "src/identity/lib/oauth/oauth_response.h"
+#include "src/lib/fxl/strings/join_strings.h"
+
+namespace google_auth_provider {
+
+namespace http = ::fuchsia::net::oldhttp;
+
+namespace {
+
+constexpr char kInjectionEntry[] = "LegacyAuthCredentialInjector";
+
+std::string GetClientId(const std::string& app_client_id) {
+ // By default, use the client_id of the invoking application.
+ std::string client_id = app_client_id;
+
+ // Use hard-coded Fuchsia client_id for downscoped tokens, if |app_client_id|
+ // is missing.
+ if (app_client_id.empty()) {
+ client_id = kFuchsiaClientId;
+ }
+
+ return client_id;
+}
+
+// Outputs information from a failing OAuthResponse to the syslog.
+void LogOauthResponse(const char* operation,
+ const auth_providers::oauth::OAuthResponse& response) {
+ FX_LOGF(WARNING, NULL,
+ "OAuthResponse error during %s: %s (Full response: %s)", operation,
+ response.error_description.c_str(),
+ modular::JsonValueToPrettyString(response.json_response).c_str());
+}
+
+// Sometimes auth codes contain non alpha characters such as a slash. When we
+// receive these in a url parameter they are Hex encoded, but they need to be
+// translated back to UTF-8 before using the auth code.
+//
+// TODO(jsankey): Remove this once we migrate to cookie delivery, or use a
+// common encoding/decoding library if that arrives earlier.
+void NormalizeAuthCode(std::string* code) {
+ // This function uses the following literals:
+ // 1 - The length of '%'
+ // 2 - The length of a hex byte, e.g. '2F'
+ // 3 - The length of a %-prefixed hex byte, e.g. '%2F'
+ // 16 - The base of hexadecimal
+ // 32 - The smallest printable character, i.e. the space character
+ // 127 - The largest single byte UTF-8 codepoint
+ std::string::size_type pos = 0;
+ while ((pos = code->find("%", pos)) != std::string::npos &&
+ pos <= code->length() - 3) {
+ int codepoint = strtol(code->substr(pos + 1, 2).c_str(), nullptr, 16);
+ if (codepoint >= 33 && codepoint <= 127) {
+ code->replace(pos, 3, std::string(1, codepoint));
+ }
+ pos += 3;
+ }
+}
+
+// Checks the supplied Google authentication URL. If the URL indicated the user
+// has aborted the flow or an error occured these are reported as error
+// statuses, otherwise a status of OK is returned. If the URL contains an auth
+// code query parameter, this will be returned in |auth_code|.
+fuchsia::auth::AuthProviderStatus ParseAuthCodeFromUrl(const std::string& url,
+ std::string& auth_code) {
+ static const std::string success_prefix =
+ std::string{kRedirectUri} + "?code=";
+ static const std::string cancel_prefix =
+ std::string{kRedirectUri} + "?error=access_denied";
+
+ if (url.find(cancel_prefix) == 0) {
+ return fuchsia::auth::AuthProviderStatus::USER_CANCELLED;
+ }
+ if (url.find(success_prefix) != 0) {
+ // The authentication process is still ongoing.
+ return fuchsia::auth::AuthProviderStatus::OK;
+ }
+
+ // Take everything up to the next query parameter or hash fragment.
+ auto end_char = url.find_first_of("#&", success_prefix.size());
+ auto length = end_char == std::string::npos
+ ? std::string::npos
+ : end_char - success_prefix.size();
+ auto code = url.substr(success_prefix.size(), length);
+ NormalizeAuthCode(&code);
+
+ if (code.empty()) {
+ return fuchsia::auth::AuthProviderStatus::OAUTH_SERVER_ERROR;
+ } else {
+ auth_code = code;
+ return fuchsia::auth::AuthProviderStatus::OK;
+ }
+}
+
+} // namespace
+
+using auth_providers::oauth::OAuthRequestBuilder;
+using auth_providers::oauth::ParseOAuthResponse;
+using fuchsia::auth::AuthenticationUIContext;
+using fuchsia::auth::AuthProviderStatus;
+using fuchsia::auth::AuthTokenPtr;
+using fuchsia::auth::FirebaseTokenPtr;
+using modular::JsonValueToPrettyString;
+
+GoogleAuthProviderImpl::GoogleAuthProviderImpl(
+ async_dispatcher_t* const main_dispatcher, sys::ComponentContext* context,
+ network_wrapper::NetworkWrapper* network_wrapper, Settings settings,
+ fidl::InterfaceRequest<fuchsia::auth::AuthProvider> request)
+ : main_dispatcher_(main_dispatcher),
+ context_(context),
+ network_wrapper_(network_wrapper),
+ settings_(std::move(settings)),
+ binding_(this, std::move(request)) {
+ FXL_DCHECK(main_dispatcher_);
+ FXL_DCHECK(network_wrapper_);
+
+ // The class shuts down when the client connection is disconnected.
+ binding_.set_error_handler([this](zx_status_t status) {
+ if (on_empty_) {
+ on_empty_();
+ }
+ });
+}
+
+GoogleAuthProviderImpl::~GoogleAuthProviderImpl() {}
+
+void GoogleAuthProviderImpl::GetPersistentCredential(
+ fidl::InterfaceHandle<AuthenticationUIContext> auth_ui_context,
+ fidl::StringPtr user_profile_id, GetPersistentCredentialCallback callback) {
+ FXL_DCHECK(auth_ui_context);
+ get_persistent_credential_callback_ = std::move(callback);
+
+ std::string url = GetAuthorizeUrl(user_profile_id);
+ fuchsia::ui::views::ViewHolderToken view_holder_token = SetupChromium();
+ if (!view_holder_token.value) {
+ return;
+ }
+ fuchsia::web::NavigationControllerPtr controller;
+ web_frame_->GetNavigationController(controller.NewRequest());
+ controller->LoadUrl(url, fuchsia::web::LoadUrlParams(),
+ [](fuchsia::web::NavigationController_LoadUrl_Result) {});
+ FX_LOGF(INFO, NULL, "Loading URL: %s", url.c_str());
+
+ auth_ui_context_ = auth_ui_context.Bind();
+ auth_ui_context_.set_error_handler([this](zx_status_t status) {
+ FX_LOG(INFO, NULL, "Overlay cancelled by the caller");
+ ReleaseResources();
+ RemoveCredentialInjectorInterface();
+ SafelyCallbackGetPersistentCredential(AuthProviderStatus::UNKNOWN_ERROR,
+ nullptr, nullptr);
+ return;
+ });
+
+ auth_ui_context_->StartOverlay(std::move(view_holder_token));
+ ExposeCredentialInjectorInterface();
+}
+
+void GoogleAuthProviderImpl::GetAppAccessToken(
+ std::string credential, fidl::StringPtr app_client_id,
+ const std::vector<std::string> app_scopes,
+ GetAppAccessTokenCallback callback) {
+ if (credential.empty()) {
+ callback(AuthProviderStatus::BAD_REQUEST, nullptr);
+ return;
+ }
+
+ auto request =
+ OAuthRequestBuilder(kGoogleOAuthTokenEndpoint, "POST")
+ .SetUrlEncodedBody("refresh_token=" + credential +
+ "&client_id=" + GetClientId(app_client_id.get()) +
+ "&grant_type=refresh_token");
+
+ auto request_factory = [request = std::move(request)] {
+ return request.Build();
+ };
+
+ Request(std::move(request_factory),
+ [callback = std::move(callback)](http::URLResponse response) {
+ auto oauth_response = ParseOAuthResponse(std::move(response));
+ if (oauth_response.status != AuthProviderStatus::OK) {
+ LogOauthResponse("GetAppAccessToken", oauth_response);
+ callback(oauth_response.status, nullptr);
+ return;
+ }
+
+ const char kRootSchema[] = R"({
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "expires_in": {
+ "type": "integer"
+ }
+ },
+ "required": ["access_token", "expires_in"]
+ })";
+ auto root_schema = rapidjson_utils::InitSchema(kRootSchema);
+ if (!root_schema) {
+ callback(AuthProviderStatus::INTERNAL_ERROR, nullptr);
+ return;
+ }
+ if (!rapidjson_utils::ValidateSchema(oauth_response.json_response,
+ *root_schema)) {
+ callback(AuthProviderStatus::OAUTH_SERVER_ERROR, nullptr);
+ return;
+ }
+
+ AuthTokenPtr access_token = fuchsia::auth::AuthToken::New();
+ access_token->token_type = fuchsia::auth::TokenType::ACCESS_TOKEN;
+ access_token->token =
+ oauth_response.json_response["access_token"].GetString();
+ access_token->expires_in =
+ oauth_response.json_response["expires_in"].GetUint64();
+
+ callback(AuthProviderStatus::OK, std::move(access_token));
+ });
+}
+
+void GoogleAuthProviderImpl::GetAppIdToken(std::string credential,
+ fidl::StringPtr audience,
+ GetAppIdTokenCallback callback) {
+ if (credential.empty()) {
+ callback(AuthProviderStatus::BAD_REQUEST, nullptr);
+ return;
+ }
+
+ auto request =
+ OAuthRequestBuilder(kGoogleOAuthTokenEndpoint, "POST")
+ .SetUrlEncodedBody("refresh_token=" + credential +
+ "&client_id=" + GetClientId(audience.get()) +
+ "&grant_type=refresh_token");
+
+ auto request_factory = [request = std::move(request)] {
+ return request.Build();
+ };
+ Request(std::move(request_factory),
+ [callback = std::move(callback)](http::URLResponse response) {
+ auto oauth_response = ParseOAuthResponse(std::move(response));
+ if (oauth_response.status != AuthProviderStatus::OK) {
+ LogOauthResponse("GetAppIdToken", oauth_response);
+ callback(oauth_response.status, nullptr);
+ return;
+ }
+
+ const char kRootSchema[] = R"({
+ "type": "object",
+ "properties": {
+ "id_token": {
+ "type": "string"
+ },
+ "expires_in": {
+ "type": "integer"
+ }
+ },
+ "required": ["id_token", "expires_in"]
+ })";
+ auto root_schema = rapidjson_utils::InitSchema(kRootSchema);
+ if (!root_schema) {
+ callback(AuthProviderStatus::INTERNAL_ERROR, nullptr);
+ return;
+ }
+ if (!rapidjson_utils::ValidateSchema(oauth_response.json_response,
+ *root_schema)) {
+ callback(AuthProviderStatus::OAUTH_SERVER_ERROR, nullptr);
+ return;
+ }
+
+ AuthTokenPtr id_token = fuchsia::auth::AuthToken::New();
+ id_token->token =
+ oauth_response.json_response["id_token"].GetString();
+ id_token->token_type = fuchsia::auth::TokenType::ID_TOKEN;
+ id_token->expires_in =
+ oauth_response.json_response["expires_in"].GetUint64();
+
+ callback(AuthProviderStatus::OK, std::move(id_token));
+ });
+}
+
+void GoogleAuthProviderImpl::GetAppFirebaseToken(
+ std::string id_token, std::string firebase_api_key,
+ GetAppFirebaseTokenCallback callback) {
+ if (id_token.empty() || firebase_api_key.empty()) {
+ callback(AuthProviderStatus::BAD_REQUEST, nullptr);
+ return;
+ }
+
+ std::map<std::string, std::string> query_params;
+ query_params["key"] = firebase_api_key;
+ auto request = OAuthRequestBuilder(kFirebaseAuthEndpoint, "POST")
+ .SetQueryParams(query_params)
+ .SetJsonBody(R"({"postBody": "id_token=)" + id_token +
+ R"(&providerId=google.com",)" +
+ R"("returnIdpCredential": true,)" +
+ R"("returnSecureToken": true,)" +
+ R"("requestUri": "http://localhost"})");
+
+ // Exchange credential to access token at Google OAuth token endpoint
+ auto request_factory = [request = std::move(request)] {
+ return request.Build();
+ };
+ Request(std::move(request_factory), [callback = std::move(callback)](
+ http::URLResponse response) {
+ auto oauth_response = ParseOAuthResponse(std::move(response));
+ if (oauth_response.status != AuthProviderStatus::OK) {
+ LogOauthResponse("GetAppFirebaseToken", oauth_response);
+ callback(oauth_response.status, nullptr);
+ return;
+ }
+
+ const char kRootSchema[] = R"({
+ "type": "object",
+ "properties": {
+ "idToken": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "localId": {
+ "type": "string"
+ },
+ "expiresIn": {
+ "type": "string"
+ }
+ },
+ "required": ["idToken", "email", "localId", "expiresIn"]
+ })";
+ auto root_schema = rapidjson_utils::InitSchema(kRootSchema);
+ if (!root_schema) {
+ callback(AuthProviderStatus::INTERNAL_ERROR, nullptr);
+ return;
+ }
+ if (!rapidjson_utils::ValidateSchema(oauth_response.json_response,
+ *root_schema)) {
+ callback(AuthProviderStatus::OAUTH_SERVER_ERROR, nullptr);
+ return;
+ }
+
+ FirebaseTokenPtr fb_token = fuchsia::auth::FirebaseToken::New();
+ fb_token->id_token = oauth_response.json_response["idToken"].GetString();
+ fb_token->email = oauth_response.json_response["email"].GetString();
+ fb_token->local_id = oauth_response.json_response["localId"].GetString();
+ fb_token->expires_in =
+ std::atoll(oauth_response.json_response["expiresIn"].GetString());
+
+ callback(AuthProviderStatus::OK, std::move(fb_token));
+ });
+}
+
+void GoogleAuthProviderImpl::RevokeAppOrPersistentCredential(
+ std::string credential, RevokeAppOrPersistentCredentialCallback callback) {
+ if (credential.empty()) {
+ callback(AuthProviderStatus::BAD_REQUEST);
+ return;
+ }
+
+ std::string url =
+ kGoogleRevokeTokenEndpoint + std::string("?token=") + credential;
+ auto request = OAuthRequestBuilder(url, "POST").SetUrlEncodedBody("");
+
+ auto request_factory = [request = std::move(request)] {
+ return request.Build();
+ };
+
+ Request(std::move(request_factory),
+ [callback = std::move(callback)](http::URLResponse response) {
+ auto oauth_response = ParseOAuthResponse(std::move(response));
+ if (oauth_response.status != AuthProviderStatus::OK) {
+ LogOauthResponse("RevokeToken", oauth_response);
+ callback(oauth_response.status);
+ return;
+ }
+
+ callback(AuthProviderStatus::OK);
+ });
+}
+
+void GoogleAuthProviderImpl::GetPersistentCredentialFromAttestationJWT(
+ fidl::InterfaceHandle<AttestationSigner> attestation_signer,
+ AttestationJWTParams jwt_params,
+ fidl::InterfaceHandle<AuthenticationUIContext> auth_ui_context,
+ fidl::StringPtr user_profile_id,
+ GetPersistentCredentialFromAttestationJWTCallback callback) {
+ // Remote attestation flow not supported for traditional OAuth.
+ callback(AuthProviderStatus::BAD_REQUEST, nullptr, nullptr, nullptr, nullptr);
+}
+
+void GoogleAuthProviderImpl::GetAppAccessTokenFromAssertionJWT(
+ fidl::InterfaceHandle<AttestationSigner> attestation_signer,
+ AssertionJWTParams jwt_params, std::string credential,
+ const std::vector<std::string> scopes,
+ GetAppAccessTokenFromAssertionJWTCallback callback) {
+ // Remote attestation flow not supported for traditional OAuth.
+ callback(AuthProviderStatus::BAD_REQUEST, nullptr, nullptr, nullptr);
+}
+
+void GoogleAuthProviderImpl::OnNavigationStateChanged(
+ NavigationState change, OnNavigationStateChangedCallback callback) {
+ // Not all events change the URL, those that don't can be ignored.
+ if (!change.has_url()) {
+ callback();
+ return;
+ }
+
+ std::string auth_code;
+ AuthProviderStatus status = ParseAuthCodeFromUrl(change.url(), auth_code);
+
+ // If either an error occured or the user successfully received an auth code
+ // we need to close the browser instance.
+ if (status != AuthProviderStatus::OK || !auth_code.empty()) {
+ ReleaseResources();
+ // InjectPersistentCredential will still be reachable even after removing
+ // the interface from output, but any requests to it will be discarded.
+ RemoveCredentialInjectorInterface();
+ if (status != AuthProviderStatus::OK) {
+ FX_LOGF(INFO, NULL, "Failed to capture auth code: Status %d", status);
+ SafelyCallbackGetPersistentCredential(status, nullptr, nullptr);
+ } else if (!auth_code.empty()) {
+ FX_LOGF(INFO, NULL, "Captured auth code of length %d", auth_code.size());
+ ExchangeAuthCode(auth_code);
+ }
+ }
+
+ callback();
+}
+
+void GoogleAuthProviderImpl::InjectPersistentCredential(
+ fuchsia::auth::UserProfileInfoPtr user_profile_info,
+ std::string credential) {
+ ReleaseResources();
+ RemoveCredentialInjectorInterface();
+ FX_LOGF(INFO, NULL, "Received injection request with credential of length %d",
+ credential.size());
+ SafelyCallbackGetPersistentCredential(AuthProviderStatus::OK,
+ std::move(credential),
+ std::move(user_profile_info));
+}
+
+std::string GoogleAuthProviderImpl::GetAuthorizeUrl(
+ fidl::StringPtr user_profile) {
+ // TODO(ukode,jsankey): use app_scopes instead of |kScopes|.
+ const std::vector<std::string> scopes(kScopes.begin(), kScopes.end());
+ std::string scopes_str = fxl::JoinStrings(scopes, "+");
+
+ std::string url = settings_.use_dedicated_endpoint ? kGoogleFuchsiaEndpoint
+ : kGoogleOAuthAuthEndpoint;
+ url += "?scope=" + scopes_str;
+ url += "&glif=";
+ url += settings_.use_glif ? "true" : "false";
+ url += "&response_type=code&redirect_uri=";
+ url += kRedirectUri;
+ url += "&client_id=";
+ url += kFuchsiaClientId;
+ // TODO(ukode,jsankey): Set user_profile_id in the state query arg for re-auth
+ // This probably involves moving the current implementation of UrlEncoding in
+ // OAuthRequestBuilder to a reusable library and using this to urlencode the
+ // supplied user_profile into the login_hint query parameter.
+ return url;
+}
+
+void GoogleAuthProviderImpl::ExchangeAuthCode(std::string auth_code) {
+ auto request = OAuthRequestBuilder(kGoogleOAuthTokenEndpoint, "POST")
+ .SetUrlEncodedBody("code=" + auth_code +
+ "&redirect_uri=" + kRedirectUri +
+ "&client_id=" + kFuchsiaClientId +
+ "&grant_type=authorization_code");
+
+ auto request_factory = [request = std::move(request)] {
+ return request.Build();
+ };
+
+ Request(std::move(request_factory), [this](http::URLResponse response) {
+ auto oauth_response = ParseOAuthResponse(std::move(response));
+ if (oauth_response.status != AuthProviderStatus::OK) {
+ LogOauthResponse("ExchangeAuthCode", oauth_response);
+ SafelyCallbackGetPersistentCredential(oauth_response.status, nullptr,
+ nullptr);
+ return;
+ }
+
+ const char kRootSchema[] = R"({
+ "type": "object",
+ "properties": {
+ "refresh_token": {
+ "type": "string"
+ },
+ "access_token": {
+ "type": "string"
+ }
+ },
+ "required": ["refresh_token", "access_token"]
+ })";
+ auto root_schema = rapidjson_utils::InitSchema(kRootSchema);
+ if (!root_schema) {
+ SafelyCallbackGetPersistentCredential(AuthProviderStatus::INTERNAL_ERROR,
+ nullptr, nullptr);
+ return;
+ }
+ if (!rapidjson_utils::ValidateSchema(oauth_response.json_response,
+ *root_schema)) {
+ FX_LOGF(WARNING, NULL,
+ "Got response without refresh and access tokens: %s",
+ JsonValueToPrettyString(oauth_response.json_response).c_str());
+ SafelyCallbackGetPersistentCredential(
+ AuthProviderStatus::OAUTH_SERVER_ERROR, nullptr, nullptr);
+ return;
+ }
+
+ auto refresh_token =
+ oauth_response.json_response["refresh_token"].GetString();
+ auto access_token =
+ oauth_response.json_response["access_token"].GetString();
+ FX_LOGF(INFO, NULL, "Received refresh token of length %d",
+ strlen(refresh_token));
+
+ GetUserProfile(refresh_token, access_token);
+ });
+}
+
+void GoogleAuthProviderImpl::GetUserProfile(fidl::StringPtr credential,
+ fidl::StringPtr access_token) {
+ FXL_DCHECK(credential.get().size() > 0);
+ FXL_DCHECK(access_token.get().size() > 0);
+
+ auto request = OAuthRequestBuilder(kGoogleUserInfoEndpoint, "GET")
+ .SetAuthorizationHeader(access_token.get());
+
+ auto request_factory = [request = std::move(request)] {
+ return request.Build();
+ };
+
+ Request(std::move(request_factory), [this,
+ credential](http::URLResponse response) {
+ fuchsia::auth::UserProfileInfoPtr user_profile_info =
+ fuchsia::auth::UserProfileInfo::New();
+
+ auto oauth_response = ParseOAuthResponse(std::move(response));
+ if (oauth_response.status != AuthProviderStatus::OK) {
+ LogOauthResponse("UserInfo", oauth_response);
+ SafelyCallbackGetPersistentCredential(oauth_response.status, credential,
+ std::move(user_profile_info));
+ return;
+ }
+
+ if (oauth_response.json_response.HasMember("sub") &&
+ oauth_response.json_response["sub"].IsString()) {
+ user_profile_info->id = oauth_response.json_response["sub"].GetString();
+ } else {
+ LogOauthResponse("UserInfo", oauth_response);
+ FX_LOG(INFO, NULL, "Missing unique identifier in UserInfo response");
+ SafelyCallbackGetPersistentCredential(
+ AuthProviderStatus::OAUTH_SERVER_ERROR, nullptr,
+ std::move(user_profile_info));
+ return;
+ }
+
+ if (oauth_response.json_response.HasMember("name") &&
+ oauth_response.json_response["name"].IsString()) {
+ user_profile_info->display_name =
+ oauth_response.json_response["name"].GetString();
+ }
+
+ if (oauth_response.json_response.HasMember("profile") &&
+ oauth_response.json_response["profile"].IsString()) {
+ user_profile_info->url =
+ oauth_response.json_response["profile"].GetString();
+ }
+
+ if (oauth_response.json_response.HasMember("picture") &&
+ oauth_response.json_response["picture"].IsString()) {
+ user_profile_info->image_url =
+ oauth_response.json_response["picture"].GetString();
+ }
+
+ FX_LOG(INFO, NULL, "Received valid UserInfo response");
+ SafelyCallbackGetPersistentCredential(oauth_response.status, credential,
+ std::move(user_profile_info));
+ });
+}
+
+fuchsia::ui::views::ViewHolderToken GoogleAuthProviderImpl::SetupChromium() {
+ // Connect to the Chromium service and create a new frame.
+ auto context_provider =
+ context_->svc()->Connect<fuchsia::web::ContextProvider>();
+
+ fidl::InterfaceHandle<fuchsia::io::Directory> incoming_service_clone =
+ context_->svc()->CloneChannel();
+
+ if (!incoming_service_clone.is_valid()) {
+ FX_LOG(ERROR, NULL, "Failed to clone service directory");
+ return fuchsia::ui::views::ViewHolderToken();
+ }
+
+ fuchsia::web::CreateContextParams params;
+ params.set_service_directory(std::move(incoming_service_clone));
+ context_provider->Create(std::move(params), web_context_.NewRequest());
+ web_context_->CreateFrame(web_frame_.NewRequest());
+
+ // Bind ourselves as a NavigationEventListener on this frame.
+ fuchsia::web::NavigationEventListenerPtr navigation_event_listener;
+ navigation_event_listener_bindings_.AddBinding(
+ this, navigation_event_listener.NewRequest());
+ web_frame_->SetNavigationEventListener(std::move(navigation_event_listener));
+
+ // And create a view for the frame.
+ auto [view_token, view_holder_token] = scenic::NewViewTokenPair();
+ web_frame_->CreateView(std::move(view_token));
+
+ return std::move(view_holder_token);
+}
+
+void GoogleAuthProviderImpl::SafelyCallbackGetPersistentCredential(
+ AuthProviderStatus auth_provider_status, fidl::StringPtr credential,
+ fuchsia::auth::UserProfileInfoPtr user_profile_info) {
+ if (get_persistent_credential_callback_) {
+ get_persistent_credential_callback_(auth_provider_status,
+ std::move(credential),
+ std::move(user_profile_info));
+ get_persistent_credential_callback_ = nullptr;
+ } else {
+ FX_LOG(WARNING, NULL,
+ "Attempted to call GetPersistentCredential callback twice.");
+ }
+}
+
+void GoogleAuthProviderImpl::ReleaseResources() {
+ // Close any open view
+ if (auth_ui_context_) {
+ FX_LOG(INFO, NULL, "Releasing Auth UI Context");
+ auth_ui_context_.set_error_handler(nullptr);
+ auth_ui_context_->StopOverlay();
+ auth_ui_context_ = nullptr;
+ }
+ // Release all smart pointers for Chromium resources.
+ web_frame_ = nullptr;
+ web_context_ = nullptr;
+}
+
+void GoogleAuthProviderImpl::ExposeCredentialInjectorInterface() {
+ context_->outgoing()->debug_dir()->AddEntry(
+ kInjectionEntry,
+ std::make_unique<vfs::Service>(injector_bindings_.GetHandler(this)));
+}
+
+void GoogleAuthProviderImpl::RemoveCredentialInjectorInterface() {
+ if (context_->outgoing()->debug_dir()->RemoveEntry(kInjectionEntry) ==
+ ZX_ERR_NOT_FOUND) {
+ FX_LOGF(WARNING, NULL,
+ "Attempted to remove nonexistent '%s' from debug directory",
+ kInjectionEntry);
+ }
+}
+
+void GoogleAuthProviderImpl::Request(
+ fit::function<http::URLRequest()> request_factory,
+ fit::function<void(http::URLResponse response)> callback) {
+ requests_.emplace(network_wrapper_->Request(std::move(request_factory),
+ std::move(callback)));
+}
+
+} // namespace google_auth_provider
diff --git a/src/identity/bin/google_auth_provider/google_auth_provider_impl.h b/src/identity/bin/google_auth_provider/google_auth_provider_impl.h
new file mode 100644
index 0000000..2816916
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/google_auth_provider_impl.h
@@ -0,0 +1,171 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This application serves as the Google Auth provider for generating OAuth
+// credentials to talk to Google Api backends. This application implements
+// |auth_provider.fidl| interface and is typically invoked by the Token Manager
+// service in Garnet layer.
+
+#ifndef SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_GOOGLE_AUTH_PROVIDER_IMPL_H_
+#define SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_GOOGLE_AUTH_PROVIDER_IMPL_H_
+
+#include <fuchsia/auth/cpp/fidl.h>
+#include <fuchsia/auth/testing/cpp/fidl.h>
+#include <fuchsia/ui/views/cpp/fidl.h>
+#include <fuchsia/web/cpp/fidl.h>
+#include <lib/callback/cancellable.h>
+#include <lib/fidl/cpp/binding.h>
+#include <lib/fidl/cpp/binding_set.h>
+#include <lib/fit/function.h>
+#include <lib/network_wrapper/network_wrapper.h>
+#include <lib/sys/cpp/component_context.h>
+
+#include "src/identity/bin/google_auth_provider/settings.h"
+#include "src/lib/fxl/macros.h"
+
+namespace google_auth_provider {
+
+using fuchsia::auth::AssertionJWTParams;
+using fuchsia::auth::AttestationJWTParams;
+using fuchsia::auth::AttestationSigner;
+using fuchsia::auth::AuthenticationUIContext;
+using fuchsia::web::NavigationState;
+
+class GoogleAuthProviderImpl
+ : fuchsia::web::NavigationEventListener,
+ fuchsia::auth::AuthProvider,
+ fuchsia::auth::testing::LegacyAuthCredentialInjector {
+ public:
+ GoogleAuthProviderImpl(
+ async_dispatcher_t* main_dispatcher, sys::ComponentContext* context,
+ network_wrapper::NetworkWrapper* network_wrapper, Settings settings,
+ fidl::InterfaceRequest<fuchsia::auth::AuthProvider> request);
+
+ ~GoogleAuthProviderImpl() override;
+
+ void set_on_empty(fit::closure on_empty) { on_empty_ = std::move(on_empty); }
+
+ private:
+ // |AuthProvider|
+ void GetPersistentCredential(
+ fidl::InterfaceHandle<fuchsia::auth::AuthenticationUIContext>
+ auth_ui_context,
+ fidl::StringPtr user_profile_id,
+ GetPersistentCredentialCallback callback) override;
+
+ // |AuthProvider|
+ void GetAppAccessToken(std::string credential, fidl::StringPtr app_client_id,
+ const std::vector<std::string> app_scopes,
+ GetAppAccessTokenCallback callback) override;
+
+ // |AuthProvider|
+ void GetAppIdToken(std::string credential, fidl::StringPtr audience,
+ GetAppIdTokenCallback callback) override;
+
+ // |AuthProvider|
+ void GetAppFirebaseToken(std::string id_token, std::string firebase_api_key,
+ GetAppFirebaseTokenCallback callback) override;
+
+ // |AuthProvider|
+ void RevokeAppOrPersistentCredential(
+ std::string credential,
+ RevokeAppOrPersistentCredentialCallback callback) override;
+
+ // |AuthProvider|
+ void GetPersistentCredentialFromAttestationJWT(
+ fidl::InterfaceHandle<AttestationSigner> attestation_signer,
+ AttestationJWTParams jwt_params,
+ fidl::InterfaceHandle<AuthenticationUIContext> auth_ui_context,
+ fidl::StringPtr user_profile_id,
+ GetPersistentCredentialFromAttestationJWTCallback callback) override;
+
+ // |AuthProvider|
+ void GetAppAccessTokenFromAssertionJWT(
+ fidl::InterfaceHandle<AttestationSigner> attestation_signer,
+ AssertionJWTParams jwt_params, std::string credential,
+ std::vector<std::string> scopes,
+ GetAppAccessTokenFromAssertionJWTCallback callback) override;
+
+ // |fuchsia::web::NavigationEventListener|
+ void OnNavigationStateChanged(
+ NavigationState change,
+ OnNavigationStateChangedCallback callback) override;
+
+ // |fuchsia::auth::testing::LegacyAuthCredentialInjector|
+ // This is a short-term solution to enable end-to-end testing. It should not
+ // be carried over during any refactoring efforts.
+ void InjectPersistentCredential(
+ fuchsia::auth::UserProfileInfoPtr user_profile_info,
+ std::string credential) override;
+
+ // Returns the URL to be used for the authentication call, respecting any
+ // settings that influence the URL.
+ std::string GetAuthorizeUrl(fidl::StringPtr user_profile_id);
+
+ // Calls the OAuth auth endpoint to exchange the supplied |auth_code| for a
+ // long term credential, and then calls |GetUserProfile| with that credential.
+ // If any errors are encountered a failure status is returned on the pending
+ // |get_credential_callback_|.
+ void ExchangeAuthCode(std::string auth_code);
+
+ // Calls the people endpoint to gather profile information using the supplied
+ // |access_token| and responds to the pending |get_credential_callback_|.
+ void GetUserProfile(fidl::StringPtr credential, fidl::StringPtr access_token);
+
+ // Launches and connects to a Chromium frame, binding |this| as a
+ // |NavigationEventListener| to process any changes in the URL, and returning
+ // a |fuchsia::ui::views::ViewHolderToken| token for the view's ViewHolder.
+ fuchsia::ui::views::ViewHolderToken SetupChromium();
+
+ // Calls the GetPersistentCredential callback if one is available, or logs
+ // and returns immediately otherwise. This enables interactive signin or
+ // InjectPersistentCredential to terminate gracefully even after the other
+ // has sent a response to the pending callback.
+ void SafelyCallbackGetPersistentCredential(
+ fuchsia::auth::AuthProviderStatus auth_provider_status,
+ fidl::StringPtr credential,
+ fuchsia::auth::UserProfileInfoPtr user_profile_info);
+
+ // Exposes a |fuchsia::auth::testing::LegacyAuthCredentialInjector| handle on
+ // the output debug directory.
+ void ExposeCredentialInjectorInterface();
+
+ // Removes the |fuchsia::auth::testing::LegacyAuthCredentialInjector| handle
+ // on the output debug directory.
+ void RemoveCredentialInjectorInterface();
+
+ // Safely releases any resources associated with an open Webkit or Chromium
+ // instance, including the associated view.
+ void ReleaseResources();
+
+ void Request(
+ fit::function<::fuchsia::net::oldhttp::URLRequest()> request_factory,
+ fit::function<void(::fuchsia::net::oldhttp::URLResponse response)>
+ callback);
+
+ async_dispatcher_t* const main_dispatcher_;
+ sys::ComponentContext* context_;
+ network_wrapper::NetworkWrapper* const network_wrapper_;
+ const Settings settings_;
+ fuchsia::sys::ComponentControllerPtr web_view_controller_;
+ fuchsia::auth::AuthenticationUIContextPtr auth_ui_context_;
+ fuchsia::web::ContextPtr web_context_;
+ fuchsia::web::FramePtr web_frame_;
+ GetPersistentCredentialCallback get_persistent_credential_callback_;
+
+ fidl::BindingSet<fuchsia::web::NavigationEventListener>
+ navigation_event_listener_bindings_;
+ fidl::BindingSet<fuchsia::auth::testing::LegacyAuthCredentialInjector>
+ injector_bindings_;
+ fidl::Binding<fuchsia::auth::AuthProvider> binding_;
+ callback::CancellableContainer requests_;
+
+ fit::closure on_empty_;
+
+ FXL_DISALLOW_COPY_AND_ASSIGN(GoogleAuthProviderImpl);
+};
+
+} // namespace google_auth_provider
+
+#endif // SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_GOOGLE_AUTH_PROVIDER_IMPL_H_
diff --git a/src/identity/bin/google_auth_provider/google_auth_provider_impl_unittest.cc b/src/identity/bin/google_auth_provider/google_auth_provider_impl_unittest.cc
new file mode 100644
index 0000000..7b398c7
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/google_auth_provider_impl_unittest.cc
@@ -0,0 +1,285 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/bin/google_auth_provider/google_auth_provider_impl.h"
+
+#include "lib/callback/capture.h"
+#include "lib/callback/set_when_called.h"
+#include "lib/fidl/cpp/binding.h"
+#include "lib/gtest/test_loop_fixture.h"
+#include "lib/network_wrapper/fake_network_wrapper.h"
+#include "peridot/lib/rapidjson/rapidjson.h"
+#include "src/identity/bin/google_auth_provider/settings.h"
+#include "src/lib/fxl/macros.h"
+
+namespace google_auth_provider {
+namespace {
+
+using fuchsia::auth::AuthProviderStatus;
+using fuchsia::auth::AuthTokenPtr;
+
+class GoogleAuthProviderImplTest : public gtest::TestLoopFixture {
+ public:
+ GoogleAuthProviderImplTest()
+ : network_wrapper_(dispatcher()),
+ context_(sys::ComponentContext::Create().get()),
+ google_auth_provider_impl_(dispatcher(), context_, &network_wrapper_,
+ {}, auth_provider_.NewRequest()) {}
+
+ ~GoogleAuthProviderImplTest() override {}
+
+ protected:
+ network_wrapper::FakeNetworkWrapper network_wrapper_;
+ sys::ComponentContext* context_;
+ fuchsia::auth::AuthProviderPtr auth_provider_;
+ GoogleAuthProviderImpl google_auth_provider_impl_;
+
+ private:
+ FXL_DISALLOW_COPY_AND_ASSIGN(GoogleAuthProviderImplTest);
+};
+
+TEST_F(GoogleAuthProviderImplTest, EmptyWhenClientDisconnected) {
+ bool on_empty_called = false;
+ google_auth_provider_impl_.set_on_empty([this, &on_empty_called] {
+ on_empty_called = true;
+ QuitLoop();
+ });
+ auth_provider_.Unbind();
+ RunLoopUntilIdle();
+ EXPECT_TRUE(on_empty_called);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppAccessTokenSuccess) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ AuthTokenPtr access_token;
+ std::vector<std::string> scopes;
+ scopes.push_back("https://www.googleapis.com/auth/gmail.modify");
+ scopes.push_back("https://www.googleapis.com/auth/userinfo.email");
+
+ rapidjson::Document ok_response;
+ ok_response.Parse(
+ "{\"access_token\":\"test_at_token\", \"expires_in\":3600}");
+ network_wrapper_.SetStringResponse(
+ modular::JsonValueToPrettyString(ok_response), 200);
+
+ auth_provider_->GetAppAccessToken(
+ "credential", "test_client_id", std::move(scopes),
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &access_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::OK);
+ EXPECT_FALSE(access_token == NULL);
+ EXPECT_EQ(access_token->token_type, fuchsia::auth::TokenType::ACCESS_TOKEN);
+ EXPECT_EQ(access_token->token, "test_at_token");
+ EXPECT_EQ(access_token->expires_in, 3600u);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppAccessTokenBadRequestError) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ fuchsia::auth::AuthTokenPtr access_token;
+ std::vector<std::string> scopes;
+ scopes.push_back("https://www.googleapis.com/auth/gmail.modify");
+
+ auth_provider_->GetAppAccessToken(
+ "", "", std::move(scopes),
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &access_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::BAD_REQUEST);
+ EXPECT_TRUE(access_token == NULL);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppAccessTokenInvalidClientError) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ AuthTokenPtr access_token;
+ std::vector<std::string> scopes;
+ scopes.push_back("https://www.googleapis.com/auth/gmail.modify");
+ scopes.push_back("https://www.googleapis.com/auth/userinfo.email");
+
+ rapidjson::Document ok_response;
+ ok_response.Parse("{\"error\":\"invalid_client\"}");
+ network_wrapper_.SetStringResponse(
+ modular::JsonValueToPrettyString(ok_response), 401);
+
+ auth_provider_->GetAppAccessToken(
+ "credential", "invalid_client_id", std::move(scopes),
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &access_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::OAUTH_SERVER_ERROR);
+ EXPECT_TRUE(access_token == NULL);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppAccessTokenInvalidUserError) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ AuthTokenPtr access_token;
+ std::vector<std::string> scopes;
+ scopes.push_back("https://www.googleapis.com/auth/gmail.modify");
+ scopes.push_back("https://www.googleapis.com/auth/userinfo.email");
+
+ rapidjson::Document ok_response;
+ ok_response.Parse("{\"error\":\"invalid_credential\"}");
+ network_wrapper_.SetStringResponse(
+ modular::JsonValueToPrettyString(ok_response), 401);
+
+ auth_provider_->GetAppAccessToken(
+ "invalid_credential", "test_client_id", std::move(scopes),
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &access_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::OAUTH_SERVER_ERROR);
+ EXPECT_TRUE(access_token == NULL);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppIdTokenSuccess) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ AuthTokenPtr id_token;
+
+ rapidjson::Document ok_response;
+ ok_response.Parse("{\"id_token\":\"test_id_token\", \"expires_in\":3600}");
+ network_wrapper_.SetStringResponse(
+ modular::JsonValueToPrettyString(ok_response), 200);
+
+ auth_provider_->GetAppIdToken(
+ "credential", "test_audience",
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &id_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::OK);
+ EXPECT_FALSE(id_token == NULL);
+ EXPECT_EQ(id_token->token_type, fuchsia::auth::TokenType::ID_TOKEN);
+ EXPECT_EQ(id_token->token, "test_id_token");
+ EXPECT_EQ(id_token->expires_in, 3600u);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppIdTokenBadRequestError) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ AuthTokenPtr id_token;
+
+ auth_provider_->GetAppIdToken(
+ "", "test_audience",
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &id_token));
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::BAD_REQUEST);
+ EXPECT_TRUE(id_token == NULL);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppIdTokenInvalidAudienceError) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ AuthTokenPtr id_token;
+
+ rapidjson::Document ok_response;
+ ok_response.Parse("{\"error\":\"invalid_client\"}");
+ network_wrapper_.SetStringResponse(
+ modular::JsonValueToPrettyString(ok_response), 401);
+
+ auth_provider_->GetAppIdToken(
+ "credential", "invalid_audience",
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &id_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::OAUTH_SERVER_ERROR);
+ EXPECT_TRUE(id_token == NULL);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppIdTokenInvalidUserError) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ AuthTokenPtr id_token;
+
+ rapidjson::Document ok_response;
+ ok_response.Parse("{\"error\":\"invalid_credential\"}");
+ network_wrapper_.SetStringResponse(
+ modular::JsonValueToPrettyString(ok_response), 401);
+
+ auth_provider_->GetAppIdToken(
+ "invalid_credential", "audience",
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &id_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::OAUTH_SERVER_ERROR);
+ EXPECT_TRUE(id_token == NULL);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppFirebaseTokenSuccess) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ fuchsia::auth::FirebaseTokenPtr fb_token;
+
+ rapidjson::Document ok_response;
+ ok_response.Parse(
+ "{\"idToken\":\"test_fb_token\", \"localId\":\"test123\",\
+ \"email\":\"foo@example.com\", \"expiresIn\":\"3600\"}");
+ network_wrapper_.SetStringResponse(
+ modular::JsonValueToPrettyString(ok_response), 200);
+
+ auth_provider_->GetAppFirebaseToken(
+ "test_id_token", "test_firebase_api_key",
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &fb_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::OK);
+ EXPECT_FALSE(fb_token == NULL);
+ EXPECT_EQ(fb_token->id_token, "test_fb_token");
+ EXPECT_EQ(fb_token->local_id, "test123");
+ EXPECT_EQ(fb_token->email, "foo@example.com");
+ EXPECT_EQ(fb_token->expires_in, 3600u);
+}
+
+TEST_F(GoogleAuthProviderImplTest, GetAppFirebaseTokenBadRequestError) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+ fuchsia::auth::FirebaseTokenPtr fb_token;
+
+ auth_provider_->GetAppFirebaseToken(
+ "", "",
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status,
+ &fb_token));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::BAD_REQUEST);
+ EXPECT_TRUE(fb_token == NULL);
+}
+
+TEST_F(GoogleAuthProviderImplTest, RevokeAppOrPersistentCredentialUnsupported) {
+ bool callback_called = false;
+ auto status = AuthProviderStatus::INTERNAL_ERROR;
+
+ auth_provider_->RevokeAppOrPersistentCredential(
+ "",
+ callback::Capture(callback::SetWhenCalled(&callback_called), &status));
+
+ RunLoopUntilIdle();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(status, AuthProviderStatus::BAD_REQUEST);
+}
+
+} // namespace
+} // namespace google_auth_provider
diff --git a/src/identity/bin/google_auth_provider/main.cc b/src/identity/bin/google_auth_provider/main.cc
new file mode 100644
index 0000000..2cba0c97
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/main.cc
@@ -0,0 +1,89 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <fuchsia/auth/cpp/fidl.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/sys/cpp/component_context.h>
+#include <trace-provider/provider.h>
+
+#include <memory>
+
+#include "lib/backoff/exponential_backoff.h"
+#include "lib/fidl/cpp/binding_set.h"
+#include "lib/fidl/cpp/interface_request.h"
+#include "lib/fsl/syslogger/init.h"
+#include "lib/fsl/vmo/strings.h"
+#include "lib/network_wrapper/network_wrapper_impl.h"
+#include "src/identity/bin/google_auth_provider/factory_impl.h"
+#include "src/identity/bin/google_auth_provider/settings.h"
+#include "src/lib/fxl/command_line.h"
+
+namespace {
+
+namespace http = ::fuchsia::net::oldhttp;
+using fuchsia::auth::AuthProviderFactory;
+using google_auth_provider::Settings;
+
+class GoogleAuthProviderApp {
+ public:
+ GoogleAuthProviderApp(fxl::CommandLine command_line)
+ : loop_(&kAsyncLoopConfigAttachToThread),
+ component_context_(sys::ComponentContext::Create()),
+ trace_provider_(loop_.dispatcher()),
+ network_wrapper_(
+ loop_.dispatcher(), std::make_unique<backoff::ExponentialBackoff>(),
+ [this] {
+ return component_context_->svc()->Connect<http::HttpService>();
+ }),
+ factory_impl_(loop_.dispatcher(), component_context_.get(),
+ &network_wrapper_, CreateSettings(command_line)) {
+ FXL_DCHECK(component_context_);
+ }
+
+ ~GoogleAuthProviderApp() { loop_.Quit(); }
+
+ void Run() {
+ component_context_->outgoing()->AddPublicService<AuthProviderFactory>(
+ [this](fidl::InterfaceRequest<AuthProviderFactory> request) {
+ factory_impl_.Bind(std::move(request));
+ });
+ loop_.Run();
+ }
+
+ private:
+ async::Loop loop_;
+ std::unique_ptr<sys::ComponentContext> component_context_;
+ trace::TraceProvider trace_provider_;
+ network_wrapper::NetworkWrapperImpl network_wrapper_;
+ google_auth_provider::FactoryImpl factory_impl_;
+
+ static Settings CreateSettings(fxl::CommandLine command_line) {
+ Settings settings;
+ if (command_line.HasOption("glif")) {
+ settings.use_glif = true;
+ } else if (command_line.HasOption("redcarpet")) {
+ settings.use_glif = false;
+ }
+ if (command_line.HasOption("fuchsiaendpoint")) {
+ settings.use_dedicated_endpoint = true;
+ } else if (command_line.HasOption("oauthendpoint")) {
+ settings.use_dedicated_endpoint = false;
+ }
+ return settings;
+ }
+
+ FXL_DISALLOW_COPY_AND_ASSIGN(GoogleAuthProviderApp);
+};
+
+} // namespace
+
+int main(int argc, const char** argv) {
+ auto command_line = fxl::CommandLineFromArgcArgv(argc, argv);
+ fsl::InitLoggerFromCommandLine(command_line, {"auth"});
+
+ GoogleAuthProviderApp app(command_line);
+ app.Run();
+
+ return 0;
+}
diff --git a/src/identity/bin/google_auth_provider/meta/google_auth_provider_2.cmx b/src/identity/bin/google_auth_provider/meta/google_auth_provider_2.cmx
new file mode 100644
index 0000000..2731608
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/meta/google_auth_provider_2.cmx
@@ -0,0 +1,22 @@
+{
+ "program": {
+ "binary": "bin/app"
+ },
+ "sandbox": {
+ "services": [
+ "fuchsia.fonts.Provider",
+ "fuchsia.media.Audio",
+ "fuchsia.net.SocketProvider",
+ "fuchsia.net.oldhttp.HttpService",
+ "fuchsia.netstack.Netstack",
+ "fuchsia.process.Launcher",
+ "fuchsia.sys.Launcher",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input.ImeVisibilityService",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.logger.LogSink",
+ "fuchsia.tracelink.Registry",
+ "fuchsia.web.ContextProvider"
+ ]
+ }
+}
diff --git a/src/identity/bin/google_auth_provider/settings.h b/src/identity/bin/google_auth_provider/settings.h
new file mode 100644
index 0000000..7439a52
--- /dev/null
+++ b/src/identity/bin/google_auth_provider/settings.h
@@ -0,0 +1,21 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_SETTINGS_H_
+#define SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_SETTINGS_H_
+
+namespace google_auth_provider {
+
+struct Settings {
+ // Set true to request the "GLIF" UI style. The default of false will request
+ // the legacy "RedCarpet" UI style.
+ bool use_glif = false;
+ // Set true to connect to a dedicated authentication endpoint for Fuchsia
+ // instead of the standard OAuth endpoint.
+ bool use_dedicated_endpoint = false;
+};
+
+} // namespace google_auth_provider
+
+#endif // SRC_IDENTITY_BIN_GOOGLE_AUTH_PROVIDER_SETTINGS_H_
diff --git a/src/identity/bin/meta/google_auth_provider_unittests.cmx b/src/identity/bin/meta/google_auth_provider_unittests.cmx
new file mode 100644
index 0000000..0114639
--- /dev/null
+++ b/src/identity/bin/meta/google_auth_provider_unittests.cmx
@@ -0,0 +1,8 @@
+{
+ "program": {
+ "binary": "test/google_auth_provider_unittests"
+ },
+ "sandbox": {
+ "services": []
+ }
+}
diff --git a/src/identity/lib/BUILD.gn b/src/identity/lib/BUILD.gn
index d12eb2f..543898d 100644
--- a/src/identity/lib/BUILD.gn
+++ b/src/identity/lib/BUILD.gn
@@ -21,6 +21,7 @@
test_package("identity_lib_unittests") {
deps = [
+ "oauth:oauth_unittests",
"token_cache:token_cache",
"token_store:token_store",
]
@@ -34,5 +35,9 @@
name = "identity_token_store_lib_test"
environments = basic_envs
},
+ {
+ name = "oauth_unittests"
+ environments = basic_envs
+ },
]
}
diff --git a/src/identity/lib/meta/oauth_unittests.cmx b/src/identity/lib/meta/oauth_unittests.cmx
new file mode 100644
index 0000000..30723ca
--- /dev/null
+++ b/src/identity/lib/meta/oauth_unittests.cmx
@@ -0,0 +1,8 @@
+{
+ "program": {
+ "binary": "test/oauth_unittests"
+ },
+ "sandbox": {
+ "services": []
+ }
+}
diff --git a/src/identity/lib/oauth/BUILD.gn b/src/identity/lib/oauth/BUILD.gn
new file mode 100644
index 0000000..76a8b6b
--- /dev/null
+++ b/src/identity/lib/oauth/BUILD.gn
@@ -0,0 +1,52 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/package.gni")
+
+source_set("lib") {
+ sources = [
+ "oauth_request_builder.cc",
+ "oauth_request_builder.h",
+ "oauth_response.cc",
+ "oauth_response.h",
+ ]
+
+ public_deps = [
+ "//sdk/fidl/fuchsia.auth",
+ "//sdk/fidl/fuchsia.net.oldhttp",
+ ]
+ deps = [
+ "//garnet/public/lib/fsl",
+ "//garnet/public/lib/network_wrapper",
+ "//peridot/lib/rapidjson",
+ "//src/lib/fxl",
+ ]
+}
+
+executable("oauth_unittests") {
+ testonly = true
+
+ deps = [
+ ":unittests",
+ "//third_party/googletest:gtest_main",
+ ]
+}
+
+source_set("unittests") {
+ testonly = true
+
+ sources = [
+ "oauth_request_builder_unittest.cc",
+ "oauth_response_unittest.cc",
+ ]
+
+ deps = [
+ ":lib",
+ "//garnet/public/lib/fsl",
+ "//peridot/lib/rapidjson",
+ "//src/lib/fxl",
+ "//src/lib/fxl:printers",
+ "//third_party/googletest:gtest",
+ ]
+}
diff --git a/src/identity/lib/oauth/oauth_request_builder.cc b/src/identity/lib/oauth/oauth_request_builder.cc
new file mode 100644
index 0000000..e8e0d9c
--- /dev/null
+++ b/src/identity/lib/oauth/oauth_request_builder.cc
@@ -0,0 +1,122 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/lib/oauth/oauth_request_builder.h"
+
+#include <iomanip>
+#include <iostream>
+
+#include "lib/fsl/socket/strings.h"
+#include "lib/fsl/vmo/strings.h"
+#include "src/lib/fxl/logging.h"
+#include "src/lib/fxl/macros.h"
+#include "src/lib/fxl/strings/join_strings.h"
+#include "src/lib/fxl/strings/string_number_conversions.h"
+
+namespace auth_providers {
+namespace oauth {
+
+namespace http = ::fuchsia::net::oldhttp;
+
+namespace {
+
+std::string UrlEncode(const std::string& value) {
+ std::ostringstream escaped;
+ escaped.fill('0');
+ escaped << std::hex;
+
+ for (char c : value) {
+ // Keep alphanumeric and other accepted characters intact
+ if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '=' ||
+ c == '&' || c == '+') {
+ escaped << c;
+ continue;
+ }
+
+ // Any other characters are percent-encoded
+ escaped << std::uppercase;
+ escaped << '%' << std::setw(2) << int(static_cast<unsigned char>(c));
+ escaped << std::nouppercase;
+ }
+
+ return escaped.str();
+}
+
+} // namespace
+
+OAuthRequestBuilder::OAuthRequestBuilder(const std::string& url,
+ const std::string& method)
+ : url_(url), method_(method) {
+ FXL_CHECK(!url_.empty());
+ FXL_CHECK(!method_.empty());
+}
+
+OAuthRequestBuilder::~OAuthRequestBuilder() {}
+
+OAuthRequestBuilder& OAuthRequestBuilder::SetAuthorizationHeader(
+ const std::string& token) {
+ FXL_DCHECK(!token.empty());
+ http_headers_["Authorization"] = "Bearer " + token;
+ return *this;
+}
+
+OAuthRequestBuilder& OAuthRequestBuilder::SetUrlEncodedBody(
+ const std::string& body) {
+ http_headers_["content-type"] = "application/x-www-form-urlencoded";
+
+ if (body.empty()) {
+ return *this;
+ }
+ return SetRequestBody(UrlEncode(body));
+}
+
+OAuthRequestBuilder& OAuthRequestBuilder::SetJsonBody(const std::string& body) {
+ http_headers_["accept"] = "application/json";
+ http_headers_["content-type"] = "application/json";
+ return SetRequestBody(body);
+}
+
+OAuthRequestBuilder& OAuthRequestBuilder::SetQueryParams(
+ std::map<std::string, std::string> query_params) {
+ for (auto it = query_params.begin(); it != query_params.end(); ++it) {
+ query_string_ += (it == query_params.begin() ? "?" : "&");
+ query_string_ += UrlEncode(it->first) + "=" + UrlEncode(it->second);
+ }
+ return *this;
+}
+
+http::URLRequest OAuthRequestBuilder::Build() const {
+ fsl::SizedVmo data;
+ auto result = fsl::VmoFromString(request_body_, &data);
+ FXL_CHECK(result);
+
+ http::URLRequest request;
+ request.url = url_ + query_string_;
+ request.method = method_;
+ request.auto_follow_redirects = true;
+ request.body = http::URLBody::New();
+ request.body->set_buffer(std::move(data).ToTransport());
+ for (const auto& http_header : http_headers_) {
+ http::HttpHeader hdr;
+ hdr.name = http_header.first;
+ hdr.value = http_header.second;
+ request.headers.push_back(std::move(hdr));
+ }
+
+ return request;
+}
+
+OAuthRequestBuilder& OAuthRequestBuilder::SetRequestBody(
+ const std::string& body) {
+ request_body_ = body;
+
+ uint64_t data_size = request_body_.length();
+ if (data_size > 0)
+ http_headers_["content-length"] = fxl::NumberToString(data_size).data();
+
+ return *this;
+}
+
+} // namespace oauth
+} // namespace auth_providers
diff --git a/src/identity/lib/oauth/oauth_request_builder.h b/src/identity/lib/oauth/oauth_request_builder.h
new file mode 100644
index 0000000..44ba3f9
--- /dev/null
+++ b/src/identity/lib/oauth/oauth_request_builder.h
@@ -0,0 +1,59 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_IDENTITY_LIB_OAUTH_OAUTH_REQUEST_BUILDER_H_
+#define SRC_IDENTITY_LIB_OAUTH_OAUTH_REQUEST_BUILDER_H_
+
+#include <fuchsia/net/oldhttp/cpp/fidl.h>
+
+namespace auth_providers {
+namespace oauth {
+
+// Request builder for an OAuth Https Request. This builder converts the
+// oauth endpoint request to an URI in the format as described by the OAuth
+// protocol specification:
+// - https://tools.ietf.org/html/rfc6749
+class OAuthRequestBuilder {
+ public:
+ OAuthRequestBuilder(const std::string& url, const std::string& method);
+
+ ~OAuthRequestBuilder();
+
+ // Sets the bearer token in the http authorization header field.
+ OAuthRequestBuilder& SetAuthorizationHeader(const std::string& token);
+
+ // Sets the HTTP request body to the url encoded format of |body|. This
+ // method also adds the relevant http request headers for content-type and
+ // content-length fields for posting "application/x-www-form-urlencoded" MIME
+ // datatypes.
+ OAuthRequestBuilder& SetUrlEncodedBody(const std::string& body);
+
+ // Sets the HTTP request body to the json encoded string |body|. This method
+ // also adds the relevant http headers for accept, content-type and
+ // content-length fields for posting JSON data.
+ OAuthRequestBuilder& SetJsonBody(const std::string& body);
+
+ // Url encodes the query params which are appended to the url string while
+ // building the request.
+ OAuthRequestBuilder& SetQueryParams(
+ std::map<std::string, std::string> query_params);
+
+ // Returns an HTTP |URLRequest| handle for the OAuth endpoint.
+ ::fuchsia::net::oldhttp::URLRequest Build() const;
+
+ private:
+ // Sets the HTTP request body field to |body|.
+ OAuthRequestBuilder& SetRequestBody(const std::string& body);
+
+ const std::string url_;
+ const std::string method_;
+ std::string query_string_;
+ std::string request_body_;
+ std::map<std::string, std::string> http_headers_;
+};
+
+} // namespace oauth
+} // namespace auth_providers
+
+#endif // SRC_IDENTITY_LIB_OAUTH_OAUTH_REQUEST_BUILDER_H_
diff --git a/src/identity/lib/oauth/oauth_request_builder_unittest.cc b/src/identity/lib/oauth/oauth_request_builder_unittest.cc
new file mode 100644
index 0000000..6aa3298
--- /dev/null
+++ b/src/identity/lib/oauth/oauth_request_builder_unittest.cc
@@ -0,0 +1,134 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/lib/oauth/oauth_request_builder.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+#include "peridot/lib/rapidjson/rapidjson.h"
+#include "src/lib/fxl/logging.h"
+
+namespace auth_providers {
+namespace oauth {
+
+namespace {
+
+constexpr char kTestUrl[] = "http://example.org";
+constexpr char kPostMethod[] = "POST";
+constexpr char kGetMethod[] = "GET";
+
+class OAuthRequestBuilderTest : public ::testing::Test {
+ protected:
+ OAuthRequestBuilderTest() {}
+
+ ~OAuthRequestBuilderTest() override {}
+};
+
+TEST_F(OAuthRequestBuilderTest, JsonEncodedPostRequest) {
+ rapidjson::Document json_doc;
+ json_doc.Parse(R"({"test_key":"test_val"})");
+
+ // convert json document to string
+ rapidjson::StringBuffer strbuf;
+ strbuf.Clear();
+ rapidjson::Writer<rapidjson::StringBuffer> writer(strbuf);
+ json_doc.Accept(writer);
+
+ auto req = OAuthRequestBuilder(kTestUrl, kPostMethod)
+ .SetJsonBody(strbuf.GetString())
+ .Build();
+
+ EXPECT_TRUE(req.url.find("example.org") != std::string::npos);
+ EXPECT_EQ(req.method, kPostMethod);
+ for (const auto& header : *req.headers) {
+ auto hdr_name = header.name;
+ if (hdr_name == "content-type") {
+ EXPECT_EQ(header.value, "application/json");
+ } else if (hdr_name == "content-length") {
+ EXPECT_TRUE(atoi(header.value.c_str()) > 0);
+ }
+ }
+}
+
+TEST_F(OAuthRequestBuilderTest, UrlEncodedPostRequest) {
+ auto req = OAuthRequestBuilder(kTestUrl, kPostMethod)
+ .SetUrlEncodedBody("test_data")
+ .Build();
+
+ EXPECT_TRUE(req.url.find("example.org") != std::string::npos);
+ EXPECT_EQ(req.method, kPostMethod);
+ EXPECT_TRUE(req.body);
+ for (const auto& header : *req.headers) {
+ auto hdr_name = header.name;
+ if (hdr_name == "content-type") {
+ EXPECT_EQ(header.value, "application/x-www-form-urlencoded");
+ } else if (hdr_name == "content-length") {
+ EXPECT_TRUE(atoi(header.value.c_str()) > 0);
+ }
+ }
+}
+
+TEST_F(OAuthRequestBuilderTest, EmptyBodyPostRequest) {
+ auto req =
+ OAuthRequestBuilder(kTestUrl, kPostMethod).SetUrlEncodedBody("").Build();
+
+ EXPECT_TRUE(req.url.find("example.org") != std::string::npos);
+ EXPECT_EQ(req.method, kPostMethod);
+ for (const auto& header : *req.headers) {
+ auto hdr_name = header.name;
+ if (hdr_name == "content-type") {
+ EXPECT_EQ(header.value, "application/x-www-form-urlencoded");
+ } else if (hdr_name == "content-length") {
+ EXPECT_TRUE(atoi(header.value.c_str()) == 0);
+ } else {
+ ASSERT_TRUE("This header should never been set");
+ }
+ }
+}
+
+TEST_F(OAuthRequestBuilderTest, CheckAuthHeader) {
+ auto req = OAuthRequestBuilder(kTestUrl, kPostMethod)
+ .SetUrlEncodedBody("test_data")
+ .SetAuthorizationHeader("test_token")
+ .Build();
+
+ EXPECT_TRUE(req.url.find("example.org") != std::string::npos);
+ EXPECT_EQ(req.method, kPostMethod);
+ EXPECT_TRUE(req.body);
+ for (const auto& header : *req.headers) {
+ auto hdr_name = header.name;
+ if (hdr_name == "Authorization") {
+ EXPECT_TRUE(header.value.find("test_token") != std::string::npos);
+ }
+ }
+}
+
+TEST_F(OAuthRequestBuilderTest, GetRequest) {
+ auto req = OAuthRequestBuilder(kTestUrl, kGetMethod).Build();
+
+ EXPECT_TRUE(req.url.find("example.org") != std::string::npos);
+ EXPECT_EQ(req.method, kGetMethod);
+}
+
+TEST_F(OAuthRequestBuilderTest, GetRequestWithQueryParams) {
+ std::map<std::string, std::string> params;
+ params["foo1"] = "bar1";
+ params["foo2"] = "bar2";
+ params["foo3"] = "bar 3";
+ auto req =
+ OAuthRequestBuilder(kTestUrl, kGetMethod).SetQueryParams(params).Build();
+
+ EXPECT_TRUE(req.url.find("example.org") != std::string::npos);
+ EXPECT_TRUE(req.url.find("foo1") != std::string::npos);
+ EXPECT_TRUE(req.url.find("foo2") != std::string::npos);
+ // check if the param values are url encoded
+ EXPECT_TRUE(req.url.find("bar%203") != std::string::npos);
+ EXPECT_EQ(req.method, kGetMethod);
+}
+
+} // namespace
+
+} // namespace oauth
+} // namespace auth_providers
diff --git a/src/identity/lib/oauth/oauth_response.cc b/src/identity/lib/oauth/oauth_response.cc
new file mode 100644
index 0000000..f758e0a
--- /dev/null
+++ b/src/identity/lib/oauth/oauth_response.cc
@@ -0,0 +1,76 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/lib/oauth/oauth_response.h"
+
+#include "lib/fsl/socket/strings.h"
+#include "rapidjson/error/en.h"
+#include "src/lib/fxl/logging.h"
+#include "src/lib/fxl/macros.h"
+
+namespace auth_providers {
+namespace oauth {
+
+namespace http = ::fuchsia::net::oldhttp;
+
+using fuchsia::auth::AuthProviderStatus;
+
+OAuthResponse ParseOAuthResponse(http::URLResponse response) {
+ rapidjson::Document out;
+ if (response.error) {
+ FXL_LOG(ERROR) << "Encountered error: " +
+ std::to_string(response.error->code) +
+ " ,with description: " +
+ response.error->description->data();
+ return OAuthResponse(AuthProviderStatus::NETWORK_ERROR,
+ response.error->description->data(), std::move(out));
+ }
+
+ std::string response_body;
+ if (response.body) {
+ FXL_DCHECK(response.body->is_stream());
+ if (!fsl::BlockingCopyToString(std::move(response.body->stream()),
+ &response_body)) {
+ FXL_LOG(ERROR) << "Internal error while reading response from socket,"
+ "network returned: " +
+ std::to_string(response.status_code);
+ return OAuthResponse(AuthProviderStatus::NETWORK_ERROR,
+ "Error reading response from socket",
+ std::move(out));
+ }
+ }
+
+ // OAuth errors are sent in the response body, parse the json response first
+ // to introspect the response.
+ rapidjson::ParseResult ok = out.Parse(response_body);
+ if (!ok) {
+ std::string error_msg = GetParseError_En(ok.Code());
+ return OAuthResponse(
+ AuthProviderStatus::BAD_RESPONSE,
+ "Error in parsing json response[" + response_body + "]: " + error_msg,
+ std::move(out));
+ }
+ switch (response.status_code) {
+ case 200: // Success
+ return OAuthResponse(AuthProviderStatus::OK, "", std::move(out));
+ case 400: // Bad request errors
+ case 401: // Unauthorized, returned with invalid_client.
+ case 403: // Forbidden, user denied access.
+ default:
+ std::string oauth_error(out.HasMember("error") && out["error"].IsString()
+ ? out["error"].GetString()
+ : "");
+ auto status = (oauth_error == "invalid_grant")
+ ? AuthProviderStatus::REAUTH_REQUIRED
+ : AuthProviderStatus::OAUTH_SERVER_ERROR;
+
+ return OAuthResponse(status,
+ "OAuth backend returned error: " +
+ std::to_string(response.status_code),
+ std::move(out));
+ }
+}
+
+} // namespace oauth
+} // namespace auth_providers
diff --git a/src/identity/lib/oauth/oauth_response.h b/src/identity/lib/oauth/oauth_response.h
new file mode 100644
index 0000000..26ccf5e
--- /dev/null
+++ b/src/identity/lib/oauth/oauth_response.h
@@ -0,0 +1,34 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_IDENTITY_LIB_OAUTH_OAUTH_RESPONSE_H_
+#define SRC_IDENTITY_LIB_OAUTH_OAUTH_RESPONSE_H_
+
+#include <fuchsia/auth/cpp/fidl.h>
+#include <fuchsia/net/oldhttp/cpp/fidl.h>
+
+#include "rapidjson/document.h"
+
+namespace auth_providers {
+namespace oauth {
+
+struct OAuthResponse {
+ const fuchsia::auth::AuthProviderStatus status;
+ const std::string error_description;
+ rapidjson::Document json_response;
+
+ OAuthResponse(const fuchsia::auth::AuthProviderStatus& status,
+ const std::string& error_description,
+ rapidjson::Document json_response)
+ : status(status),
+ error_description(error_description),
+ json_response(std::move(json_response)) {}
+};
+
+OAuthResponse ParseOAuthResponse(::fuchsia::net::oldhttp::URLResponse response);
+
+} // namespace oauth
+} // namespace auth_providers
+
+#endif // SRC_IDENTITY_LIB_OAUTH_OAUTH_RESPONSE_H_
diff --git a/src/identity/lib/oauth/oauth_response_unittest.cc b/src/identity/lib/oauth/oauth_response_unittest.cc
new file mode 100644
index 0000000..7f1c4fb
--- /dev/null
+++ b/src/identity/lib/oauth/oauth_response_unittest.cc
@@ -0,0 +1,131 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/identity/lib/oauth/oauth_response.h"
+
+#include <stdlib.h>
+
+#include <string>
+
+#include "gtest/gtest.h"
+#include "lib/fsl/socket/strings.h"
+#include "peridot/lib/rapidjson/rapidjson.h"
+#include "src/lib/fxl/strings/string_number_conversions.h"
+
+namespace auth_providers {
+namespace oauth {
+
+using fuchsia::auth::AuthProviderStatus;
+
+namespace {
+
+namespace http = ::fuchsia::net::oldhttp;
+
+http::URLResponse FakeError(int32_t code, std::string reason) {
+ http::URLResponse response;
+ response.error = http::HttpError::New();
+ response.error->code = code;
+ response.error->description = reason;
+ return response;
+}
+
+http::URLResponse FakeSuccess(int32_t code, const std::string& body) {
+ http::URLResponse response;
+ response.body = http::URLBody::New();
+ response.body->set_stream(fsl::WriteStringToSocket(body));
+ response.status_code = code;
+
+ http::HttpHeader content_length_header;
+ content_length_header.name = "content-length";
+ content_length_header.value = fxl::NumberToString(body.size());
+ response.headers.push_back(std::move(content_length_header));
+
+ return response;
+}
+
+void VerifyOAuthResponse(OAuthResponse got, OAuthResponse want) {
+ EXPECT_EQ(want.status, got.status);
+ EXPECT_TRUE(got.error_description.find(want.error_description) !=
+ std::string::npos);
+
+ if (want.json_response.IsObject() && got.json_response.IsObject()) {
+ for (auto& m : want.json_response.GetObject()) {
+ auto key = m.name.GetString();
+ rapidjson::Value::ConstMemberIterator itr =
+ got.json_response.FindMember(key);
+ EXPECT_TRUE(itr != got.json_response.MemberEnd());
+
+ if (m.value.IsString())
+ EXPECT_EQ(got.json_response[key].GetString(), itr->value.GetString());
+ if (m.value.IsUint64())
+ EXPECT_EQ(got.json_response[key].GetUint64(), itr->value.GetUint64());
+ if (m.value.IsInt())
+ EXPECT_EQ(got.json_response[key].GetInt(), itr->value.GetInt());
+ if (m.value.IsInt64())
+ EXPECT_EQ(got.json_response[key].GetInt64(), itr->value.GetInt64());
+ if (m.value.IsBool())
+ EXPECT_EQ(got.json_response[key].GetBool(), itr->value.GetBool());
+ if (m.value.IsArray()) {
+ for (uint i = 0; i < want.json_response[key].GetArray().Size(); i++) {
+ EXPECT_EQ(want.json_response[key].GetArray()[i],
+ got.json_response[key].GetArray()[i]);
+ }
+ }
+ }
+ }
+}
+
+} // namespace
+
+class OAuthResponseTest : public ::testing::Test {
+ protected:
+ OAuthResponseTest() {}
+
+ ~OAuthResponseTest() override {}
+};
+
+TEST_F(OAuthResponseTest, CheckParseOAuthResponse) {
+ rapidjson::Document no_json_body;
+ VerifyOAuthResponse(ParseOAuthResponse(FakeError(-2, "Bad request")),
+ OAuthResponse(AuthProviderStatus::NETWORK_ERROR,
+ "Bad request", std::move(no_json_body)));
+
+ rapidjson::Document json_200;
+ json_200.Parse("{\"token\":\"xyz\"}");
+ VerifyOAuthResponse(
+ ParseOAuthResponse(
+ FakeSuccess(200, modular::JsonValueToPrettyString(json_200))),
+ OAuthResponse(AuthProviderStatus::OK, "", std::move(json_200)));
+
+ rapidjson::Document json_400;
+ json_400.Parse("{\"error\":\"invalid_grant\"}");
+ VerifyOAuthResponse(ParseOAuthResponse(FakeSuccess(
+ 400, modular::JsonValueToPrettyString(json_400))),
+ OAuthResponse(AuthProviderStatus::REAUTH_REQUIRED, "400",
+ std::move(json_400)));
+
+ rapidjson::Document json_400_br;
+ json_400_br.Parse("{\"error\":\"invalid_argument\"}");
+ VerifyOAuthResponse(ParseOAuthResponse(FakeSuccess(
+ 400, modular::JsonValueToPrettyString(json_400_br))),
+ OAuthResponse(AuthProviderStatus::OAUTH_SERVER_ERROR,
+ "400", std::move(json_400_br)));
+
+ rapidjson::Document json_401;
+ json_401.Parse("{\"error\":\"invalid_client\"}");
+ VerifyOAuthResponse(ParseOAuthResponse(FakeSuccess(
+ 401, modular::JsonValueToPrettyString(json_401))),
+ OAuthResponse(AuthProviderStatus::OAUTH_SERVER_ERROR,
+ "401", std::move(json_401)));
+
+ rapidjson::Document json_403;
+ json_403.Parse("{\"error\":\"access_denied\"}");
+ VerifyOAuthResponse(ParseOAuthResponse(FakeSuccess(
+ 403, modular::JsonValueToPrettyString(json_403))),
+ OAuthResponse(AuthProviderStatus::OAUTH_SERVER_ERROR,
+ "403", std::move(json_403)));
+}
+
+} // namespace oauth
+} // namespace auth_providers