blob: c12c49449e5597bf3fa9c170a597556202ef9e2b [file] [log] [blame]
//
// 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 <android-base/logging.h>
#include <android-base/strings.h>
#include <json/json.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#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<CredentialSource> GceMetadataCredentialSource::make(
CurlWrapper& curl) {
return std::unique_ptr<CredentialSource>(
new GceMetadataCredentialSource(curl));
}
FixedCredentialSource::FixedCredentialSource(const std::string& credential) {
this->credential = credential;
}
std::string FixedCredentialSource::Credential() {
return credential;
}
std::unique_ptr<CredentialSource> FixedCredentialSource::make(
const std::string& credential) {
return std::unique_ptr<CredentialSource>(new FixedCredentialSource(credential));
}
Result<RefreshCredentialSource> RefreshCredentialSource::FromOauth2ClientFile(
CurlWrapper& curl, std::istream& stream) {
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> 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<std::string> 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>
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<BIO, int (*)(BIO*)> 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<seconds>(time.time_since_epoch()).count();
auto exp = time + minutes(30);
claim_set_json["exp"] =
(uint64_t)duration_cast<seconds>(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<EVP_MD_CTX, void (*)(EVP_MD_CTX*)> 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<uint8_t> 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<std::string> 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