| // 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. |
| |
| #include "peridot/lib/firebase_auth/testing/service_account_token_provider.h" |
| |
| #include <openssl/bio.h> |
| #include <openssl/digest.h> |
| #include <openssl/hmac.h> |
| #include <openssl/pem.h> |
| #include <rapidjson/document.h> |
| #include <rapidjson/schema.h> |
| #include <rapidjson/stringbuffer.h> |
| #include <rapidjson/writer.h> |
| #include <time.h> |
| |
| #include "lib/fidl/cpp/clone.h" |
| #include "lib/fidl/cpp/optional.h" |
| #include "lib/fsl/vmo/strings.h" |
| #include "lib/fxl/arraysize.h" |
| #include "lib/fxl/files/file.h" |
| #include "lib/fxl/logging.h" |
| #include "lib/fxl/strings/string_number_conversions.h" |
| #include "lib/fxl/strings/string_view.h" |
| #include "peridot/lib/base64url/base64url.h" |
| #include "peridot/lib/convert/convert.h" |
| |
| namespace service_account { |
| |
| namespace { |
| const char kServiceAccountConfigurationSchema[] = R"({ |
| "type": "object", |
| "additionalProperties": true, |
| "properties": { |
| "private_key": { |
| "type": "string" |
| }, |
| "client_email": { |
| "type": "string" |
| }, |
| "client_id": { |
| "type": "string" |
| } |
| }, |
| "required": ["private_key", "client_email", "client_id"] |
| })"; |
| |
| const char kIdentityResponseSchema[] = R"({ |
| "type": "object", |
| "additionalProperties": true, |
| "properties": { |
| "idToken": { |
| "type": "string" |
| }, |
| "expiresIn": { |
| "type": "string" |
| } |
| }, |
| "required": ["idToken", "expiresIn"] |
| })"; |
| |
| std::string GetHeader() { |
| rapidjson::StringBuffer string_buffer; |
| rapidjson::Writer<rapidjson::StringBuffer> writer(string_buffer); |
| |
| writer.StartObject(); |
| |
| writer.Key("typ"); |
| writer.String("JWT"); |
| |
| writer.Key("alg"); |
| writer.String("RS256"); |
| |
| writer.EndObject(); |
| |
| return base64url::Base64UrlEncode( |
| fxl::StringView(string_buffer.GetString(), string_buffer.GetSize())); |
| } |
| |
| modular_auth::AuthErr GetError(modular_auth::Status status, |
| std::string message) { |
| modular_auth::AuthErr error; |
| error.status = status; |
| error.message = message; |
| return error; |
| } |
| |
| std::unique_ptr<rapidjson::SchemaDocument> InitSchema(const char schemaSpec[]) { |
| rapidjson::Document schema_document; |
| if (schema_document.Parse(schemaSpec).HasParseError()) { |
| FXL_DCHECK(false) << "Schema validation spec itself is not valid JSON."; |
| } |
| return std::make_unique<rapidjson::SchemaDocument>(schema_document); |
| } |
| |
| bool ValidateSchema(const rapidjson::Value& value, |
| const rapidjson::SchemaDocument& schema) { |
| rapidjson::SchemaValidator validator(schema); |
| if (!value.Accept(validator)) { |
| rapidjson::StringBuffer uri_buffer; |
| validator.GetInvalidSchemaPointer().StringifyUriFragment(uri_buffer); |
| FXL_LOG(ERROR) << "Incorrect schema at " << uri_buffer.GetString() |
| << " , schema violation: " |
| << validator.GetInvalidSchemaKeyword(); |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| struct ServiceAccountTokenProvider::Credentials { |
| std::string client_email; |
| std::string client_id; |
| |
| bssl::UniquePtr<EVP_PKEY> private_key; |
| |
| std::unique_ptr<rapidjson::SchemaDocument> response_schema; |
| }; |
| |
| struct ServiceAccountTokenProvider::CachedToken { |
| std::string id_token; |
| time_t expiration_time; |
| }; |
| |
| ServiceAccountTokenProvider::ServiceAccountTokenProvider( |
| network_wrapper::NetworkWrapper* network_wrapper, |
| std::string user_id) |
| : network_wrapper_(network_wrapper), user_id_(std::move(user_id)) {} |
| |
| ServiceAccountTokenProvider::~ServiceAccountTokenProvider() { |
| for (const auto& pair : in_progress_callbacks_) { |
| ResolveCallbacks( |
| pair.first, nullptr, |
| GetError(modular_auth::Status::INTERNAL_ERROR, |
| "Account provider deleted with requests in flight.")); |
| } |
| } |
| |
| bool ServiceAccountTokenProvider::LoadCredentials( |
| const std::string& json_file) { |
| std::string file_content; |
| if (!files::ReadFileToString(json_file, &file_content)) { |
| FXL_LOG(ERROR) << "Unable to read file at: " << json_file; |
| return false; |
| } |
| |
| rapidjson::Document document; |
| document.Parse(file_content.c_str(), file_content.size()); |
| if (document.HasParseError() || !document.IsObject()) { |
| FXL_LOG(ERROR) << "Json file is incorrect at: " << json_file; |
| return false; |
| } |
| |
| auto service_account_schema = InitSchema(kServiceAccountConfigurationSchema); |
| if (!ValidateSchema(document, *service_account_schema)) { |
| return false; |
| } |
| |
| credentials_ = std::make_unique<Credentials>(); |
| credentials_->client_email = document["client_email"].GetString(); |
| credentials_->client_id = document["client_id"].GetString(); |
| |
| bssl::UniquePtr<BIO> bio( |
| BIO_new_mem_buf(document["private_key"].GetString(), |
| document["private_key"].GetStringLength())); |
| credentials_->private_key.reset( |
| PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr)); |
| |
| if (EVP_PKEY_id(credentials_->private_key.get()) != EVP_PKEY_RSA) { |
| FXL_LOG(ERROR) << "Provided key is not a RSA key."; |
| return false; |
| } |
| |
| credentials_->response_schema = InitSchema(kIdentityResponseSchema); |
| |
| return true; |
| } |
| |
| void ServiceAccountTokenProvider::GetAccessToken( |
| GetAccessTokenCallback callback) { |
| FXL_NOTIMPLEMENTED(); |
| callback(nullptr, |
| GetError(modular_auth::Status::INTERNAL_ERROR, "Not implemented.")); |
| } |
| |
| void ServiceAccountTokenProvider::GetIdToken(GetIdTokenCallback callback) { |
| FXL_NOTIMPLEMENTED(); |
| callback(nullptr, |
| GetError(modular_auth::Status::INTERNAL_ERROR, "Not implemented.")); |
| } |
| |
| void ServiceAccountTokenProvider::GetFirebaseAuthToken( |
| fidl::StringPtr firebase_api_key, |
| GetFirebaseAuthTokenCallback callback) { |
| // A request is in progress to get a token. Registers the callback that will |
| // be called when the request ends. |
| if (!in_progress_callbacks_[firebase_api_key].empty()) { |
| in_progress_callbacks_[firebase_api_key].push_back(callback); |
| return; |
| } |
| |
| // Check if a token is currently cached. |
| if (cached_tokens_[firebase_api_key]) { |
| auto& cached_token = cached_tokens_[firebase_api_key]; |
| if (time(nullptr) < cached_token->expiration_time) { |
| callback(GetFirebaseToken(cached_token->id_token), |
| GetError(modular_auth::Status::OK, "OK")); |
| return; |
| } |
| |
| // The token expired. Falls back to fetch a new one. |
| cached_tokens_.erase(firebase_api_key); |
| } |
| |
| // Build the custom token to exchange for an id token. |
| std::string custom_token; |
| if (!GetCustomToken(&custom_token)) { |
| callback(GetFirebaseToken(nullptr), |
| GetError(modular_auth::Status::INTERNAL_ERROR, |
| "Unable to compute custom authentication token.")); |
| } |
| |
| in_progress_callbacks_[firebase_api_key].push_back(callback); |
| |
| in_progress_requests_.emplace(network_wrapper_->Request( |
| [this, firebase_api_key = firebase_api_key.get(), |
| custom_token = std::move(custom_token)] { |
| return GetIdentityRequest(firebase_api_key, custom_token); |
| }, |
| [this, firebase_api_key = |
| firebase_api_key.get()](network::URLResponse response) { |
| HandleIdentityResponse(firebase_api_key, std::move(response)); |
| })); |
| } |
| |
| void ServiceAccountTokenProvider::GetClientId(GetClientIdCallback callback) { |
| callback(credentials_->client_id); |
| } |
| |
| std::string ServiceAccountTokenProvider::GetClaims() { |
| rapidjson::StringBuffer string_buffer; |
| rapidjson::Writer<rapidjson::StringBuffer> writer(string_buffer); |
| |
| writer.StartObject(); |
| |
| writer.Key("iss"); |
| writer.String(credentials_->client_email); |
| |
| writer.Key("sub"); |
| writer.String(credentials_->client_email); |
| |
| writer.Key("aud"); |
| writer.String( |
| "https://identitytoolkit.googleapis.com/" |
| "google.identity.identitytoolkit.v1.IdentityToolkit"); |
| |
| time_t current_time = time(nullptr); |
| // current_time = 1500471342; |
| writer.Key("iat"); |
| writer.Int(current_time); |
| |
| writer.Key("exp"); |
| writer.Int(current_time + 3600); |
| |
| writer.Key("uid"); |
| writer.String(user_id_); |
| |
| writer.EndObject(); |
| |
| return base64url::Base64UrlEncode( |
| fxl::StringView(string_buffer.GetString(), string_buffer.GetSize())); |
| } |
| |
| bool ServiceAccountTokenProvider::GetCustomToken(std::string* custom_token) { |
| std::string message = GetHeader() + "." + GetClaims(); |
| |
| bssl::ScopedEVP_MD_CTX md_ctx; |
| if (EVP_DigestSignInit(md_ctx.get(), nullptr, EVP_sha256(), nullptr, |
| credentials_->private_key.get()) != 1) { |
| FXL_LOG(ERROR) << ERR_reason_error_string(ERR_get_error()); |
| return false; |
| } |
| |
| if (EVP_DigestSignUpdate(md_ctx.get(), message.c_str(), message.size()) != |
| 1) { |
| FXL_LOG(ERROR) << ERR_reason_error_string(ERR_get_error()); |
| return false; |
| } |
| |
| size_t result_length; |
| if (EVP_DigestSignFinal(md_ctx.get(), nullptr, &result_length) != 1) { |
| FXL_LOG(ERROR) << ERR_reason_error_string(ERR_get_error()); |
| return false; |
| } |
| |
| char result[result_length]; |
| if (EVP_DigestSignFinal(md_ctx.get(), reinterpret_cast<uint8_t*>(result), |
| &result_length) != 1) { |
| FXL_LOG(ERROR) << ERR_reason_error_string(ERR_get_error()); |
| return false; |
| } |
| |
| std::string signature = |
| base64url::Base64UrlEncode(fxl::StringView(result, result_length)); |
| |
| *custom_token = message + "." + signature; |
| return true; |
| } |
| |
| modular_auth::FirebaseTokenPtr ServiceAccountTokenProvider::GetFirebaseToken( |
| const std::string& id_token) { |
| auto token = modular_auth::FirebaseToken::New(); |
| token->id_token = id_token; |
| token->local_id = user_id_; |
| token->email = user_id_ + "@example.com"; |
| return token; |
| } |
| |
| network::URLRequest ServiceAccountTokenProvider::GetIdentityRequest( |
| const std::string& api_key, |
| const std::string& custom_token) { |
| network::URLRequest request; |
| request.url = |
| "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" |
| "verifyCustomToken?key=" + |
| api_key; |
| request.method = "POST"; |
| request.auto_follow_redirects = true; |
| request.response_body_mode = network::ResponseBodyMode::SIZED_BUFFER; |
| |
| // content-type header. |
| network::HttpHeader content_type_header; |
| content_type_header.name = "content-type"; |
| content_type_header.value = "application/json"; |
| request.headers.push_back(std::move(content_type_header)); |
| |
| // set accept header |
| network::HttpHeader accept_header; |
| accept_header.name = "accept"; |
| accept_header.value = "application/json"; |
| request.headers.push_back(std::move(accept_header)); |
| |
| fsl::SizedVmo data; |
| bool result = fsl::VmoFromString(GetIdentityRequestBody(custom_token), &data); |
| FXL_DCHECK(result); |
| |
| request.body = network::URLBody::New(); |
| request.body->set_sized_buffer(std::move(data).ToTransport()); |
| |
| return request; |
| } |
| |
| std::string ServiceAccountTokenProvider::GetIdentityRequestBody( |
| const std::string& custom_token) { |
| rapidjson::StringBuffer string_buffer; |
| rapidjson::Writer<rapidjson::StringBuffer> writer(string_buffer); |
| |
| writer.StartObject(); |
| |
| writer.Key("token"); |
| writer.String(custom_token); |
| |
| writer.Key("returnSecureToken"); |
| writer.Bool(true); |
| |
| writer.EndObject(); |
| |
| return std::string(string_buffer.GetString(), string_buffer.GetSize()); |
| } |
| |
| void ServiceAccountTokenProvider::HandleIdentityResponse( |
| const std::string& api_key, |
| network::URLResponse response) { |
| if (response.error) { |
| ResolveCallbacks(api_key, nullptr, |
| GetError(modular_auth::Status::NETWORK_ERROR, |
| response.error->description)); |
| return; |
| } |
| |
| std::string response_body; |
| if (response.body) { |
| FXL_DCHECK(response.body->is_sized_buffer()); |
| if (!fsl::StringFromVmo(response.body->sized_buffer(), &response_body)) { |
| ResolveCallbacks(api_key, nullptr, |
| GetError(modular_auth::Status::INTERNAL_ERROR, |
| "Unable to read from VMO.")); |
| return; |
| } |
| } |
| |
| if (response.status_code != 200) { |
| ResolveCallbacks( |
| api_key, nullptr, |
| GetError(modular_auth::Status::OAUTH_SERVER_ERROR, response_body)); |
| return; |
| } |
| |
| rapidjson::Document document; |
| document.Parse(response_body.c_str(), response_body.size()); |
| if (document.HasParseError() || !document.IsObject()) { |
| ResolveCallbacks(api_key, nullptr, |
| GetError(modular_auth::Status::BAD_RESPONSE, |
| "Unable to parse response: " + response_body)); |
| return; |
| } |
| |
| if (!ValidateSchema(document, *credentials_->response_schema)) { |
| ResolveCallbacks(api_key, nullptr, |
| GetError(modular_auth::Status::BAD_RESPONSE, |
| "Malformed response: " + response_body)); |
| return; |
| } |
| |
| auto cached_token = std::make_unique<CachedToken>(); |
| cached_token->id_token = convert::ToString(document["idToken"]); |
| cached_token->expiration_time = |
| time(nullptr) + (9u * |
| fxl::StringToNumber<time_t>( |
| convert::ToStringView(document["expiresIn"])) / |
| 10u); |
| const auto& id_token = cached_token->id_token; |
| cached_tokens_[api_key] = std::move(cached_token); |
| ResolveCallbacks(api_key, GetFirebaseToken(id_token), |
| GetError(modular_auth::Status::OK, "OK")); |
| } |
| |
| void ServiceAccountTokenProvider::ResolveCallbacks( |
| const std::string& api_key, |
| modular_auth::FirebaseTokenPtr token, |
| modular_auth::AuthErr error) { |
| auto callbacks = std::move(in_progress_callbacks_[api_key]); |
| in_progress_callbacks_[api_key].clear(); |
| for (const auto& callback : callbacks) { |
| callback(fidl::Clone(token), fidl::Clone(error)); |
| } |
| } |
| |
| } // namespace service_account |