blob: f5d9442e3623a69a8bb815b4e10f11c3c0db94fc [file] [log] [blame]
//
// Copyright 2020 gRPC authors.
//
// 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 <grpc/support/port_platform.h>
#include "src/core/lib/security/credentials/external/external_account_credentials.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "src/core/lib/http/parser.h"
#include "src/core/lib/security/util/json_util.h"
#include "src/core/lib/slice/b64.h"
#include "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
#include "src/core/lib/security/credentials/external/file_external_account_credentials.h"
#include "src/core/lib/security/credentials/external/url_external_account_credentials.h"
#define EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE \
"urn:ietf:params:oauth:grant-type:token-exchange"
#define EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE \
"urn:ietf:params:oauth:token-type:access_token"
#define GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE \
"https://www.googleapis.com/auth/cloud-platform"
namespace grpc_core {
namespace {
std::string UrlEncode(const absl::string_view& s) {
const char* hex = "0123456789ABCDEF";
std::string result;
result.reserve(s.length());
for (auto c : s) {
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
result.push_back(c);
} else {
result.push_back('%');
result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
result.push_back(hex[static_cast<unsigned char>(c) & 15]);
}
}
return result;
}
} // namespace
RefCountedPtr<ExternalAccountCredentials> ExternalAccountCredentials::Create(
const Json& json, std::vector<std::string> scopes,
grpc_error_handle* error) {
GPR_ASSERT(*error == GRPC_ERROR_NONE);
Options options;
options.type = GRPC_AUTH_JSON_TYPE_INVALID;
if (json.type() != Json::Type::OBJECT) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Invalid json to construct credentials options.");
return nullptr;
}
auto it = json.object_value().find("type");
if (it == json.object_value().end()) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING("type field not present.");
return nullptr;
}
if (it->second.type() != Json::Type::STRING) {
*error =
GRPC_ERROR_CREATE_FROM_STATIC_STRING("type field must be a string.");
return nullptr;
}
if (it->second.string_value() != GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT) {
*error =
GRPC_ERROR_CREATE_FROM_STATIC_STRING("Invalid credentials json type.");
return nullptr;
}
options.type = GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT;
it = json.object_value().find("audience");
if (it == json.object_value().end()) {
*error =
GRPC_ERROR_CREATE_FROM_STATIC_STRING("audience field not present.");
return nullptr;
}
if (it->second.type() != Json::Type::STRING) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"audience field must be a string.");
return nullptr;
}
options.audience = it->second.string_value();
it = json.object_value().find("subject_token_type");
if (it == json.object_value().end()) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"subject_token_type field not present.");
return nullptr;
}
if (it->second.type() != Json::Type::STRING) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"subject_token_type field must be a string.");
return nullptr;
}
options.subject_token_type = it->second.string_value();
it = json.object_value().find("service_account_impersonation_url");
if (it != json.object_value().end()) {
options.service_account_impersonation_url = it->second.string_value();
}
it = json.object_value().find("token_url");
if (it == json.object_value().end()) {
*error =
GRPC_ERROR_CREATE_FROM_STATIC_STRING("token_url field not present.");
return nullptr;
}
if (it->second.type() != Json::Type::STRING) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"token_url field must be a string.");
return nullptr;
}
options.token_url = it->second.string_value();
it = json.object_value().find("token_info_url");
if (it != json.object_value().end()) {
options.token_info_url = it->second.string_value();
}
it = json.object_value().find("credential_source");
if (it == json.object_value().end()) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"credential_source field not present.");
return nullptr;
}
options.credential_source = it->second;
it = json.object_value().find("quota_project_id");
if (it != json.object_value().end()) {
options.quota_project_id = it->second.string_value();
}
it = json.object_value().find("client_id");
if (it != json.object_value().end()) {
options.client_id = it->second.string_value();
}
it = json.object_value().find("client_secret");
if (it != json.object_value().end()) {
options.client_secret = it->second.string_value();
}
RefCountedPtr<ExternalAccountCredentials> creds;
if (options.credential_source.object_value().find("environment_id") !=
options.credential_source.object_value().end()) {
creds = MakeRefCounted<AwsExternalAccountCredentials>(
std::move(options), std::move(scopes), error);
} else if (options.credential_source.object_value().find("file") !=
options.credential_source.object_value().end()) {
creds = MakeRefCounted<FileExternalAccountCredentials>(
std::move(options), std::move(scopes), error);
} else if (options.credential_source.object_value().find("url") !=
options.credential_source.object_value().end()) {
creds = MakeRefCounted<UrlExternalAccountCredentials>(
std::move(options), std::move(scopes), error);
} else {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Invalid options credential source to create "
"ExternalAccountCredentials.");
}
if (*error == GRPC_ERROR_NONE) {
return creds;
} else {
return nullptr;
}
}
ExternalAccountCredentials::ExternalAccountCredentials(
Options options, std::vector<std::string> scopes)
: options_(std::move(options)) {
if (scopes.empty()) {
scopes.push_back(GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE);
}
scopes_ = std::move(scopes);
}
ExternalAccountCredentials::~ExternalAccountCredentials() {}
std::string ExternalAccountCredentials::debug_string() {
return absl::StrFormat("ExternalAccountCredentials{Audience:%s,%s}",
options_.audience,
grpc_oauth2_token_fetcher_credentials::debug_string());
}
// The token fetching flow:
// 1. Retrieve subject token - Subclass's RetrieveSubjectToken() gets called
// and the subject token is received in OnRetrieveSubjectTokenInternal().
// 2. Exchange token - ExchangeToken() gets called with the
// subject token from #1. Receive the response in OnExchangeTokenInternal().
// 3. (Optional) Impersonate service account - ImpersenateServiceAccount() gets
// called with the access token of the response from #2. Get an impersonated
// access token in OnImpersenateServiceAccountInternal().
// 4. Finish token fetch - Return back the response that contains an access
// token in FinishTokenFetch().
// TODO(chuanr): Avoid starting the remaining requests if the channel gets shut
// down.
void ExternalAccountCredentials::fetch_oauth2(
grpc_credentials_metadata_request* metadata_req,
grpc_httpcli_context* httpcli_context, grpc_polling_entity* pollent,
grpc_iomgr_cb_func response_cb, grpc_millis deadline) {
GPR_ASSERT(ctx_ == nullptr);
ctx_ = new HTTPRequestContext(httpcli_context, pollent, deadline);
metadata_req_ = metadata_req;
response_cb_ = response_cb;
auto cb = [this](std::string token, grpc_error_handle error) {
OnRetrieveSubjectTokenInternal(token, error);
};
RetrieveSubjectToken(ctx_, options_, cb);
}
void ExternalAccountCredentials::OnRetrieveSubjectTokenInternal(
absl::string_view subject_token, grpc_error_handle error) {
if (error != GRPC_ERROR_NONE) {
FinishTokenFetch(error);
} else {
ExchangeToken(subject_token);
}
}
void ExternalAccountCredentials::ExchangeToken(
absl::string_view subject_token) {
absl::StatusOr<URI> uri = URI::Parse(options_.token_url);
if (!uri.ok()) {
FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Invalid token url: %s. Error: %s", options_.token_url,
uri.status().ToString())
.c_str()));
return;
}
grpc_httpcli_request request;
memset(&request, 0, sizeof(grpc_httpcli_request));
request.host = const_cast<char*>(uri->authority().c_str());
request.http.path = gpr_strdup(uri->path().c_str());
grpc_http_header* headers = nullptr;
if (!options_.client_id.empty() && !options_.client_secret.empty()) {
request.http.hdr_count = 2;
headers = static_cast<grpc_http_header*>(
gpr_malloc(sizeof(grpc_http_header) * request.http.hdr_count));
headers[0].key = gpr_strdup("Content-Type");
headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
std::string raw_cred =
absl::StrFormat("%s:%s", options_.client_id, options_.client_secret);
char* encoded_cred =
grpc_base64_encode(raw_cred.c_str(), raw_cred.length(), 0, 0);
std::string str = absl::StrFormat("Basic %s", std::string(encoded_cred));
headers[1].key = gpr_strdup("Authorization");
headers[1].value = gpr_strdup(str.c_str());
gpr_free(encoded_cred);
} else {
request.http.hdr_count = 1;
headers = static_cast<grpc_http_header*>(
gpr_malloc(sizeof(grpc_http_header) * request.http.hdr_count));
headers[0].key = gpr_strdup("Content-Type");
headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
}
request.http.hdrs = headers;
request.handshaker =
uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
std::vector<std::string> body_parts;
body_parts.push_back(absl::StrFormat("%s=%s", "audience",
UrlEncode(options_.audience).c_str()));
body_parts.push_back(absl::StrFormat(
"%s=%s", "grant_type",
UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE).c_str()));
body_parts.push_back(absl::StrFormat(
"%s=%s", "requested_token_type",
UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE).c_str()));
body_parts.push_back(
absl::StrFormat("%s=%s", "subject_token_type",
UrlEncode(options_.subject_token_type).c_str()));
body_parts.push_back(absl::StrFormat("%s=%s", "subject_token",
UrlEncode(subject_token).c_str()));
std::string scope = GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE;
if (options_.service_account_impersonation_url.empty()) {
scope = absl::StrJoin(scopes_, " ");
}
body_parts.push_back(
absl::StrFormat("%s=%s", "scope", UrlEncode(scope).c_str()));
std::string body = absl::StrJoin(body_parts, "&");
grpc_resource_quota* resource_quota =
grpc_resource_quota_create("external_account_credentials");
grpc_http_response_destroy(&ctx_->response);
ctx_->response = {};
GRPC_CLOSURE_INIT(&ctx_->closure, OnExchangeToken, this, nullptr);
grpc_httpcli_post(ctx_->httpcli_context, ctx_->pollent, resource_quota,
&request, body.c_str(), body.size(), ctx_->deadline,
&ctx_->closure, &ctx_->response);
grpc_resource_quota_unref_internal(resource_quota);
grpc_http_request_destroy(&request.http);
}
void ExternalAccountCredentials::OnExchangeToken(void* arg,
grpc_error_handle error) {
ExternalAccountCredentials* self =
static_cast<ExternalAccountCredentials*>(arg);
self->OnExchangeTokenInternal(GRPC_ERROR_REF(error));
}
void ExternalAccountCredentials::OnExchangeTokenInternal(
grpc_error_handle error) {
if (error != GRPC_ERROR_NONE) {
FinishTokenFetch(error);
} else {
if (options_.service_account_impersonation_url.empty()) {
metadata_req_->response = ctx_->response;
metadata_req_->response.body = gpr_strdup(
std::string(ctx_->response.body, ctx_->response.body_length).c_str());
metadata_req_->response.hdrs = static_cast<grpc_http_header*>(
gpr_malloc(sizeof(grpc_http_header) * ctx_->response.hdr_count));
for (size_t i = 0; i < ctx_->response.hdr_count; i++) {
metadata_req_->response.hdrs[i].key =
gpr_strdup(ctx_->response.hdrs[i].key);
metadata_req_->response.hdrs[i].value =
gpr_strdup(ctx_->response.hdrs[i].value);
}
FinishTokenFetch(GRPC_ERROR_NONE);
} else {
ImpersenateServiceAccount();
}
}
}
void ExternalAccountCredentials::ImpersenateServiceAccount() {
grpc_error_handle error = GRPC_ERROR_NONE;
absl::string_view response_body(ctx_->response.body,
ctx_->response.body_length);
Json json = Json::Parse(response_body, &error);
if (error != GRPC_ERROR_NONE || json.type() != Json::Type::OBJECT) {
FinishTokenFetch(GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
"Invalid token exchange response.", &error, 1));
GRPC_ERROR_UNREF(error);
return;
}
auto it = json.object_value().find("access_token");
if (it == json.object_value().end() ||
it->second.type() != Json::Type::STRING) {
FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Missing or invalid access_token in %s.", response_body)
.c_str()));
return;
}
std::string access_token = it->second.string_value();
absl::StatusOr<URI> uri =
URI::Parse(options_.service_account_impersonation_url);
if (!uri.ok()) {
FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat(
"Invalid service account impersonation url: %s. Error: %s",
options_.service_account_impersonation_url, uri.status().ToString())
.c_str()));
return;
}
grpc_httpcli_request request;
memset(&request, 0, sizeof(grpc_httpcli_request));
request.host = const_cast<char*>(uri->authority().c_str());
request.http.path = gpr_strdup(uri->path().c_str());
request.http.hdr_count = 2;
grpc_http_header* headers = static_cast<grpc_http_header*>(
gpr_malloc(sizeof(grpc_http_header) * request.http.hdr_count));
headers[0].key = gpr_strdup("Content-Type");
headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
std::string str = absl::StrFormat("Bearer %s", access_token);
headers[1].key = gpr_strdup("Authorization");
headers[1].value = gpr_strdup(str.c_str());
request.http.hdrs = headers;
request.handshaker =
uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
std::string scope = absl::StrJoin(scopes_, " ");
std::string body = absl::StrFormat("%s=%s", "scope", scope);
grpc_resource_quota* resource_quota =
grpc_resource_quota_create("external_account_credentials");
grpc_http_response_destroy(&ctx_->response);
ctx_->response = {};
GRPC_CLOSURE_INIT(&ctx_->closure, OnImpersenateServiceAccount, this, nullptr);
grpc_httpcli_post(ctx_->httpcli_context, ctx_->pollent, resource_quota,
&request, body.c_str(), body.size(), ctx_->deadline,
&ctx_->closure, &ctx_->response);
grpc_resource_quota_unref_internal(resource_quota);
grpc_http_request_destroy(&request.http);
}
void ExternalAccountCredentials::OnImpersenateServiceAccount(
void* arg, grpc_error_handle error) {
ExternalAccountCredentials* self =
static_cast<ExternalAccountCredentials*>(arg);
self->OnImpersenateServiceAccountInternal(GRPC_ERROR_REF(error));
}
void ExternalAccountCredentials::OnImpersenateServiceAccountInternal(
grpc_error_handle error) {
if (error != GRPC_ERROR_NONE) {
FinishTokenFetch(error);
return;
}
absl::string_view response_body(ctx_->response.body,
ctx_->response.body_length);
Json json = Json::Parse(response_body, &error);
if (error != GRPC_ERROR_NONE || json.type() != Json::Type::OBJECT) {
FinishTokenFetch(GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
"Invalid service account impersonation response.", &error, 1));
GRPC_ERROR_UNREF(error);
return;
}
auto it = json.object_value().find("accessToken");
if (it == json.object_value().end() ||
it->second.type() != Json::Type::STRING) {
FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Missing or invalid accessToken in %s.", response_body)
.c_str()));
return;
}
std::string access_token = it->second.string_value();
it = json.object_value().find("expireTime");
if (it == json.object_value().end() ||
it->second.type() != Json::Type::STRING) {
FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Missing or invalid expireTime in %s.", response_body)
.c_str()));
return;
}
std::string expire_time = it->second.string_value();
absl::Time t;
if (!absl::ParseTime(absl::RFC3339_full, expire_time, &t, nullptr)) {
FinishTokenFetch(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Invalid expire time of service account impersonation response."));
return;
}
int expire_in = (t - absl::Now()) / absl::Seconds(1);
std::string body = absl::StrFormat(
"{\"access_token\":\"%s\",\"expires_in\":%d,\"token_type\":\"Bearer\"}",
access_token, expire_in);
metadata_req_->response = ctx_->response;
metadata_req_->response.body = gpr_strdup(body.c_str());
metadata_req_->response.body_length = body.length();
metadata_req_->response.hdrs = static_cast<grpc_http_header*>(
gpr_malloc(sizeof(grpc_http_header) * ctx_->response.hdr_count));
for (size_t i = 0; i < ctx_->response.hdr_count; i++) {
metadata_req_->response.hdrs[i].key =
gpr_strdup(ctx_->response.hdrs[i].key);
metadata_req_->response.hdrs[i].value =
gpr_strdup(ctx_->response.hdrs[i].value);
}
FinishTokenFetch(GRPC_ERROR_NONE);
}
void ExternalAccountCredentials::FinishTokenFetch(grpc_error_handle error) {
GRPC_LOG_IF_ERROR("Fetch external account credentials access token",
GRPC_ERROR_REF(error));
// Move object state into local variables.
auto* cb = response_cb_;
response_cb_ = nullptr;
auto* metadata_req = metadata_req_;
metadata_req_ = nullptr;
auto* ctx = ctx_;
ctx_ = nullptr;
// Invoke the callback.
cb(metadata_req, error);
// Delete context.
delete ctx;
GRPC_ERROR_UNREF(error);
}
} // namespace grpc_core
grpc_call_credentials* grpc_external_account_credentials_create(
const char* json_string, const char* scopes_string) {
grpc_error_handle error = GRPC_ERROR_NONE;
grpc_core::Json json = grpc_core::Json::Parse(json_string, &error);
if (error != GRPC_ERROR_NONE) {
gpr_log(GPR_ERROR,
"External account credentials creation failed. Error: %s.",
grpc_error_std_string(error).c_str());
GRPC_ERROR_UNREF(error);
return nullptr;
}
std::vector<std::string> scopes = absl::StrSplit(scopes_string, ',');
auto creds = grpc_core::ExternalAccountCredentials::Create(
json, std::move(scopes), &error)
.release();
if (error != GRPC_ERROR_NONE) {
gpr_log(GPR_ERROR,
"External account credentials creation failed. Error: %s.",
grpc_error_std_string(error).c_str());
GRPC_ERROR_UNREF(error);
return nullptr;
}
return creds;
}