| // Copyright 2017 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. |
| |
| // OAuthTokenManagerApp is a simple auth service hack for fetching user OAuth |
| // tokens to talk programmatically to backend apis. These apis are hosted or |
| // integrated with Identity providers such as Google, Twitter, Spotify etc. |
| |
| #include <iomanip> |
| #include <iostream> |
| #include <map> |
| #include <memory> |
| #include <utility> |
| |
| #include <fuchsia/modular/auth/cpp/fidl.h> |
| #include <fuchsia/net/oldhttp/cpp/fidl.h> |
| #include <fuchsia/ui/viewsv1/cpp/fidl.h> |
| #include <fuchsia/webview/cpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <trace-provider/provider.h> |
| |
| #include "lib/async/cpp/operation.h" |
| #include "lib/component/cpp/connect.h" |
| #include "lib/component/cpp/startup_context.h" |
| #include "lib/fidl/cpp/interface_request.h" |
| #include "lib/fidl/cpp/optional.h" |
| #include "lib/fidl/cpp/string.h" |
| #include "lib/fsl/socket/strings.h" |
| #include "lib/fsl/vmo/strings.h" |
| #include "lib/fxl/command_line.h" |
| #include "lib/fxl/files/directory.h" |
| #include "lib/fxl/files/file.h" |
| #include "lib/fxl/files/path.h" |
| #include "lib/fxl/functional/make_copyable.h" |
| #include "lib/fxl/log_settings_command_line.h" |
| #include "lib/fxl/macros.h" |
| #include "lib/fxl/strings/join_strings.h" |
| #include "lib/fxl/strings/string_number_conversions.h" |
| #include "lib/fxl/time/time_point.h" |
| #include "lib/svc/cpp/services.h" |
| #include "rapidjson/document.h" |
| #include "rapidjson/error/en.h" |
| #include "rapidjson/pointer.h" |
| #include "rapidjson/prettywriter.h" |
| #include "rapidjson/stringbuffer.h" |
| #include "rapidjson/writer.h" |
| #include "topaz/examples/oauth_token_manager/credentials_generated.h" |
| |
| namespace { |
| |
| namespace http = ::fuchsia::net::oldhttp; |
| |
| using ShortLivedTokenCallback = |
| fit::function<void(std::string, fuchsia::modular::auth::AuthErr)>; |
| |
| using FirebaseTokenCallback = fit::function<void( |
| fuchsia::modular::auth::FirebaseTokenPtr, fuchsia::modular::auth::AuthErr)>; |
| |
| namespace { |
| |
| // TODO(alhaad/ukode): Move the following to a configuration file. |
| // NOTE: We are currently using a single client-id in Fuchsia. This is temporary |
| // and will change in the future. |
| constexpr char kClientId[] = |
| "934259141868-rejmm4ollj1bs7th1vg2ur6antpbug79.apps.googleusercontent.com"; |
| constexpr char kGoogleOAuthAuthEndpoint[] = |
| "https://accounts.google.com/o/oauth2/v2/auth"; |
| constexpr char kGoogleOAuthGlifParam[] = "false"; |
| constexpr char kGoogleOAuthTokenEndpoint[] = |
| "https://www.googleapis.com/oauth2/v4/token"; |
| constexpr char kGoogleRevokeTokenEndpoint[] = |
| "https://accounts.google.com/o/oauth2/revoke"; |
| constexpr char kGooglePeopleGetEndpoint[] = |
| "https://www.googleapis.com/plus/v1/people/me"; |
| constexpr char kFirebaseAuthEndpoint[] = |
| "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" |
| "verifyAssertion"; |
| constexpr char kRedirectUri[] = "com.google.fuchsia.auth:/oauth2redirect"; |
| constexpr char kCredentialsFile[] = "/data/v2/creds.db"; |
| constexpr char kWebViewUrl[] = "web_view"; |
| |
| constexpr auto kScopes = { |
| "openid", |
| "email", |
| "https://www.googleapis.com/auth/admin.directory.user.readonly", |
| "https://www.googleapis.com/auth/assistant", |
| "https://www.googleapis.com/auth/userinfo.email", |
| "https://www.googleapis.com/auth/userinfo.profile", |
| "https://www.googleapis.com/auth/youtube.readonly", |
| "https://www.googleapis.com/auth/contacts", |
| "https://www.googleapis.com/auth/drive.file", |
| "https://www.googleapis.com/auth/plus.login", |
| "https://www.googleapis.com/auth/calendar.readonly"}; |
| |
| // Type of token requested. |
| enum TokenType { |
| ACCESS_TOKEN = 0, |
| ID_TOKEN = 1, |
| FIREBASE_JWT_TOKEN = 2, |
| }; |
| |
| // Adjusts the token expiration window by a small amount to proactively refresh |
| // tokens before the expiry time limit has reached. |
| const uint64_t kPaddingForTokenExpiryInS = 600; |
| |
| template <typename T> |
| inline std::string JsonValueToPrettyString(const T& v) { |
| rapidjson::StringBuffer buffer; |
| rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer); |
| v.Accept(writer); |
| return buffer.GetString(); |
| } |
| |
| // TODO(alhaad/ukode): Don't use a hand-rolled version of this. |
| 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(); |
| } |
| |
| // 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::modular::auth::Status 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::modular::auth::Status::USER_CANCELLED; |
| } |
| if (url.find(success_prefix) != 0) { |
| // The authentication process is still ongoing. |
| return fuchsia::modular::auth::Status::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); |
| |
| // Note: The full auth stack normalizes the code here since the GLIF endpoint |
| // URL-encodes the slash prefix. We omit this normalization since our endpoint |
| // doesn't have that behavior. |
| |
| if (code.empty()) { |
| return fuchsia::modular::auth::Status::OAUTH_SERVER_ERROR; |
| } else { |
| auth_code = code; |
| return fuchsia::modular::auth::Status::OK; |
| } |
| } |
| |
| // Read the contents of |kCredentialsFile| into the supplied buffer, validates |
| // that these contents are a valid credentials file, and then returns a |
| // |::auth::CredentialStore| pointer to the contents. |
| const ::auth::CredentialStore* ParseCredsFile(std::string* buffer) { |
| if (!files::IsFile(kCredentialsFile)) { |
| return nullptr; |
| } |
| |
| if (!files::ReadFileToString(kCredentialsFile, buffer)) { |
| FXL_LOG(WARNING) << "Unable to read user credential file at: " |
| << kCredentialsFile; |
| return nullptr; |
| } |
| |
| flatbuffers::Verifier verifier( |
| reinterpret_cast<const unsigned char*>(buffer->data()), buffer->size()); |
| if (!::auth::VerifyCredentialStoreBuffer(verifier)) { |
| FXL_LOG(WARNING) << "Unable to verify credentials buffer"; |
| return nullptr; |
| } |
| |
| return ::auth::GetCredentialStore(buffer->data()); |
| } |
| |
| // Serializes |::auth::CredentialStore| to the |kCredentialsFIle| on disk. |
| bool WriteCredsFile(const std::string& serialized_creds) { |
| // verify file before saving |
| flatbuffers::Verifier verifier( |
| reinterpret_cast<const unsigned char*>(serialized_creds.data()), |
| serialized_creds.size()); |
| if (!::auth::VerifyCredentialStoreBuffer(verifier)) { |
| FXL_LOG(ERROR) << "Unable to verify credentials buffer:" |
| << serialized_creds.data(); |
| return false; |
| } |
| |
| if (!files::CreateDirectory(files::GetDirectoryName(kCredentialsFile))) { |
| FXL_LOG(ERROR) << "Unable to create directory for " << kCredentialsFile; |
| return false; |
| } |
| |
| if (!files::WriteFile(kCredentialsFile, serialized_creds.data(), |
| serialized_creds.size())) { |
| FXL_LOG(ERROR) << "Unable to write file " << kCredentialsFile; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Fetch user's refresh token from local credential store. In case of errors |
| // or account not found, an empty token is returned. |
| std::string GetRefreshTokenFromCredsFile(const std::string& account_id) { |
| if (account_id.empty()) { |
| FXL_LOG(ERROR) << "Account id is empty."; |
| return ""; |
| } |
| |
| std::string file_buffer; |
| const ::auth::CredentialStore* credentials_storage = |
| ParseCredsFile(&file_buffer); |
| if (credentials_storage == nullptr) { |
| FXL_LOG(ERROR) << "Failed to parse credentials."; |
| return ""; |
| } |
| |
| for (const auto* credential : *credentials_storage->creds()) { |
| if (credential->account_id()->str() == account_id) { |
| for (const auto* token : *credential->tokens()) { |
| switch (token->identity_provider()) { |
| case ::auth::IdentityProvider_GOOGLE: |
| return token->refresh_token()->str(); |
| default: |
| FXL_LOG(WARNING) << "Unrecognized IdentityProvider" |
| << token->identity_provider(); |
| } |
| } |
| } |
| } |
| return ""; |
| } |
| |
| // Exactly one of success_callback and failure_callback is ever invoked. |
| void Post(const std::string& request_body, http::URLLoader* const url_loader, |
| const std::string& url, const std::function<void()>& success_callback, |
| const std::function<void(fuchsia::modular::auth::Status, |
| std::string)>& failure_callback, |
| const std::function<bool(rapidjson::Document)>& set_token_callback) { |
| std::string encoded_request_body(request_body); |
| if (url.find(kFirebaseAuthEndpoint) == std::string::npos) { |
| encoded_request_body = UrlEncode(request_body); |
| } |
| |
| fsl::SizedVmo data; |
| auto result = fsl::VmoFromString(encoded_request_body, &data); |
| FXL_VLOG(1) << "Post Data:" << encoded_request_body; |
| FXL_DCHECK(result); |
| |
| http::URLRequest request; |
| request.url = url; |
| request.method = "POST"; |
| request.auto_follow_redirects = true; |
| |
| // Content-length header. |
| http::HttpHeader content_length_header; |
| content_length_header.name = "Content-length"; |
| uint64_t data_size = encoded_request_body.length(); |
| content_length_header.value = fxl::NumberToString(data_size); |
| request.headers.push_back(std::move(content_length_header)); |
| |
| // content-type header. |
| http::HttpHeader content_type_header; |
| content_type_header.name = "content-type"; |
| if (url.find("identitytoolkit") != std::string::npos) { |
| // set accept header |
| http::HttpHeader accept_header; |
| accept_header.name = "accept"; |
| accept_header.value = "application/json"; |
| request.headers.push_back(std::move(accept_header)); |
| |
| // set content_type header |
| content_type_header.value = "application/json"; |
| } else { |
| content_type_header.value = "application/x-www-form-urlencoded"; |
| } |
| request.headers.push_back(std::move(content_type_header)); |
| |
| request.body = http::URLBody::New(); |
| request.body->set_buffer(std::move(data).ToTransport()); |
| |
| url_loader->Start(std::move(request), [success_callback, failure_callback, |
| set_token_callback]( |
| http::URLResponse response) { |
| FXL_VLOG(1) << "URL Loader response:" |
| << std::to_string(response.status_code); |
| |
| if (response.error) { |
| failure_callback( |
| fuchsia::modular::auth::Status::NETWORK_ERROR, |
| "POST error: " + std::to_string(response.error->code) + |
| " , with description: " + response.error->description->data()); |
| return; |
| } |
| |
| std::string response_body; |
| if (response.body) { |
| FXL_DCHECK(response.body->is_stream()); |
| // TODO(alhaad/ukode): Use non-blocking variant. |
| if (!fsl::BlockingCopyToString(std::move(response.body->stream()), |
| &response_body)) { |
| failure_callback(fuchsia::modular::auth::Status::NETWORK_ERROR, |
| "Failed to read response from socket with status:" + |
| std::to_string(response.status_code)); |
| return; |
| } |
| } |
| |
| if (response.status_code != 200) { |
| failure_callback( |
| fuchsia::modular::auth::Status::OAUTH_SERVER_ERROR, |
| "Received status code:" + std::to_string(response.status_code) + |
| ", and response body:" + response_body); |
| return; |
| } |
| |
| rapidjson::Document doc; |
| rapidjson::ParseResult ok = doc.Parse(response_body); |
| if (!ok) { |
| std::string error_msg = GetParseError_En(ok.Code()); |
| failure_callback(fuchsia::modular::auth::Status::BAD_RESPONSE, |
| "JSON parse error: " + error_msg); |
| return; |
| }; |
| auto result = set_token_callback(std::move(doc)); |
| if (result) { |
| success_callback(); |
| } else { |
| failure_callback(fuchsia::modular::auth::Status::BAD_RESPONSE, |
| "Invalid response: " + JsonValueToPrettyString(doc)); |
| } |
| return; |
| }); |
| } |
| |
| // Exactly one of success_callback and failure_callback is ever invoked. |
| void Get(http::URLLoader* const url_loader, const std::string& url, |
| const std::string& access_token, |
| const std::function<void()>& success_callback, |
| const std::function<void(fuchsia::modular::auth::Status status, |
| std::string)>& failure_callback, |
| const std::function<bool(rapidjson::Document)>& set_token_callback) { |
| http::URLRequest request; |
| request.url = url; |
| request.method = "GET"; |
| request.auto_follow_redirects = true; |
| |
| // Set Authorization header. |
| http::HttpHeader auth_header; |
| auth_header.name = "Authorization"; |
| auth_header.value = "Bearer " + access_token; |
| request.headers.push_back(std::move(auth_header)); |
| |
| // set content-type header to json. |
| http::HttpHeader content_type_header; |
| content_type_header.name = "content-type"; |
| content_type_header.value = "application/json"; |
| |
| // set accept header to json |
| http::HttpHeader accept_header; |
| accept_header.name = "accept"; |
| accept_header.value = "application/json"; |
| request.headers.push_back(std::move(accept_header)); |
| |
| url_loader->Start(std::move(request), [success_callback, failure_callback, |
| set_token_callback]( |
| http::URLResponse response) { |
| if (response.error) { |
| failure_callback( |
| fuchsia::modular::auth::Status::NETWORK_ERROR, |
| "GET error: " + std::to_string(response.error->code) + |
| " ,with description: " + response.error->description->data()); |
| return; |
| } |
| |
| std::string response_body; |
| if (response.body) { |
| FXL_DCHECK(response.body->is_stream()); |
| // TODO(alhaad/ukode): Use non-blocking variant. |
| if (!fsl::BlockingCopyToString(std::move(response.body->stream()), |
| &response_body)) { |
| failure_callback(fuchsia::modular::auth::Status::NETWORK_ERROR, |
| "Failed to read response from socket with status:" + |
| std::to_string(response.status_code)); |
| return; |
| } |
| } |
| |
| if (response.status_code != 200) { |
| failure_callback( |
| fuchsia::modular::auth::Status::OAUTH_SERVER_ERROR, |
| "Status code: " + std::to_string(response.status_code) + |
| " while fetching tokens with error description:" + response_body); |
| return; |
| } |
| |
| rapidjson::Document doc; |
| rapidjson::ParseResult ok = doc.Parse(response_body); |
| if (!ok) { |
| std::string error_msg = GetParseError_En(ok.Code()); |
| failure_callback(fuchsia::modular::auth::Status::BAD_RESPONSE, |
| "JSON parse error: " + error_msg); |
| return; |
| }; |
| auto result = set_token_callback(std::move(doc)); |
| if (result) { |
| success_callback(); |
| } else { |
| failure_callback(fuchsia::modular::auth::Status::BAD_RESPONSE, |
| "Invalid response: " + JsonValueToPrettyString(doc)); |
| } |
| }); |
| } |
| |
| } // namespace |
| |
| // Implementation of the OAuth Token Manager app. |
| class OAuthTokenManagerApp : fuchsia::modular::auth::AccountProvider { |
| public: |
| OAuthTokenManagerApp(async::Loop* loop); |
| |
| private: |
| // |AccountProvider| |
| void Initialize( |
| fidl::InterfaceHandle<fuchsia::modular::auth::AccountProviderContext> |
| provider) override; |
| |
| // |AccountProvider| |
| void Terminate() override; |
| |
| // |AccountProvider| |
| void AddAccount(fuchsia::modular::auth::IdentityProvider identity_provider, |
| AddAccountCallback callback) override; |
| |
| // |AccountProvider| |
| void RemoveAccount(fuchsia::modular::auth::Account account, bool revoke_all, |
| RemoveAccountCallback callback) override; |
| |
| // |AccountProvider| |
| void GetTokenProviderFactory( |
| fidl::StringPtr account_id, |
| fidl::InterfaceRequest<fuchsia::modular::auth::TokenProviderFactory> |
| request) override; |
| |
| // Generate a random account id. |
| std::string GenerateAccountId(); |
| |
| // Refresh access and id tokens. |
| void RefreshToken(const std::string& account_id, const TokenType& token_type, |
| ShortLivedTokenCallback callback); |
| |
| // Refresh firebase tokens. |
| void RefreshFirebaseToken(const std::string& account_id, |
| const std::string& firebase_api_key, |
| const std::string& id_token, |
| FirebaseTokenCallback callback); |
| async::Loop* const loop_; |
| |
| std::shared_ptr<component::StartupContext> startup_context_; |
| |
| fuchsia::modular::auth::AccountProviderContextPtr account_provider_context_; |
| |
| fidl::Binding<fuchsia::modular::auth::AccountProvider> binding_; |
| |
| class TokenProviderFactoryImpl; |
| // account_id -> TokenProviderFactoryImpl |
| std::unordered_map<std::string, std::unique_ptr<TokenProviderFactoryImpl>> |
| token_provider_factory_impls_; |
| |
| // In-memory cache for short lived firebase auth id tokens. These tokens get |
| // reset on system reboots. Tokens are cached based on the expiration time |
| // set by the Firebase servers. Cache is indexed by firebase api keys. |
| struct FirebaseAuthToken { |
| uint64_t creation_ts; |
| uint64_t expires_in; |
| std::string id_token; |
| std::string local_id; |
| std::string email; |
| }; |
| |
| // In-memory cache for short lived oauth tokens that resets on system reboots. |
| // Tokens are cached based on the expiration time set by the Identity |
| // provider. Cache is indexed by unique account_ids. |
| struct ShortLivedToken { |
| uint64_t creation_ts; |
| uint64_t expires_in; |
| std::string access_token; |
| std::string id_token; |
| std::map<std::string, FirebaseAuthToken> fb_tokens_; |
| }; |
| std::map<std::string, ShortLivedToken> oauth_tokens_; |
| |
| // We are using operations here not to guard state across asynchronous calls |
| // but rather to clean up state after an 'operation' is done. |
| // TODO(ukode): All operations are running in a queue now which is |
| // inefficient because we block on operations that could be done in parallel. |
| // Instead we may want to create an operation for what |
| // TokenProviderFactoryImpl::GetFirebaseAuthToken() is doing in an sub |
| // operation queue. |
| modular::OperationQueue operation_queue_; |
| |
| class GoogleFirebaseTokensCall; |
| class GoogleOAuthTokensCall; |
| class GoogleUserCredsCall; |
| class GoogleRevokeTokensCall; |
| class GoogleProfileAttributesCall; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(OAuthTokenManagerApp); |
| }; |
| |
| class OAuthTokenManagerApp::TokenProviderFactoryImpl |
| : fuchsia::modular::auth::TokenProviderFactory, |
| fuchsia::modular::auth::TokenProvider { |
| public: |
| TokenProviderFactoryImpl( |
| const fidl::StringPtr& account_id, OAuthTokenManagerApp* const app, |
| fidl::InterfaceRequest<fuchsia::modular::auth::TokenProviderFactory> |
| request) |
| : account_id_(account_id), binding_(this, std::move(request)), app_(app) { |
| binding_.set_error_handler([this](zx_status_t status) { |
| app_->token_provider_factory_impls_.erase(account_id_); |
| }); |
| } |
| |
| private: |
| // |TokenProviderFactory| |
| void GetTokenProvider( |
| fidl::StringPtr /*application_url*/, |
| fidl::InterfaceRequest<fuchsia::modular::auth::TokenProvider> request) |
| override { |
| // TODO(alhaad/ukode): Current implementation is agnostic about which |
| // agent is requesting what token. Fix this. |
| token_provider_bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| // |TokenProvider| |
| void GetAccessToken(GetAccessTokenCallback callback) override { |
| FXL_DCHECK(app_); |
| app_->RefreshToken(account_id_, ACCESS_TOKEN, std::move(callback)); |
| } |
| |
| // |TokenProvider| |
| void GetIdToken(GetIdTokenCallback callback) override { |
| FXL_DCHECK(app_); |
| app_->RefreshToken(account_id_, ID_TOKEN, std::move(callback)); |
| } |
| |
| // |TokenProvider| |
| void GetFirebaseAuthToken(fidl::StringPtr firebase_api_key, |
| GetFirebaseAuthTokenCallback callback) override { |
| FXL_DCHECK(app_); |
| |
| // Oauth id token is used as input to fetch firebase auth token. |
| GetIdToken([this, firebase_api_key = firebase_api_key, |
| callback = std::move(callback)]( |
| const std::string id_token, |
| const fuchsia::modular::auth::AuthErr auth_err) mutable { |
| if (auth_err.status != fuchsia::modular::auth::Status::OK) { |
| FXL_LOG(ERROR) << "Error in refreshing Idtoken."; |
| callback(nullptr, std::move(auth_err)); |
| return; |
| } |
| |
| app_->RefreshFirebaseToken(account_id_, firebase_api_key, id_token, |
| std::move(callback)); |
| }); |
| } |
| |
| // |TokenProvider| |
| void GetClientId(GetClientIdCallback callback) override { |
| callback(kClientId); |
| } |
| |
| std::string account_id_; |
| fidl::Binding<fuchsia::modular::auth::TokenProviderFactory> binding_; |
| fidl::BindingSet<fuchsia::modular::auth::TokenProvider> |
| token_provider_bindings_; |
| |
| OAuthTokenManagerApp* const app_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(TokenProviderFactoryImpl); |
| }; |
| |
| class OAuthTokenManagerApp::GoogleFirebaseTokensCall |
| : public modular::Operation<fuchsia::modular::auth::FirebaseTokenPtr, |
| fuchsia::modular::auth::AuthErr> { |
| public: |
| GoogleFirebaseTokensCall(std::string account_id, std::string firebase_api_key, |
| std::string id_token, |
| OAuthTokenManagerApp* const app, |
| FirebaseTokenCallback callback) |
| : Operation("OAuthTokenManagerApp::GoogleFirebaseTokensCall", |
| fxl::MakeCopyable(std::move(callback))), |
| account_id_(std::move(account_id)), |
| firebase_api_key_(std::move(firebase_api_key)), |
| id_token_(std::move(id_token)), |
| app_(app) {} |
| |
| private: |
| void Run() override { |
| FlowToken flow{this, &firebase_token_, &auth_err_}; |
| |
| if (account_id_.empty()) { |
| Failure(flow, fuchsia::modular::auth::Status::BAD_REQUEST, |
| "Account id is empty"); |
| return; |
| } |
| |
| if (firebase_api_key_.empty()) { |
| Failure(flow, fuchsia::modular::auth::Status::BAD_REQUEST, |
| "Firebase Api key is empty"); |
| return; |
| } |
| |
| if (id_token_.empty()) { |
| // TODO(ukode): Need to differentiate between deleted users, users that |
| // are not provisioned and Guest mode users. For now, return empty |
| // response in such cases as there is no clear way to differentiate |
| // between regular users and guest users. |
| Success(flow); |
| return; |
| } |
| |
| // check cache for existing firebase tokens. |
| bool cacheValid = IsCacheValid(); |
| if (!cacheValid) { |
| FetchFirebaseToken(flow); |
| } else { |
| Success(flow); |
| } |
| } |
| |
| // Fetch fresh firebase auth token by exchanging idToken from Google. |
| void FetchFirebaseToken(FlowToken flow) { |
| FXL_DCHECK(!id_token_.empty()); |
| FXL_DCHECK(!firebase_api_key_.empty()); |
| |
| // JSON post request body |
| const std::string json_request_body = |
| R"({ "postBody": "id_token=)" + id_token_ + |
| "&providerId=google.com\"," + " \"returnIdpCredential\": true," + |
| " \"returnSecureToken\": true," + |
| R"( "requestUri": "http://localhost")" + "}"; |
| |
| app_->startup_context_->ConnectToEnvironmentService( |
| http_service_.NewRequest()); |
| http_service_->CreateURLLoader(url_loader_.NewRequest()); |
| |
| std::string url(kFirebaseAuthEndpoint); |
| url += "?key=" + UrlEncode(firebase_api_key_); |
| |
| // This flow branches below, so we need to put it in a shared |
| // container from which it can be removed once for all branches. |
| FlowTokenHolder branch{flow}; |
| |
| Post(json_request_body, url_loader_.get(), url, |
| [this, branch] { |
| std::unique_ptr<FlowToken> flow = branch.Continue(); |
| FXL_CHECK(flow); |
| Success(*flow); |
| }, |
| [this, branch](const fuchsia::modular::auth::Status status, |
| const std::string error_message) { |
| std::unique_ptr<FlowToken> flow = branch.Continue(); |
| FXL_CHECK(flow); |
| Failure(*flow, status, error_message); |
| }, |
| [this](rapidjson::Document doc) { |
| return GetFirebaseToken(std::move(doc)); |
| }); |
| } |
| |
| // Parses firebase jwt auth token from firebase auth endpoint response and |
| // saves it to local token in-memory cache. |
| bool GetFirebaseToken(rapidjson::Document jwt_token) { |
| FXL_VLOG(1) << "Firebase Token: " << JsonValueToPrettyString(jwt_token); |
| |
| if (!jwt_token.HasMember("idToken") || !jwt_token.HasMember("localId") || |
| !jwt_token.HasMember("email") || !jwt_token.HasMember("expiresIn")) { |
| FXL_LOG(ERROR) |
| << "Firebase Token returned from server is missing " |
| << "either idToken or email or localId fields. Returned token: " |
| << JsonValueToPrettyString(jwt_token); |
| return false; |
| } |
| |
| uint64_t expiresIn; |
| std::istringstream(jwt_token["expiresIn"].GetString()) >> expiresIn; |
| |
| app_->oauth_tokens_[account_id_].fb_tokens_[firebase_api_key_] = { |
| static_cast<uint64_t>(fxl::TimePoint::Now().ToEpochDelta().ToSeconds()), |
| expiresIn, |
| jwt_token["idToken"].GetString(), |
| jwt_token["localId"].GetString(), |
| jwt_token["email"].GetString(), |
| }; |
| return true; |
| } |
| |
| // Returns true if the firebase tokens stored in cache are still valid and |
| // not expired. |
| bool IsCacheValid() { |
| FXL_DCHECK(app_); |
| FXL_DCHECK(!account_id_.empty()); |
| FXL_DCHECK(!firebase_api_key_.empty()); |
| |
| if (app_->oauth_tokens_[account_id_].fb_tokens_.find(firebase_api_key_) == |
| app_->oauth_tokens_[account_id_].fb_tokens_.end()) { |
| FXL_VLOG(1) << "Firebase api key: [" << firebase_api_key_ |
| << "] not found in cache."; |
| return false; |
| } |
| |
| uint64_t current_ts = fxl::TimePoint::Now().ToEpochDelta().ToSeconds(); |
| auto fb_token = |
| app_->oauth_tokens_[account_id_].fb_tokens_[firebase_api_key_]; |
| uint64_t creation_ts = fb_token.creation_ts; |
| uint64_t token_expiry = fb_token.expires_in; |
| if ((current_ts - creation_ts) < |
| (token_expiry - kPaddingForTokenExpiryInS)) { |
| FXL_VLOG(1) << "Returning firebase token for api key [" |
| << firebase_api_key_ << "] from cache. "; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void Success(FlowToken /*flow*/) { |
| // Set firebase token |
| firebase_token_ = fuchsia::modular::auth::FirebaseToken::New(); |
| if (id_token_.empty()) { |
| firebase_token_->id_token = ""; |
| firebase_token_->local_id = ""; |
| firebase_token_->email = ""; |
| } else { |
| auto fb_token = |
| app_->oauth_tokens_[account_id_].fb_tokens_[firebase_api_key_]; |
| firebase_token_->id_token = fb_token.id_token; |
| firebase_token_->local_id = fb_token.local_id; |
| firebase_token_->email = fb_token.email; |
| } |
| |
| // Set status to success |
| auth_err_.status = fuchsia::modular::auth::Status::OK; |
| auth_err_.message = ""; |
| } |
| |
| void Failure(FlowToken /*flow*/, const fuchsia::modular::auth::Status& status, |
| const std::string& error_message) { |
| FXL_LOG(ERROR) << "Failed with error status:" << fidl::ToUnderlying(status) |
| << " ,and message:" << error_message; |
| auth_err_.status = status; |
| auth_err_.message = error_message; |
| } |
| |
| const std::string account_id_; |
| const std::string firebase_api_key_; |
| const std::string id_token_; |
| OAuthTokenManagerApp* const app_; |
| |
| fuchsia::modular::auth::FirebaseTokenPtr firebase_token_; |
| fuchsia::modular::auth::AuthErr auth_err_; |
| |
| http::HttpServicePtr http_service_; |
| http::URLLoaderPtr url_loader_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(GoogleFirebaseTokensCall); |
| }; |
| |
| class OAuthTokenManagerApp::GoogleOAuthTokensCall |
| : public modular::Operation<fidl::StringPtr, |
| fuchsia::modular::auth::AuthErr> { |
| public: |
| GoogleOAuthTokensCall(std::string account_id, const TokenType& token_type, |
| OAuthTokenManagerApp* const app, |
| ShortLivedTokenCallback callback) |
| : Operation("OAuthTokenManagerApp::GoogleOAuthTokensCall", |
| fxl::MakeCopyable(std::move(callback))), |
| account_id_(std::move(account_id)), |
| token_type_(token_type), |
| app_(app) {} |
| |
| private: |
| void Run() override { |
| FlowToken flow{this, &result_, &auth_err_}; |
| |
| if (account_id_.empty()) { |
| Failure(flow, fuchsia::modular::auth::Status::BAD_REQUEST, |
| "Account id is empty."); |
| return; |
| } |
| |
| FXL_VLOG(1) << "Fetching access/id tokens for Account_ID:" << account_id_; |
| |
| // Use an entry from the cache if one exists |
| bool cacheValid = IsCacheValid(); |
| if (cacheValid) { |
| Success(flow); // fetching tokens from local cache. |
| return; |
| } |
| |
| // Check if the user has a stored refesh token |
| const std::string refresh_token = GetRefreshTokenFromCredsFile(account_id_); |
| if (refresh_token.empty()) { |
| // TODO(ukode): Need to differentiate between deleted users, users that |
| // are not provisioned and Guest mode users. For now, return empty |
| // response in such cases as there is no clear way to differentiate |
| // between regular users and guest users. |
| Success(flow); |
| } else { |
| // Use this refresh token to generate and cache new short lived tokens. |
| FetchAccessAndIdToken(refresh_token, flow); |
| } |
| } |
| |
| // Fetch fresh access and id tokens by exchanging refresh token from Google |
| // token endpoint. |
| void FetchAccessAndIdToken(const std::string& refresh_token, FlowToken flow) { |
| FXL_CHECK(!refresh_token.empty()); |
| |
| const std::string request_body = "refresh_token=" + refresh_token + |
| "&client_id=" + kClientId + |
| "&grant_type=refresh_token"; |
| |
| app_->startup_context_->ConnectToEnvironmentService( |
| http_service_.NewRequest()); |
| http_service_->CreateURLLoader(url_loader_.NewRequest()); |
| |
| // This flow exlusively branches below, so we need to put it in a shared |
| // container from which it can be removed once for all branches. |
| FlowTokenHolder branch{flow}; |
| |
| Post(request_body, url_loader_.get(), kGoogleOAuthTokenEndpoint, |
| [this, branch] { |
| std::unique_ptr<FlowToken> flow = branch.Continue(); |
| FXL_CHECK(flow); |
| Success(*flow); |
| }, |
| [this, branch](const fuchsia::modular::auth::Status status, |
| const std::string error_message) { |
| std::unique_ptr<FlowToken> flow = branch.Continue(); |
| FXL_CHECK(flow); |
| Failure(*flow, status, error_message); |
| }, |
| [this](rapidjson::Document doc) { |
| return GetShortLivedTokens(std::move(doc)); |
| }); |
| } |
| |
| // Parse access and id tokens from OAUth endpoints into local token in-memory |
| // cache. |
| bool GetShortLivedTokens(rapidjson::Document tokens) { |
| if (!tokens.HasMember("access_token")) { |
| FXL_LOG(ERROR) << "Tokens returned from server does not contain " |
| << "access_token. Returned token: " |
| << JsonValueToPrettyString(tokens); |
| return false; |
| }; |
| |
| if ((token_type_ == ID_TOKEN) && !tokens.HasMember("id_token")) { |
| FXL_LOG(ERROR) << "Tokens returned from server does not contain " |
| << "id_token. Returned token: " |
| << JsonValueToPrettyString(tokens); |
| return false; |
| } |
| |
| // Add the token generation timestamp to |tokens| for caching. |
| uint64_t creation_ts = fxl::TimePoint::Now().ToEpochDelta().ToSeconds(); |
| app_->oauth_tokens_[account_id_] = { |
| creation_ts, |
| tokens["expires_in"].GetUint64(), |
| tokens["access_token"].GetString(), |
| tokens["id_token"].GetString(), |
| std::map<std::string, FirebaseAuthToken>(), |
| }; |
| |
| return true; |
| } |
| |
| // Returns true if the access and idtokens stored in cache are still valid and |
| // not expired. |
| bool IsCacheValid() { |
| FXL_DCHECK(app_); |
| FXL_DCHECK(!account_id_.empty()); |
| |
| if (app_->oauth_tokens_.find(account_id_) == app_->oauth_tokens_.end()) { |
| FXL_VLOG(1) << "Account: [" << account_id_ << "] not found in cache."; |
| return false; |
| } |
| |
| uint64_t current_ts = fxl::TimePoint::Now().ToEpochDelta().ToSeconds(); |
| uint64_t creation_ts = app_->oauth_tokens_[account_id_].creation_ts; |
| uint64_t token_expiry = app_->oauth_tokens_[account_id_].expires_in; |
| if ((current_ts - creation_ts) < |
| (token_expiry - kPaddingForTokenExpiryInS)) { |
| FXL_VLOG(1) << "Returning access/id tokens for account [" << account_id_ |
| << "] from cache. "; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void Success(FlowToken flow) { |
| if (app_->oauth_tokens_.find(account_id_) == app_->oauth_tokens_.end()) { |
| // In guest mode, return empty tokens. |
| result_ = ""; |
| } else { |
| switch (token_type_) { |
| case ACCESS_TOKEN: |
| result_ = app_->oauth_tokens_[account_id_].access_token; |
| break; |
| case ID_TOKEN: |
| result_ = app_->oauth_tokens_[account_id_].id_token; |
| break; |
| case FIREBASE_JWT_TOKEN: |
| Failure(flow, fuchsia::modular::auth::Status::INTERNAL_ERROR, |
| "invalid token type"); |
| } |
| } |
| |
| // Set status to success |
| auth_err_.status = fuchsia::modular::auth::Status::OK; |
| auth_err_.message = ""; |
| } |
| |
| void Failure(FlowToken /*flow*/, const fuchsia::modular::auth::Status& status, |
| const std::string& error_message) { |
| FXL_LOG(ERROR) << "Failed with error status:" << fidl::ToUnderlying(status) |
| << " ,and message:" << error_message; |
| auth_err_.status = status; |
| auth_err_.message = error_message; |
| } |
| |
| const std::string account_id_; |
| const std::string firebase_api_key_; |
| TokenType token_type_; |
| OAuthTokenManagerApp* const app_; |
| |
| http::HttpServicePtr http_service_; |
| http::URLLoaderPtr url_loader_; |
| |
| fidl::StringPtr result_; |
| fuchsia::modular::auth::AuthErr auth_err_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(GoogleOAuthTokensCall); |
| }; |
| |
| // TODO(alhaad): Use variadic template in |Operation|. That way, parameters to |
| // |callback| can be returned as parameters to |Done()|. |
| class OAuthTokenManagerApp::GoogleUserCredsCall |
| : public modular::Operation<>, |
| fuchsia::webview::WebRequestDelegate { |
| public: |
| GoogleUserCredsCall(fuchsia::modular::auth::AccountPtr account, |
| OAuthTokenManagerApp* const app, |
| AddAccountCallback callback) |
| : Operation("OAuthTokenManagerApp::GoogleUserCredsCall", [] {}), |
| account_(std::move(account)), |
| app_(app), |
| callback_(std::move(callback)) {} |
| |
| private: |
| // |Operation| |
| void Run() override { |
| // No FlowToken used here; calling Done() directly is more suitable, |
| // because of the flow of control through |
| // fuchsia::webview::WebRequestDelegate. |
| |
| auto view_owner = SetupWebView(); |
| |
| // Set a delegate which will parse incoming URLs for authorization code. |
| // TODO(alhaad/ukode): We need to set a timout here in-case we do not get |
| // the code. |
| fuchsia::webview::WebRequestDelegatePtr web_request_delegate; |
| web_request_delegate_bindings_.AddBinding( |
| this, web_request_delegate.NewRequest()); |
| web_view_->SetWebRequestDelegate(std::move(web_request_delegate)); |
| |
| web_view_->ClearCookies(); |
| |
| const std::vector<std::string> scopes(kScopes.begin(), kScopes.end()); |
| std::string joined_scopes = fxl::JoinStrings(scopes, "+"); |
| |
| std::string url = kGoogleOAuthAuthEndpoint; |
| url += "?scope=" + joined_scopes; |
| url += "&response_type=code&redirect_uri="; |
| url += kRedirectUri; |
| url += "&glif="; |
| url += kGoogleOAuthGlifParam; |
| url += "&client_id="; |
| url += kClientId; |
| |
| web_view_->SetUrl(url); |
| |
| app_->account_provider_context_->GetAuthenticationContext( |
| account_->id, auth_context_.NewRequest()); |
| |
| auth_context_.set_error_handler([this](zx_status_t status) { |
| callback_(nullptr, "Overlay cancelled by base shell."); |
| Done(); |
| }); |
| auth_context_->StartOverlay(std::move(view_owner)); |
| } |
| |
| // |fuchsia::webview::WebRequestDelegate| |
| void WillSendRequest(fidl::StringPtr incoming_url) override { |
| std::string auth_code; |
| fuchsia::modular::auth::Status status = |
| ParseAuthCodeFromUrl(incoming_url.get(), auth_code); |
| |
| if (status != fuchsia::modular::auth::Status::OK) { |
| Failure(status, "User cancelled OAuth flow"); |
| } else if (auth_code.empty()) { |
| // Authentication is ongoing. |
| return; |
| } |
| |
| // User accepted OAuth permissions - close the webview and exchange auth |
| // code to long lived credential. |
| // Also, de-register previously registered error callbacks since calling |
| // StopOverlay() might cause this connection to be closed. |
| auth_context_.set_error_handler([](zx_status_t status) {}); |
| auth_context_->StopOverlay(); |
| |
| const std::string request_body = |
| "code=" + auth_code + "&redirect_uri=" + kRedirectUri + |
| "&client_id=" + kClientId + "&grant_type=authorization_code"; |
| |
| app_->startup_context_->ConnectToEnvironmentService( |
| http_service_.NewRequest()); |
| http_service_->CreateURLLoader(url_loader_.NewRequest()); |
| |
| Post(request_body, url_loader_.get(), kGoogleOAuthTokenEndpoint, |
| [this] { Success(); }, |
| [this](const fuchsia::modular::auth::Status status, |
| const std::string error_message) { |
| Failure(status, error_message); |
| }, |
| [this](rapidjson::Document doc) { |
| return ProcessCredentials(std::move(doc)); |
| }); |
| } |
| |
| // Parses refresh tokens from auth endpoint response and persists it in |
| // |kCredentialsFile|. |
| bool ProcessCredentials(rapidjson::Document tokens) { |
| if (!tokens.HasMember("refresh_token") || |
| !tokens.HasMember("access_token")) { |
| FXL_LOG(ERROR) << "Tokens returned from server does not contain " |
| << "refresh_token or access_token. Returned token: " |
| << JsonValueToPrettyString(tokens); |
| return false; |
| }; |
| |
| if (!SaveCredentials(tokens["refresh_token"].GetString())) { |
| return false; |
| } |
| |
| // Store short lived tokens local in-memory cache. |
| uint64_t creation_ts = fxl::TimePoint::Now().ToEpochDelta().ToSeconds(); |
| app_->oauth_tokens_[account_->id] = { |
| creation_ts, |
| tokens["expires_in"].GetUint64(), |
| tokens["access_token"].GetString(), |
| tokens["id_token"].GetString(), |
| std::map<std::string, FirebaseAuthToken>(), |
| }; |
| return true; |
| } |
| |
| // Saves new credentials to the persistent creds storage file. |
| bool SaveCredentials(const std::string& refresh_token) { |
| flatbuffers::FlatBufferBuilder builder; |
| std::vector<flatbuffers::Offset<::auth::UserCredential>> creds; |
| |
| std::string file_buffer; |
| const ::auth::CredentialStore* file_creds = ParseCredsFile(&file_buffer); |
| if (file_creds != nullptr) { |
| // Reserialize existing users. |
| for (const auto* cred : *file_creds->creds()) { |
| if (cred->account_id()->str() == account_->id) { |
| // Update existing credentials |
| continue; |
| } |
| |
| std::vector<flatbuffers::Offset<::auth::IdpCredential>> idp_creds; |
| for (const auto* idp_cred : *cred->tokens()) { |
| idp_creds.push_back(::auth::CreateIdpCredential( |
| builder, idp_cred->identity_provider(), |
| builder.CreateString(idp_cred->refresh_token()))); |
| } |
| |
| creds.push_back(::auth::CreateUserCredential( |
| builder, builder.CreateString(cred->account_id()), |
| builder.CreateVector<flatbuffers::Offset<::auth::IdpCredential>>( |
| idp_creds))); |
| } |
| } |
| |
| // add the new credential for |account_->id|. |
| std::vector<flatbuffers::Offset<::auth::IdpCredential>> new_idp_creds; |
| new_idp_creds.push_back( |
| ::auth::CreateIdpCredential(builder, ::auth::IdentityProvider_GOOGLE, |
| builder.CreateString(refresh_token))); |
| |
| creds.push_back(::auth::CreateUserCredential( |
| builder, builder.CreateString(account_->id), |
| builder.CreateVector<flatbuffers::Offset<::auth::IdpCredential>>( |
| new_idp_creds))); |
| |
| builder.Finish( |
| ::auth::CreateCredentialStore(builder, builder.CreateVector(creds))); |
| |
| std::string new_serialized_creds = std::string( |
| reinterpret_cast<const char*>(builder.GetCurrentBufferPointer()), |
| builder.GetSize()); |
| |
| return WriteCredsFile(new_serialized_creds); |
| } |
| |
| void Success() { |
| callback_(std::move(account_), nullptr); |
| Done(); |
| } |
| |
| void Failure(const fuchsia::modular::auth::Status& status, |
| const std::string& error_message) { |
| FXL_LOG(ERROR) << "Failed with error status:" << fidl::ToUnderlying(status) |
| << " ,and message:" << error_message; |
| callback_(nullptr, error_message); |
| auth_context_.set_error_handler([](zx_status_t status) {}); |
| auth_context_->StopOverlay(); |
| Done(); |
| } |
| |
| fuchsia::ui::viewsv1token::ViewOwnerPtr SetupWebView() { |
| component::Services web_view_services; |
| fuchsia::sys::LaunchInfo web_view_launch_info; |
| web_view_launch_info.url = kWebViewUrl; |
| web_view_launch_info.directory_request = web_view_services.NewRequest(); |
| app_->startup_context_->launcher()->CreateComponent( |
| std::move(web_view_launch_info), web_view_controller_.NewRequest()); |
| web_view_controller_.set_error_handler([this](zx_status_t status) { |
| FXL_CHECK(false) << "web_view not found at " << kWebViewUrl << "."; |
| }); |
| |
| fuchsia::ui::viewsv1token::ViewOwnerPtr view_owner; |
| fuchsia::ui::viewsv1::ViewProviderPtr view_provider; |
| web_view_services.ConnectToService(view_provider.NewRequest()); |
| fuchsia::sys::ServiceProviderPtr web_view_moz_services; |
| view_provider->CreateView(view_owner.NewRequest(), |
| web_view_moz_services.NewRequest()); |
| |
| component::ConnectToService(web_view_moz_services.get(), |
| web_view_.NewRequest()); |
| |
| return view_owner; |
| } |
| |
| fuchsia::modular::auth::AccountPtr account_; |
| OAuthTokenManagerApp* const app_; |
| const AddAccountCallback callback_; |
| |
| fuchsia::modular::auth::AuthenticationContextPtr auth_context_; |
| |
| fuchsia::webview::WebViewPtr web_view_; |
| fuchsia::sys::ComponentControllerPtr web_view_controller_; |
| |
| http::HttpServicePtr http_service_; |
| http::URLLoaderPtr url_loader_; |
| |
| fidl::BindingSet<fuchsia::webview::WebRequestDelegate> |
| web_request_delegate_bindings_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(GoogleUserCredsCall); |
| }; |
| |
| class OAuthTokenManagerApp::GoogleRevokeTokensCall |
| : public modular::Operation<fuchsia::modular::auth::AuthErr> { |
| public: |
| GoogleRevokeTokensCall(fuchsia::modular::auth::AccountPtr account, |
| bool revoke_all, OAuthTokenManagerApp* const app, |
| RemoveAccountCallback callback) |
| : Operation("OAuthTokenManagerApp::GoogleRevokeTokensCall", |
| fxl::MakeCopyable(callback.share())), |
| account_(std::move(account)), |
| revoke_all_(revoke_all), |
| app_(app), |
| callback_(callback.share()) {} |
| |
| private: |
| // |Operation| |
| void Run() override { |
| FlowToken flow{this, &auth_err_}; |
| |
| if (!account_) { |
| Failure(flow, fuchsia::modular::auth::Status::BAD_REQUEST, |
| "Account is null."); |
| return; |
| } |
| |
| switch (account_->identity_provider) { |
| case fuchsia::modular::auth::IdentityProvider::DEV: |
| Success(flow); // guest mode |
| return; |
| case fuchsia::modular::auth::IdentityProvider::GOOGLE: |
| break; |
| default: |
| Failure(flow, fuchsia::modular::auth::Status::BAD_REQUEST, |
| "Unsupported IDP."); |
| return; |
| } |
| |
| // If there is a cache entry always ensure its deleted |
| app_->oauth_tokens_.erase(account_->id); |
| |
| // If no credentials exist in the database we are now done. |
| const std::string refresh_token = |
| GetRefreshTokenFromCredsFile(account_->id); |
| if (refresh_token.empty()) { |
| FXL_LOG(ERROR) << "Account: " << account_->id << " not found."; |
| Success(flow); // Maybe a guest account. |
| return; |
| } |
| |
| // Delete user credentials from local persistent storage. |
| if (!DeleteCredentials()) { |
| Failure(flow, fuchsia::modular::auth::Status::INTERNAL_ERROR, |
| "Unable to delete persistent credentials for account:" + |
| std::string(account_->id)); |
| return; |
| } |
| |
| if (!revoke_all_) { |
| Success(flow); |
| return; |
| } |
| |
| // Revoke persistent tokens on backend IDP server. |
| app_->startup_context_->ConnectToEnvironmentService( |
| http_service_.NewRequest()); |
| http_service_->CreateURLLoader(url_loader_.NewRequest()); |
| |
| std::string url = kGoogleRevokeTokenEndpoint + std::string("?token="); |
| url += refresh_token; |
| |
| std::string request_body; |
| |
| // This flow branches below, so we need to put it in a shared container |
| // from which it can be removed once for all branches. |
| FlowTokenHolder branch{flow}; |
| |
| Post(request_body, url_loader_.get(), url, |
| [this, branch] { |
| std::unique_ptr<FlowToken> flow = branch.Continue(); |
| FXL_CHECK(flow); |
| Success(*flow); |
| }, |
| [this, branch](const fuchsia::modular::auth::Status status, |
| const std::string error_message) { |
| std::unique_ptr<FlowToken> flow = branch.Continue(); |
| FXL_CHECK(flow); |
| Failure(*flow, status, error_message); |
| }, |
| [this](rapidjson::Document doc) { |
| return RevokeAllTokens(std::move(doc)); |
| }); |
| } |
| |
| // Deletes existing user credentials for |account_->id|. |
| bool DeleteCredentials() { |
| std::string file_buffer; |
| const ::auth::CredentialStore* credentials_storage = |
| ParseCredsFile(&file_buffer); |
| if (credentials_storage == nullptr) { |
| FXL_LOG(ERROR) << "Failed to parse credentials."; |
| return false; |
| } |
| |
| // Delete |account_->id| credentials and reserialize existing users. |
| flatbuffers::FlatBufferBuilder builder; |
| std::vector<flatbuffers::Offset<::auth::UserCredential>> creds; |
| |
| for (const auto* cred : *credentials_storage->creds()) { |
| if (cred->account_id()->str() == account_->id) { |
| // Delete existing credentials |
| continue; |
| } |
| |
| std::vector<flatbuffers::Offset<::auth::IdpCredential>> idp_creds; |
| for (const auto* idp_cred : *cred->tokens()) { |
| idp_creds.push_back(::auth::CreateIdpCredential( |
| builder, idp_cred->identity_provider(), |
| builder.CreateString(idp_cred->refresh_token()))); |
| } |
| |
| creds.push_back(::auth::CreateUserCredential( |
| builder, builder.CreateString(cred->account_id()), |
| builder.CreateVector<flatbuffers::Offset<::auth::IdpCredential>>( |
| idp_creds))); |
| } |
| |
| builder.Finish( |
| ::auth::CreateCredentialStore(builder, builder.CreateVector(creds))); |
| |
| std::string new_serialized_creds = std::string( |
| reinterpret_cast<const char*>(builder.GetCurrentBufferPointer()), |
| builder.GetSize()); |
| |
| return WriteCredsFile(new_serialized_creds); |
| } |
| |
| // Invalidate both refresh and access tokens on backend IDP server. |
| // If the revocation is successfully processed, then the status code of the |
| // response is 200. For error conditions, a status code 400 is returned along |
| // with an error code in the response body. |
| bool RevokeAllTokens(rapidjson::Document status) { |
| FXL_VLOG(1) << "Revoke token api response: " |
| << JsonValueToPrettyString(status); |
| |
| return true; |
| } |
| |
| void Success(FlowToken /*flow*/) { |
| // Set status to success |
| auth_err_.status = fuchsia::modular::auth::Status::OK; |
| auth_err_.message = ""; |
| } |
| |
| void Failure(FlowToken /*flow*/, const fuchsia::modular::auth::Status& status, |
| const std::string& error_message) { |
| FXL_LOG(ERROR) << "Failed with error status:" << fidl::ToUnderlying(status) |
| << " ,and message:" << error_message; |
| auth_err_.status = status; |
| auth_err_.message = error_message; |
| } |
| |
| fuchsia::modular::auth::AccountPtr account_; |
| // By default, RemoveAccount deletes account only from the device where the |
| // user performed the operation. |
| bool revoke_all_ = false; |
| OAuthTokenManagerApp* const app_; |
| const RemoveAccountCallback callback_; |
| |
| http::HttpServicePtr http_service_; |
| http::URLLoaderPtr url_loader_; |
| |
| fuchsia::modular::auth::AuthErr auth_err_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(GoogleRevokeTokensCall); |
| }; |
| |
| class OAuthTokenManagerApp::GoogleProfileAttributesCall |
| : public modular::Operation<> { |
| public: |
| GoogleProfileAttributesCall(fuchsia::modular::auth::AccountPtr account, |
| OAuthTokenManagerApp* const app, |
| AddAccountCallback callback) |
| : Operation("OAuthTokenManagerApp::GoogleProfileAttributesCall", [] {}), |
| account_(std::move(account)), |
| app_(app), |
| callback_(std::move(callback)) {} |
| |
| private: |
| // |Operation| |
| void Run() override { |
| if (!account_) { |
| Failure(fuchsia::modular::auth::Status::BAD_REQUEST, "Account is null."); |
| return; |
| } |
| |
| if (app_->oauth_tokens_.find(account_->id) == app_->oauth_tokens_.end()) { |
| FXL_LOG(ERROR) << "Account: " << account_->id << " not found."; |
| Success(); // Maybe a guest account. |
| return; |
| } |
| |
| const std::string access_token = |
| app_->oauth_tokens_[account_->id].access_token; |
| app_->startup_context_->ConnectToEnvironmentService( |
| http_service_.NewRequest()); |
| http_service_->CreateURLLoader(url_loader_.NewRequest()); |
| |
| // Fetch profile atrributes for the provisioned user using |
| // https://developers.google.com/+/web/api/rest/latest/people/get api. |
| Get(url_loader_.get(), kGooglePeopleGetEndpoint, access_token, |
| [this] { Success(); }, |
| [this](const fuchsia::modular::auth::Status status, |
| const std::string error_message) { |
| Failure(status, error_message); |
| }, |
| [this](rapidjson::Document doc) { |
| return SetAccountAttributes(std::move(doc)); |
| }); |
| } |
| |
| // Populate profile urls and display name for the account. |
| bool SetAccountAttributes(rapidjson::Document attributes) { |
| FXL_VLOG(1) << "People:get api response: " |
| << JsonValueToPrettyString(attributes); |
| |
| if (!account_) { |
| return false; |
| } |
| |
| if (attributes.HasMember("id")) { |
| account_->profile_id = attributes["id"].GetString(); |
| } |
| if (account_->profile_id.is_null()) { |
| account_->profile_id = ""; |
| } |
| |
| if (attributes.HasMember("displayName")) { |
| account_->display_name = attributes["displayName"].GetString(); |
| } |
| if (account_->display_name.is_null()) { |
| account_->display_name = ""; |
| } |
| |
| if (attributes.HasMember("url")) { |
| account_->url = attributes["url"].GetString(); |
| } else { |
| account_->url = ""; |
| } |
| |
| if (attributes.HasMember("image")) { |
| account_->image_url = attributes["image"]["url"].GetString(); |
| } else { |
| account_->image_url = ""; |
| } |
| |
| return true; |
| } |
| |
| void Success() { |
| callback_(std::move(account_), nullptr); |
| Done(); |
| } |
| |
| void Failure(const fuchsia::modular::auth::Status& status, |
| const std::string& error_message) { |
| FXL_LOG(ERROR) << "Failed with error status:" << fidl::ToUnderlying(status) |
| << " ,and message:" << error_message; |
| |
| // Account is missing profile attributes, but still valid. |
| callback_(std::move(account_), error_message); |
| Done(); |
| } |
| |
| fuchsia::modular::auth::AccountPtr account_; |
| OAuthTokenManagerApp* const app_; |
| const AddAccountCallback callback_; |
| |
| http::HttpServicePtr http_service_; |
| http::URLLoaderPtr url_loader_; |
| |
| FXL_DISALLOW_COPY_AND_ASSIGN(GoogleProfileAttributesCall); |
| }; |
| |
| OAuthTokenManagerApp::OAuthTokenManagerApp(async::Loop* loop) |
| : loop_(loop), |
| startup_context_(component::StartupContext::CreateFromStartupInfo()), |
| binding_(this) { |
| startup_context_->outgoing().AddPublicService<AccountProvider>( |
| [this](fidl::InterfaceRequest<AccountProvider> request) { |
| binding_.Bind(std::move(request)); |
| }); |
| // Log an error if the existing credential file is invalid. |
| if (files::IsFile(kCredentialsFile)) { |
| std::string file_buffer; |
| if (ParseCredsFile(&file_buffer) == nullptr) { |
| FXL_LOG(WARNING) << "Error in parsing existing credentials from: " |
| << kCredentialsFile; |
| } |
| } |
| } |
| |
| void OAuthTokenManagerApp::Initialize( |
| fidl::InterfaceHandle<fuchsia::modular::auth::AccountProviderContext> |
| provider) { |
| FXL_VLOG(1) << "OAuthTokenManagerApp::Initialize()"; |
| account_provider_context_.Bind(std::move(provider)); |
| } |
| |
| void OAuthTokenManagerApp::Terminate() { |
| FXL_LOG(INFO) << "OAuthTokenManagerApp::Terminate()"; |
| loop_->Quit(); |
| } |
| |
| // TODO(alhaad): Check if account id already exists. |
| std::string OAuthTokenManagerApp::GenerateAccountId() { |
| uint32_t random_number = 0; |
| zx_cprng_draw(&random_number, sizeof(random_number)); |
| return std::to_string(random_number); |
| } |
| |
| void OAuthTokenManagerApp::AddAccount( |
| fuchsia::modular::auth::IdentityProvider identity_provider, |
| AddAccountCallback callback) { |
| FXL_VLOG(1) << "OAuthTokenManagerApp::AddAccount()"; |
| auto account = fuchsia::modular::auth::Account::New(); |
| account->id = GenerateAccountId(); |
| account->identity_provider = identity_provider; |
| account->profile_id = ""; |
| account->display_name = ""; |
| account->url = ""; |
| account->image_url = ""; |
| |
| switch (identity_provider) { |
| case fuchsia::modular::auth::IdentityProvider::DEV: |
| callback(std::move(account), nullptr); |
| return; |
| case fuchsia::modular::auth::IdentityProvider::GOOGLE: |
| operation_queue_.Add(new GoogleUserCredsCall( |
| std::move(account), this, |
| [this, callback = std::move(callback)]( |
| fuchsia::modular::auth::AccountPtr account, |
| const fidl::StringPtr error_msg) mutable { |
| if (error_msg) { |
| callback(nullptr, error_msg); |
| return; |
| } |
| |
| operation_queue_.Add(new GoogleProfileAttributesCall( |
| std::move(account), this, std::move(callback))); |
| })); |
| return; |
| default: |
| callback(nullptr, "Unrecognized Identity Provider"); |
| } |
| } |
| |
| void OAuthTokenManagerApp::RemoveAccount( |
| fuchsia::modular::auth::Account account, bool revoke_all, |
| RemoveAccountCallback callback) { |
| FXL_VLOG(1) << "OAuthTokenManagerApp::RemoveAccount()"; |
| operation_queue_.Add( |
| new GoogleRevokeTokensCall(fidl::MakeOptional(std::move(account)), |
| revoke_all, this, std::move(callback))); |
| } |
| |
| void OAuthTokenManagerApp::GetTokenProviderFactory( |
| fidl::StringPtr account_id, |
| fidl::InterfaceRequest<fuchsia::modular::auth::TokenProviderFactory> |
| request) { |
| new TokenProviderFactoryImpl(account_id, this, std::move(request)); |
| } |
| |
| void OAuthTokenManagerApp::RefreshToken(const std::string& account_id, |
| const TokenType& token_type, |
| ShortLivedTokenCallback callback) { |
| FXL_VLOG(1) << "OAuthTokenManagerApp::RefreshToken()"; |
| operation_queue_.Add(new GoogleOAuthTokensCall(account_id, token_type, this, |
| std::move(callback))); |
| } |
| |
| void OAuthTokenManagerApp::RefreshFirebaseToken( |
| const std::string& account_id, const std::string& firebase_api_key, |
| const std::string& id_token, FirebaseTokenCallback callback) { |
| FXL_VLOG(1) << "OAuthTokenManagerApp::RefreshFirebaseToken()"; |
| operation_queue_.Add(new GoogleFirebaseTokensCall( |
| account_id, firebase_api_key, id_token, this, std::move(callback))); |
| } |
| |
| } // namespace |
| |
| int main(int argc, const char** argv) { |
| auto command_line = fxl::CommandLineFromArgcArgv(argc, argv); |
| if (!fxl::SetLogSettingsFromCommandLine(command_line)) { |
| return 1; |
| } |
| |
| async::Loop loop(&kAsyncLoopConfigAttachToThread); |
| trace::TraceProvider trace_provider(loop.dispatcher()); |
| |
| OAuthTokenManagerApp app(&loop); |
| loop.Run(); |
| return 0; |
| } |