// // 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(CurlWrapper& curl) : curl(curl) { latest_credential = ""; expiration = std::chrono::steady_clock::now(); } std::string GceMetadataCredentialSource::Credential() { if (expiration - std::chrono::steady_clock::now() < REFRESH_WINDOW) { RefreshCredential(); } return latest_credential; } void GceMetadataCredentialSource::RefreshCredential() { auto curl_response = curl.DownloadToJson(REFRESH_URL, {"Metadata-Flavor: Google"}); const auto& json = curl_response.data; if (!curl_response.HttpSuccess()) { LOG(FATAL) << "Error fetching credentials. The server response was \"" << json << "\", and code was " << curl_response.http_code; } CHECK(!json.isMember("error")) << "Response had \"error\" but had http success status. Received \"" << json << "\""; bool has_access_token = json.isMember("access_token"); bool has_expires_in = json.isMember("expires_in"); if (!has_access_token || !has_expires_in) { LOG(FATAL) << "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(); } std::unique_ptr GceMetadataCredentialSource::make( CurlWrapper& curl) { return std::unique_ptr( new GceMetadataCredentialSource(curl)); } FixedCredentialSource::FixedCredentialSource(const std::string& credential) { this->credential = credential; } std::string FixedCredentialSource::Credential() { return credential; } std::unique_ptr FixedCredentialSource::make( const std::string& credential) { return std::unique_ptr(new FixedCredentialSource(credential)); } Result RefreshCredentialSource::FromOauth2ClientFile( CurlWrapper& curl, 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("token_response")); auto& token_response = credential["token_response"]; CF_EXPECT(token_response.type() == Json::ValueType::objectValue); CF_EXPECT(token_response.isMember("refresh_token")); auto& refresh_token = credential["refresh_token"]; CF_EXPECT(refresh_token.type() == Json::ValueType::stringValue); return RefreshCredentialSource(curl, client_id.asString(), client_secret.asString(), refresh_token.asString()); } RefreshCredentialSource::RefreshCredentialSource( CurlWrapper& curl, const std::string& client_id, const std::string& client_secret, const std::string& refresh_token) : curl_(curl), client_id_(client_id), client_secret_(client_secret), refresh_token_(refresh_token) {} std::string RefreshCredentialSource::Credential() { if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) { UpdateLatestCredential(); } return latest_credential_; } void RefreshCredentialSource::UpdateLatestCredential() { std::vector headers = { "Content-Type: application/x-www-form-urlencoded"}; std::stringstream data; data << "client_id=" << curl_.UrlEscape(client_id_) << "&"; data << "client_secret=" << curl_.UrlEscape(client_secret_) << "&"; data << "refresh_token=" << curl_.UrlEscape(refresh_token_) << "&"; data << "grant_type=refresh_token"; static constexpr char kUrl[] = "https://oauth2.googleapis.com/token"; auto response = curl_.PostToJson(kUrl, data.str(), headers); CHECK(response.HttpSuccess()) << response.data; auto& json = response.data; CHECK(!json.isMember("error")) << "Response had \"error\" but had http success status. Received \"" << json << "\""; bool has_access_token = json.isMember("access_token"); bool has_expires_in = json.isMember("expires_in"); CHECK(has_access_token && has_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(); } 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(CurlWrapper& curl, const Json::Value& json, const std::string& scope) { ServiceAccountOauthCredentialSource source(curl); 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( CurlWrapper& curl) : curl_(curl), private_key_(nullptr, EVP_PKEY_free) {} static std::string Base64Url(const char* data, std::size_t size) { std::string base64; CHECK(EncodeBase64(data, size, &base64)); base64 = android::base::StringReplace(base64, "+", "-", /* all */ true); base64 = android::base::StringReplace(base64, "/", "_", /* all */ true); return base64; } static std::string JsonToBase64Url(const Json::Value& json) { Json::StreamWriterBuilder factory; auto serialized = Json::writeString(factory, json); return Base64Url(serialized.c_str(), serialized.size()); } static std::string 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 = 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 = 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); CHECK(EVP_DigestSignInit(sign_ctx.get(), nullptr, EVP_sha256(), nullptr, private_key)); CHECK(EVP_DigestSignUpdate(sign_ctx.get(), jwt_to_sign.c_str(), jwt_to_sign.size())); size_t length; CHECK(EVP_DigestSignFinal(sign_ctx.get(), nullptr, &length)); std::vector sig_raw(length); CHECK(EVP_DigestSignFinal(sign_ctx.get(), sig_raw.data(), &length)); return jwt_to_sign + "." + Base64Url((const char*)sig_raw.data(), length); } void 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=" << curl_.UrlEscape(GRANT) << "&"; auto jwt = CreateJwt(email_, scope_, private_key_.get()); content << "assertion=" << curl_.UrlEscape(jwt); std::vector headers = { "Content-Type: application/x-www-form-urlencoded"}; auto curl_response = curl_.PostToJson(URL, content.str(), headers); if (!curl_response.HttpSuccess()) { LOG(FATAL) << "Error fetching credentials. The server response was \"" << curl_response.data << "\", and code was " << curl_response.http_code; } Json::Value json = curl_response.data; CHECK(!json.isMember("error")) << "Response had \"error\" but had http success status. Received \"" << json << "\""; bool has_access_token = json.isMember("access_token"); bool has_expires_in = json.isMember("expires_in"); if (!has_access_token || !has_expires_in) { LOG(FATAL) << "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(); } std::string ServiceAccountOauthCredentialSource::Credential() { if (expiration_ - std::chrono::steady_clock::now() < REFRESH_WINDOW) { RefreshCredential(); } return latest_credential_; } } // namespace cuttlefish