// // Copyright (C) 2019 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "credential_source.h" #include #include #include #include #include #include #include "common/libs/utils/base64.h" namespace cuttlefish { namespace { std::chrono::steady_clock::duration REFRESH_WINDOW = std::chrono::minutes(2); std::string REFRESH_URL = "http://metadata.google.internal/computeMetadata/" "v1/instance/service-accounts/default/token"; } // namespace GceMetadataCredentialSource::GceMetadataCredentialSource( HttpClient& http_client) : http_client(http_client) { latest_credential = ""; expiration = std::chrono::steady_clock::now(); } Result GceMetadataCredentialSource::Credential() { if (expiration - std::chrono::steady_clock::now() < REFRESH_WINDOW) { CF_EXPECT(RefreshCredential()); } return latest_credential; } Result GceMetadataCredentialSource::RefreshCredential() { auto response = CF_EXPECT( http_client.DownloadToJson(REFRESH_URL, {"Metadata-Flavor: Google"})); const auto& json = response.data; CF_EXPECT(response.HttpSuccess(), "Error fetching credentials. The server response was \"" << json << "\", and code was " << response.http_code); CF_EXPECT(!json.isMember("error"), "Response had \"error\" but had http success status. Received \"" << json << "\""); CF_EXPECT(json.isMember("access_token") && json.isMember("expires_in"), "GCE credential was missing access_token or expires_in. " << "Full response was " << json << ""); expiration = std::chrono::steady_clock::now() + std::chrono::seconds(json["expires_in"].asInt()); latest_credential = json["access_token"].asString(); return {}; } std::unique_ptr GceMetadataCredentialSource::make( HttpClient& http_client) { return std::unique_ptr( new GceMetadataCredentialSource(http_client)); } FixedCredentialSource::FixedCredentialSource(const std::string& credential) { this->credential = credential; } Result FixedCredentialSource::Credential() { return credential; } std::unique_ptr FixedCredentialSource::make( const std::string& credential) { return std::unique_ptr(new FixedCredentialSource(credential)); } Result RefreshCredentialSource::FromOauth2ClientFile( HttpClient& http_client, std::istream& stream) { Json::CharReaderBuilder builder; std::unique_ptr reader(builder.newCharReader()); Json::Value json; std::string errorMessage; CF_EXPECT(Json::parseFromStream(builder, stream, &json, &errorMessage), "Failed to parse json: " << errorMessage); CF_EXPECT(json.isMember("data")); auto& data = json["data"]; CF_EXPECT(data.type() == Json::ValueType::arrayValue); CF_EXPECT(data.size() == 1); auto& data_first = data[0]; CF_EXPECT(data_first.type() == Json::ValueType::objectValue); CF_EXPECT(data_first.isMember("credential")); auto& credential = data_first["credential"]; CF_EXPECT(credential.type() == Json::ValueType::objectValue); CF_EXPECT(credential.isMember("client_id")); auto& client_id = credential["client_id"]; CF_EXPECT(client_id.type() == Json::ValueType::stringValue); CF_EXPECT(credential.isMember("client_secret")); auto& client_secret = credential["client_secret"]; CF_EXPECT(client_secret.type() == Json::ValueType::stringValue); CF_EXPECT(credential.isMember("refresh_token")); auto& refresh_token = credential["refresh_token"]; CF_EXPECT(refresh_token.type() == Json::ValueType::stringValue); return RefreshCredentialSource(http_client, client_id.asString(), client_secret.asString(), refresh_token.asString()); } RefreshCredentialSource::RefreshCredentialSource( HttpClient& http_client, const std::string& client_id, const std::string& client_secret, const std::string& refresh_token) : http_client_(http_client), client_id_(client_id), client_secret_(client_secret), refresh_token_(refresh_token) {} Result RefreshCredentialSource::Credential() { if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) { CF_EXPECT(UpdateLatestCredential()); } return latest_credential_; } Result RefreshCredentialSource::UpdateLatestCredential() { std::vector headers = { "Content-Type: application/x-www-form-urlencoded"}; std::stringstream data; data << "client_id=" << http_client_.UrlEscape(client_id_) << "&"; data << "client_secret=" << http_client_.UrlEscape(client_secret_) << "&"; data << "refresh_token=" << http_client_.UrlEscape(refresh_token_) << "&"; data << "grant_type=refresh_token"; static constexpr char kUrl[] = "https://oauth2.googleapis.com/token"; auto response = CF_EXPECT(http_client_.PostToJson(kUrl, data.str(), headers)); CF_EXPECT(response.HttpSuccess(), response.data); auto& json = response.data; CF_EXPECT(!json.isMember("error"), "Response had \"error\" but had http success status. Received \"" << json << "\""); CF_EXPECT(json.isMember("access_token") && json.isMember("expires_in"), "Refresh credential was missing access_token or expires_in." << " Full response was " << json << ""); expiration_ = std::chrono::steady_clock::now() + std::chrono::seconds(json["expires_in"].asInt()); latest_credential_ = json["access_token"].asString(); return {}; } static std::string CollectSslErrors() { std::stringstream errors; auto callback = [](const char* str, size_t len, void* stream) { ((std::stringstream*)stream)->write(str, len); return 1; // success }; ERR_print_errors_cb(callback, &errors); return errors.str(); } Result ServiceAccountOauthCredentialSource::FromJson(HttpClient& http_client, const Json::Value& json, const std::string& scope) { ServiceAccountOauthCredentialSource source(http_client); source.scope_ = scope; CF_EXPECT(json.isMember("client_email")); CF_EXPECT(json["client_email"].type() == Json::ValueType::stringValue); source.email_ = json["client_email"].asString(); CF_EXPECT(json.isMember("private_key")); CF_EXPECT(json["private_key"].type() == Json::ValueType::stringValue); std::string key_str = json["private_key"].asString(); std::unique_ptr bo(CF_EXPECT(BIO_new(BIO_s_mem())), BIO_free); CF_EXPECT(BIO_write(bo.get(), key_str.c_str(), key_str.size()) == key_str.size()); auto pkey = CF_EXPECT(PEM_read_bio_PrivateKey(bo.get(), nullptr, 0, 0), CollectSslErrors()); source.private_key_.reset(pkey); return source; } ServiceAccountOauthCredentialSource::ServiceAccountOauthCredentialSource( HttpClient& http_client) : http_client_(http_client), private_key_(nullptr, EVP_PKEY_free) {} static Result Base64Url(const char* data, std::size_t size) { std::string base64; CF_EXPECT(EncodeBase64(data, size, &base64)); base64 = android::base::StringReplace(base64, "+", "-", /* all */ true); base64 = android::base::StringReplace(base64, "/", "_", /* all */ true); return base64; } static Result JsonToBase64Url(const Json::Value& json) { Json::StreamWriterBuilder factory; auto serialized = Json::writeString(factory, json); return CF_EXPECT(Base64Url(serialized.c_str(), serialized.size())); } static Result CreateJwt(const std::string& email, const std::string& scope, EVP_PKEY* private_key) { using std::chrono::duration_cast; using std::chrono::minutes; using std::chrono::seconds; using std::chrono::system_clock; // https://developers.google.com/identity/protocols/oauth2/service-account Json::Value header_json; header_json["alg"] = "RS256"; header_json["typ"] = "JWT"; std::string header_str = CF_EXPECT(JsonToBase64Url(header_json)); Json::Value claim_set_json; claim_set_json["iss"] = email; claim_set_json["scope"] = scope; claim_set_json["aud"] = "https://oauth2.googleapis.com/token"; auto time = system_clock::now(); claim_set_json["iat"] = (uint64_t)duration_cast(time.time_since_epoch()).count(); auto exp = time + minutes(30); claim_set_json["exp"] = (uint64_t)duration_cast(exp.time_since_epoch()).count(); std::string claim_set_str = CF_EXPECT(JsonToBase64Url(claim_set_json)); std::string jwt_to_sign = header_str + "." + claim_set_str; std::unique_ptr sign_ctx( EVP_MD_CTX_create(), EVP_MD_CTX_free); CF_EXPECT(EVP_DigestSignInit(sign_ctx.get(), nullptr, EVP_sha256(), nullptr, private_key)); CF_EXPECT(EVP_DigestSignUpdate(sign_ctx.get(), jwt_to_sign.c_str(), jwt_to_sign.size())); size_t length; CF_EXPECT(EVP_DigestSignFinal(sign_ctx.get(), nullptr, &length)); std::vector sig_raw(length); CF_EXPECT(EVP_DigestSignFinal(sign_ctx.get(), sig_raw.data(), &length)); auto signature = CF_EXPECT(Base64Url((const char*)sig_raw.data(), length)); return jwt_to_sign + "." + signature; } Result ServiceAccountOauthCredentialSource::RefreshCredential() { static constexpr char URL[] = "https://oauth2.googleapis.com/token"; static constexpr char GRANT[] = "urn:ietf:params:oauth:grant-type:jwt-bearer"; std::stringstream content; content << "grant_type=" << http_client_.UrlEscape(GRANT) << "&"; auto jwt = CF_EXPECT(CreateJwt(email_, scope_, private_key_.get())); content << "assertion=" << http_client_.UrlEscape(jwt); std::vector headers = { "Content-Type: application/x-www-form-urlencoded"}; auto response = CF_EXPECT(http_client_.PostToJson(URL, content.str(), headers)); CF_EXPECT(response.HttpSuccess(), "Error fetching credentials. The server response was \"" << response.data << "\", and code was " << response.http_code); Json::Value json = response.data; CF_EXPECT(!json.isMember("error"), "Response had \"error\" but had http success status. Received \"" << json << "\""); CF_EXPECT(json.isMember("access_token") && json.isMember("expires_in"), "Service account credential was missing access_token or expires_in." << " Full response was " << json << ""); expiration_ = std::chrono::steady_clock::now() + std::chrono::seconds(json["expires_in"].asInt()); latest_credential_ = json["access_token"].asString(); return {}; } Result ServiceAccountOauthCredentialSource::Credential() { if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) { CF_EXPECT(RefreshCredential()); } return latest_credential_; } } // namespace cuttlefish