diff --git a/BUILD b/BUILD index 1c84d9f4b68..120aad21a06 100644 --- a/BUILD +++ b/BUILD @@ -1741,6 +1741,7 @@ grpc_cc_library( "src/core/lib/security/credentials/composite/composite_credentials.cc", "src/core/lib/security/credentials/credentials.cc", "src/core/lib/security/credentials/credentials_metadata.cc", + "src/core/lib/security/credentials/external/external_account_credentials.cc", "src/core/lib/security/credentials/fake/fake_credentials.cc", "src/core/lib/security/credentials/google_default/credentials_generic.cc", "src/core/lib/security/credentials/google_default/google_default_credentials.cc", @@ -1786,6 +1787,7 @@ grpc_cc_library( "src/core/lib/security/credentials/alts/alts_credentials.h", "src/core/lib/security/credentials/composite/composite_credentials.h", "src/core/lib/security/credentials/credentials.h", + "src/core/lib/security/credentials/external/external_account_credentials.h", "src/core/lib/security/credentials/fake/fake_credentials.h", "src/core/lib/security/credentials/google_default/google_default_credentials.h", "src/core/lib/security/credentials/iam/iam_credentials.h", diff --git a/BUILD.gn b/BUILD.gn index 9cc7c83d959..e30c5e268df 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -845,6 +845,8 @@ config("grpc_config") { "src/core/lib/security/credentials/credentials.cc", "src/core/lib/security/credentials/credentials.h", "src/core/lib/security/credentials/credentials_metadata.cc", + "src/core/lib/security/credentials/external/external_account_credentials.cc", + "src/core/lib/security/credentials/external/external_account_credentials.h", "src/core/lib/security/credentials/fake/fake_credentials.cc", "src/core/lib/security/credentials/fake/fake_credentials.h", "src/core/lib/security/credentials/google_default/credentials_generic.cc", diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dba3dd03a4..caa1f818190 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1755,6 +1755,7 @@ add_library(grpc src/core/lib/security/credentials/composite/composite_credentials.cc src/core/lib/security/credentials/credentials.cc src/core/lib/security/credentials/credentials_metadata.cc + src/core/lib/security/credentials/external/external_account_credentials.cc src/core/lib/security/credentials/fake/fake_credentials.cc src/core/lib/security/credentials/google_default/credentials_generic.cc src/core/lib/security/credentials/google_default/google_default_credentials.cc diff --git a/Makefile b/Makefile index 699afe6f0b0..ae10e17629b 100644 --- a/Makefile +++ b/Makefile @@ -2156,6 +2156,7 @@ LIBGRPC_SRC = \ src/core/lib/security/credentials/composite/composite_credentials.cc \ src/core/lib/security/credentials/credentials.cc \ src/core/lib/security/credentials/credentials_metadata.cc \ + src/core/lib/security/credentials/external/external_account_credentials.cc \ src/core/lib/security/credentials/fake/fake_credentials.cc \ src/core/lib/security/credentials/google_default/credentials_generic.cc \ src/core/lib/security/credentials/google_default/google_default_credentials.cc \ @@ -4602,6 +4603,7 @@ src/core/lib/security/credentials/alts/grpc_alts_credentials_server_options.cc: src/core/lib/security/credentials/composite/composite_credentials.cc: $(OPENSSL_DEP) src/core/lib/security/credentials/credentials.cc: $(OPENSSL_DEP) src/core/lib/security/credentials/credentials_metadata.cc: $(OPENSSL_DEP) +src/core/lib/security/credentials/external/external_account_credentials.cc: $(OPENSSL_DEP) src/core/lib/security/credentials/fake/fake_credentials.cc: $(OPENSSL_DEP) src/core/lib/security/credentials/google_default/credentials_generic.cc: $(OPENSSL_DEP) src/core/lib/security/credentials/google_default/google_default_credentials.cc: $(OPENSSL_DEP) diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml index a9228fefe89..f13ad017dd3 100644 --- a/build_autogenerated.yaml +++ b/build_autogenerated.yaml @@ -683,6 +683,7 @@ libs: - src/core/lib/security/credentials/alts/grpc_alts_credentials_options.h - src/core/lib/security/credentials/composite/composite_credentials.h - src/core/lib/security/credentials/credentials.h + - src/core/lib/security/credentials/external/external_account_credentials.h - src/core/lib/security/credentials/fake/fake_credentials.h - src/core/lib/security/credentials/google_default/google_default_credentials.h - src/core/lib/security/credentials/iam/iam_credentials.h @@ -1113,6 +1114,7 @@ libs: - src/core/lib/security/credentials/composite/composite_credentials.cc - src/core/lib/security/credentials/credentials.cc - src/core/lib/security/credentials/credentials_metadata.cc + - src/core/lib/security/credentials/external/external_account_credentials.cc - src/core/lib/security/credentials/fake/fake_credentials.cc - src/core/lib/security/credentials/google_default/credentials_generic.cc - src/core/lib/security/credentials/google_default/google_default_credentials.cc diff --git a/config.m4 b/config.m4 index 6ace72cd08a..f41f94ee407 100644 --- a/config.m4 +++ b/config.m4 @@ -418,6 +418,7 @@ if test "$PHP_GRPC" != "no"; then src/core/lib/security/credentials/composite/composite_credentials.cc \ src/core/lib/security/credentials/credentials.cc \ src/core/lib/security/credentials/credentials_metadata.cc \ + src/core/lib/security/credentials/external/external_account_credentials.cc \ src/core/lib/security/credentials/fake/fake_credentials.cc \ src/core/lib/security/credentials/google_default/credentials_generic.cc \ src/core/lib/security/credentials/google_default/google_default_credentials.cc \ @@ -992,6 +993,7 @@ if test "$PHP_GRPC" != "no"; then PHP_ADD_BUILD_DIR($ext_builddir/src/core/lib/security/credentials) PHP_ADD_BUILD_DIR($ext_builddir/src/core/lib/security/credentials/alts) PHP_ADD_BUILD_DIR($ext_builddir/src/core/lib/security/credentials/composite) + PHP_ADD_BUILD_DIR($ext_builddir/src/core/lib/security/credentials/external) PHP_ADD_BUILD_DIR($ext_builddir/src/core/lib/security/credentials/fake) PHP_ADD_BUILD_DIR($ext_builddir/src/core/lib/security/credentials/google_default) PHP_ADD_BUILD_DIR($ext_builddir/src/core/lib/security/credentials/iam) diff --git a/config.w32 b/config.w32 index 66bfada67e5..7e7d025c831 100644 --- a/config.w32 +++ b/config.w32 @@ -385,6 +385,7 @@ if (PHP_GRPC != "no") { "src\\core\\lib\\security\\credentials\\composite\\composite_credentials.cc " + "src\\core\\lib\\security\\credentials\\credentials.cc " + "src\\core\\lib\\security\\credentials\\credentials_metadata.cc " + + "src\\core\\lib\\security\\credentials\\external\\external_account_credentials.cc " + "src\\core\\lib\\security\\credentials\\fake\\fake_credentials.cc " + "src\\core\\lib\\security\\credentials\\google_default\\credentials_generic.cc " + "src\\core\\lib\\security\\credentials\\google_default\\google_default_credentials.cc " + @@ -1035,6 +1036,7 @@ if (PHP_GRPC != "no") { FSO.CreateFolder(base_dir+"\\ext\\grpc\\src\\core\\lib\\security\\credentials"); FSO.CreateFolder(base_dir+"\\ext\\grpc\\src\\core\\lib\\security\\credentials\\alts"); FSO.CreateFolder(base_dir+"\\ext\\grpc\\src\\core\\lib\\security\\credentials\\composite"); + FSO.CreateFolder(base_dir+"\\ext\\grpc\\src\\core\\lib\\security\\credentials\\external"); FSO.CreateFolder(base_dir+"\\ext\\grpc\\src\\core\\lib\\security\\credentials\\fake"); FSO.CreateFolder(base_dir+"\\ext\\grpc\\src\\core\\lib\\security\\credentials\\google_default"); FSO.CreateFolder(base_dir+"\\ext\\grpc\\src\\core\\lib\\security\\credentials\\iam"); diff --git a/gRPC-C++.podspec b/gRPC-C++.podspec index 5e09e3cf732..67509b76579 100644 --- a/gRPC-C++.podspec +++ b/gRPC-C++.podspec @@ -530,6 +530,7 @@ Pod::Spec.new do |s| 'src/core/lib/security/credentials/alts/grpc_alts_credentials_options.h', 'src/core/lib/security/credentials/composite/composite_credentials.h', 'src/core/lib/security/credentials/credentials.h', + 'src/core/lib/security/credentials/external/external_account_credentials.h', 'src/core/lib/security/credentials/fake/fake_credentials.h', 'src/core/lib/security/credentials/google_default/google_default_credentials.h', 'src/core/lib/security/credentials/iam/iam_credentials.h', @@ -1049,6 +1050,7 @@ Pod::Spec.new do |s| 'src/core/lib/security/credentials/alts/grpc_alts_credentials_options.h', 'src/core/lib/security/credentials/composite/composite_credentials.h', 'src/core/lib/security/credentials/credentials.h', + 'src/core/lib/security/credentials/external/external_account_credentials.h', 'src/core/lib/security/credentials/fake/fake_credentials.h', 'src/core/lib/security/credentials/google_default/google_default_credentials.h', 'src/core/lib/security/credentials/iam/iam_credentials.h', diff --git a/gRPC-Core.podspec b/gRPC-Core.podspec index d748105ec56..a11224f65dd 100644 --- a/gRPC-Core.podspec +++ b/gRPC-Core.podspec @@ -900,6 +900,8 @@ Pod::Spec.new do |s| 'src/core/lib/security/credentials/credentials.cc', 'src/core/lib/security/credentials/credentials.h', 'src/core/lib/security/credentials/credentials_metadata.cc', + 'src/core/lib/security/credentials/external/external_account_credentials.cc', + 'src/core/lib/security/credentials/external/external_account_credentials.h', 'src/core/lib/security/credentials/fake/fake_credentials.cc', 'src/core/lib/security/credentials/fake/fake_credentials.h', 'src/core/lib/security/credentials/google_default/credentials_generic.cc', @@ -1491,6 +1493,7 @@ Pod::Spec.new do |s| 'src/core/lib/security/credentials/alts/grpc_alts_credentials_options.h', 'src/core/lib/security/credentials/composite/composite_credentials.h', 'src/core/lib/security/credentials/credentials.h', + 'src/core/lib/security/credentials/external/external_account_credentials.h', 'src/core/lib/security/credentials/fake/fake_credentials.h', 'src/core/lib/security/credentials/google_default/google_default_credentials.h', 'src/core/lib/security/credentials/iam/iam_credentials.h', diff --git a/grpc.gemspec b/grpc.gemspec index 269ac44a4b6..4437badcb0a 100644 --- a/grpc.gemspec +++ b/grpc.gemspec @@ -818,6 +818,8 @@ Gem::Specification.new do |s| s.files += %w( src/core/lib/security/credentials/credentials.cc ) s.files += %w( src/core/lib/security/credentials/credentials.h ) s.files += %w( src/core/lib/security/credentials/credentials_metadata.cc ) + s.files += %w( src/core/lib/security/credentials/external/external_account_credentials.cc ) + s.files += %w( src/core/lib/security/credentials/external/external_account_credentials.h ) s.files += %w( src/core/lib/security/credentials/fake/fake_credentials.cc ) s.files += %w( src/core/lib/security/credentials/fake/fake_credentials.h ) s.files += %w( src/core/lib/security/credentials/google_default/credentials_generic.cc ) diff --git a/grpc.gyp b/grpc.gyp index d615edb3be3..2c66c4637c7 100644 --- a/grpc.gyp +++ b/grpc.gyp @@ -782,6 +782,7 @@ 'src/core/lib/security/credentials/composite/composite_credentials.cc', 'src/core/lib/security/credentials/credentials.cc', 'src/core/lib/security/credentials/credentials_metadata.cc', + 'src/core/lib/security/credentials/external/external_account_credentials.cc', 'src/core/lib/security/credentials/fake/fake_credentials.cc', 'src/core/lib/security/credentials/google_default/credentials_generic.cc', 'src/core/lib/security/credentials/google_default/google_default_credentials.cc', diff --git a/package.xml b/package.xml index 208e4878576..f9dfce3a06d 100644 --- a/package.xml +++ b/package.xml @@ -798,6 +798,8 @@ + + diff --git a/src/core/lib/security/credentials/external/external_account_credentials.cc b/src/core/lib/security/credentials/external/external_account_credentials.cc new file mode 100644 index 00000000000..599b718655f --- /dev/null +++ b/src/core/lib/security/credentials/external/external_account_credentials.cc @@ -0,0 +1,311 @@ +// +// 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 + +#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/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" + +#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 { + +ExternalAccountCredentials::ExternalAccountCredentials( + ExternalAccountCredentialsOptions options, std::vector 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* error) { + OnRetrieveSubjectTokenInternal(token, error); + }; + RetrieveSubjectToken(ctx_, options_, cb); +} + +void ExternalAccountCredentials::OnRetrieveSubjectTokenInternal( + absl::string_view subject_token, grpc_error* error) { + if (error != GRPC_ERROR_NONE) { + FinishTokenFetch(error); + } else { + ExchangeToken(subject_token); + } +} + +void ExternalAccountCredentials::ExchangeToken( + absl::string_view subject_token) { + grpc_uri* uri = grpc_uri_parse(options_.token_url, false); + if (uri == nullptr) { + FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING( + absl::StrFormat("Invalid token url: %s.", options_.token_url).c_str())); + return; + } + grpc_httpcli_request request; + memset(&request, 0, sizeof(grpc_httpcli_request)); + request.host = const_cast(uri->authority); + request.http.path = gpr_strdup(uri->path); + grpc_http_header* headers = nullptr; + if (!options_.client_id.empty() && !options_.client_secret.empty()) { + request.http.hdr_count = 2; + headers = static_cast( + 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( + 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 = (strcmp(uri->scheme, "https") == 0) + ? &grpc_httpcli_ssl + : &grpc_httpcli_plaintext; + std::vector body_parts; + body_parts.push_back(absl::StrFormat("%s=%s", "audience", options_.audience)); + body_parts.push_back(absl::StrFormat( + "%s=%s", "grant_type", EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE)); + body_parts.push_back( + absl::StrFormat("%s=%s", "requested_token_type", + EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE)); + body_parts.push_back(absl::StrFormat("%s=%s", "subject_token_type", + options_.subject_token_type)); + body_parts.push_back( + absl::StrFormat("%s=%s", "subject_token", subject_token)); + 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", scope)); + 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); + grpc_uri_destroy(uri); +} + +void ExternalAccountCredentials::OnExchangeToken(void* arg, grpc_error* error) { + ExternalAccountCredentials* self = + static_cast(arg); + self->OnExchangeTokenInternal(GRPC_ERROR_REF(error)); +} + +void ExternalAccountCredentials::OnExchangeTokenInternal(grpc_error* 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(ctx_->response.body); + FinishTokenFetch(GRPC_ERROR_NONE); + } else { + ImpersenateServiceAccount(); + } + } +} + +void ExternalAccountCredentials::ImpersenateServiceAccount() { + grpc_error* 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(); + grpc_uri* uri = + grpc_uri_parse(options_.service_account_impersonation_url, false); + if (uri == nullptr) { + FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING( + absl::StrFormat("Invalid service account impersonation url: %s.", + options_.service_account_impersonation_url) + .c_str())); + return; + } + grpc_httpcli_request request; + memset(&request, 0, sizeof(grpc_httpcli_request)); + request.host = const_cast(uri->authority); + request.http.path = gpr_strdup(uri->path); + request.http.hdr_count = 2; + grpc_http_header* headers = static_cast( + 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 = (strcmp(uri->scheme, "https") == 0) + ? &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); + grpc_uri_destroy(uri); +} + +void ExternalAccountCredentials::OnImpersenateServiceAccount( + void* arg, grpc_error* error) { + ExternalAccountCredentials* self = + static_cast(arg); + self->OnImpersenateServiceAccountInternal(GRPC_ERROR_REF(error)); +} + +void ExternalAccountCredentials::OnImpersenateServiceAccountInternal( + grpc_error* 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(); + FinishTokenFetch(GRPC_ERROR_NONE); +} + +void ExternalAccountCredentials::FinishTokenFetch(grpc_error* 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 diff --git a/src/core/lib/security/credentials/external/external_account_credentials.h b/src/core/lib/security/credentials/external/external_account_credentials.h new file mode 100644 index 00000000000..96a02232739 --- /dev/null +++ b/src/core/lib/security/credentials/external/external_account_credentials.h @@ -0,0 +1,119 @@ +// +// 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. +// + +#ifndef GRPC_CORE_LIB_SECURITY_CREDENTIALS_EXTERNAL_EXTERNAL_ACCOUNT_CREDENTIALS_H +#define GRPC_CORE_LIB_SECURITY_CREDENTIALS_EXTERNAL_EXTERNAL_ACCOUNT_CREDENTIALS_H + +#include + +#include +#include + +#include "src/core/lib/json/json.h" +#include "src/core/lib/security/credentials/oauth2/oauth2_credentials.h" + +namespace grpc_core { + +// Base external account credentials. The base class implements common logic for +// exchanging external account credentials for GCP access token to authorize +// requests to GCP APIs. The specific logic of retrieving subject token is +// implemented in subclasses. +class ExternalAccountCredentials + : public grpc_oauth2_token_fetcher_credentials { + public: + // External account credentials json interface. + struct ExternalAccountCredentialsOptions { + std::string type; + std::string audience; + std::string subject_token_type; + std::string service_account_impersonation_url; + std::string token_url; + std::string token_info_url; + Json credential_source; + std::string quota_project_id; + std::string client_id; + std::string client_secret; + }; + + ExternalAccountCredentials(ExternalAccountCredentialsOptions options, + std::vector scopes); + ~ExternalAccountCredentials() override; + std::string debug_string() override; + + protected: + // This is a helper struct to pass information between multiple callback based + // asynchronous calls. + struct HTTPRequestContext { + HTTPRequestContext(grpc_httpcli_context* httpcli_context, + grpc_polling_entity* pollent, grpc_millis deadline) + : httpcli_context(httpcli_context), + pollent(pollent), + deadline(deadline) {} + ~HTTPRequestContext() { grpc_http_response_destroy(&response); } + + // Contextual parameters passed from + // grpc_oauth2_token_fetcher_credentials::fetch_oauth2(). + grpc_httpcli_context* httpcli_context; + grpc_polling_entity* pollent; + grpc_millis deadline; + + // Reusable token fetch http response and closure. + grpc_closure closure; + grpc_http_response response; + }; + + // Subclasses of base external account credentials need to override this + // method to implement the specific subject token retrieval logic. + // Once the subject token is ready, subclasses need to invoke + // the callback function (cb) to pass the subject token (or error) + // back. + virtual void RetrieveSubjectToken( + const HTTPRequestContext* ctx, + const ExternalAccountCredentialsOptions& options, + std::function cb) = 0; + + private: + // This method implements the common token fetch logic and it will be called + // when grpc_oauth2_token_fetcher_credentials request a new access token. + void fetch_oauth2(grpc_credentials_metadata_request* req, + grpc_httpcli_context* httpcli_context, + grpc_polling_entity* pollent, grpc_iomgr_cb_func cb, + grpc_millis deadline) override; + + void OnRetrieveSubjectTokenInternal(absl::string_view subject_token, + grpc_error* error); + + void ExchangeToken(absl::string_view subject_token); + static void OnExchangeToken(void* arg, grpc_error* error); + void OnExchangeTokenInternal(grpc_error* error); + + void ImpersenateServiceAccount(); + static void OnImpersenateServiceAccount(void* arg, grpc_error* error); + void OnImpersenateServiceAccountInternal(grpc_error* error); + + void FinishTokenFetch(grpc_error* error); + + ExternalAccountCredentialsOptions options_; + std::vector scopes_; + + HTTPRequestContext* ctx_ = nullptr; + grpc_credentials_metadata_request* metadata_req_ = nullptr; + grpc_iomgr_cb_func response_cb_ = nullptr; +}; + +} // namespace grpc_core + +#endif // GRPC_CORE_LIB_SECURITY_CREDENTIALS_EXTERNAL_EXTERNAL_ACCOUNT_CREDENTIALS_H diff --git a/src/core/lib/security/util/json_util.h b/src/core/lib/security/util/json_util.h index 42f7005e00e..cde9ca97ffd 100644 --- a/src/core/lib/security/util/json_util.h +++ b/src/core/lib/security/util/json_util.h @@ -30,6 +30,7 @@ #define GRPC_AUTH_JSON_TYPE_INVALID "invalid" #define GRPC_AUTH_JSON_TYPE_SERVICE_ACCOUNT "service_account" #define GRPC_AUTH_JSON_TYPE_AUTHORIZED_USER "authorized_user" +#define GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT "external_account" // Gets a child property from a json node. const char* grpc_json_get_string_property(const grpc_core::Json& json, diff --git a/src/python/grpcio/grpc_core_dependencies.py b/src/python/grpcio/grpc_core_dependencies.py index 74cbc4235b6..35edeb799df 100644 --- a/src/python/grpcio/grpc_core_dependencies.py +++ b/src/python/grpcio/grpc_core_dependencies.py @@ -394,6 +394,7 @@ CORE_SOURCE_FILES = [ 'src/core/lib/security/credentials/composite/composite_credentials.cc', 'src/core/lib/security/credentials/credentials.cc', 'src/core/lib/security/credentials/credentials_metadata.cc', + 'src/core/lib/security/credentials/external/external_account_credentials.cc', 'src/core/lib/security/credentials/fake/fake_credentials.cc', 'src/core/lib/security/credentials/google_default/credentials_generic.cc', 'src/core/lib/security/credentials/google_default/google_default_credentials.cc', diff --git a/test/core/security/credentials_test.cc b/test/core/security/credentials_test.cc index 22f6cdb7573..32c00179acf 100644 --- a/test/core/security/credentials_test.cc +++ b/test/core/security/credentials_test.cc @@ -43,6 +43,7 @@ #include "src/core/lib/http/httpcli.h" #include "src/core/lib/iomgr/error.h" #include "src/core/lib/security/credentials/composite/composite_credentials.h" +#include "src/core/lib/security/credentials/external/external_account_credentials.h" #include "src/core/lib/security/credentials/fake/fake_credentials.h" #include "src/core/lib/security/credentials/google_default/google_default_credentials.h" #include "src/core/lib/security/credentials/jwt/jwt_credentials.h" @@ -135,6 +136,16 @@ static const char test_sts_endpoint_url[] = static const char test_method[] = "ThisIsNotAMethod"; +static const char valid_external_account_creds_token_exchange_response[] = + "{\"access_token\":\"token_exchange_access_token\"," + " \"expires_in\":3599," + " \"token_type\":\"Bearer\"}"; + +static const char + valid_external_account_creds_service_account_impersonation_response[] = + "{\"accessToken\":\"service_account_impersonation_access_token\"," + " \"expireTime\":\"2050-01-01T00:00:00Z\"}"; + /* -- Global state flags. -- */ static bool g_test_is_on_gce = false; @@ -1881,6 +1892,297 @@ static void test_auth_metadata_context(void) { } } +static void validate_external_account_creds_token_exchage_request( + const grpc_httpcli_request* request, const char* body, size_t body_size, + bool expect_actor_token) { + // Check that the body is constructed properly. + GPR_ASSERT(body != nullptr); + GPR_ASSERT(body_size != 0); + GPR_ASSERT(request->handshaker == &grpc_httpcli_ssl); + std::string get_url_equivalent = + absl::StrFormat("%s?%s", "https://foo.com:5555/token", body); + grpc_uri* uri = grpc_uri_parse(get_url_equivalent.c_str(), false); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(uri, "audience"), "audience") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(uri, "grant_type"), + "urn:ietf:params:oauth:grant-type:token-exchange") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(uri, "requested_token_type"), + "urn:ietf:params:oauth:token-type:access_token") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(uri, "subject_token"), + "test_subject_token") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(uri, "subject_token_type"), + "subject_token_type") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(uri, "scope"), + "https://www.googleapis.com/auth/cloud-platform") == 0); + grpc_uri_destroy(uri); + + // Check the rest of the request. + GPR_ASSERT(strcmp(request->host, "foo.com:5555") == 0); + GPR_ASSERT(strcmp(request->http.path, "/token") == 0); + GPR_ASSERT(request->http.hdr_count == 2); + GPR_ASSERT(strcmp(request->http.hdrs[0].key, "Content-Type") == 0); + GPR_ASSERT(strcmp(request->http.hdrs[0].value, + "application/x-www-form-urlencoded") == 0); + GPR_ASSERT(strcmp(request->http.hdrs[1].key, "Authorization") == 0); + GPR_ASSERT(strcmp(request->http.hdrs[1].value, + "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=") == 0); +} + +static void +validate_external_account_creds_service_account_impersonation_request( + const grpc_httpcli_request* request, const char* body, size_t body_size, + bool expect_actor_token) { + // Check that the body is constructed properly. + GPR_ASSERT(body != nullptr); + GPR_ASSERT(body_size != 0); + GPR_ASSERT(request->handshaker == &grpc_httpcli_ssl); + GPR_ASSERT(strcmp(body, "scope=scope_1 scope_2") == 0); + + // Check the rest of the request. + GPR_ASSERT(strcmp(request->host, "foo.com:5555") == 0); + GPR_ASSERT(strcmp(request->http.path, "/service_account_impersonation") == 0); + GPR_ASSERT(request->http.hdr_count == 2); + GPR_ASSERT(strcmp(request->http.hdrs[0].key, "Content-Type") == 0); + GPR_ASSERT(strcmp(request->http.hdrs[0].value, + "application/x-www-form-urlencoded") == 0); + GPR_ASSERT(strcmp(request->http.hdrs[1].key, "Authorization") == 0); + GPR_ASSERT(strcmp(request->http.hdrs[1].value, + "Bearer token_exchange_access_token") == 0); +} + +static int external_account_creds_httpcli_post_success( + const grpc_httpcli_request* request, const char* body, size_t body_size, + grpc_millis /*deadline*/, grpc_closure* on_done, + grpc_httpcli_response* response) { + if (strcmp(request->http.path, "/token") == 0) { + validate_external_account_creds_token_exchage_request(request, body, + body_size, true); + *response = http_response( + 200, valid_external_account_creds_token_exchange_response); + } else if (strcmp(request->http.path, "/service_account_impersonation") == + 0) { + validate_external_account_creds_service_account_impersonation_request( + request, body, body_size, true); + *response = http_response( + 200, + valid_external_account_creds_service_account_impersonation_response); + } + grpc_core::ExecCtx::Run(DEBUG_LOCATION, on_done, GRPC_ERROR_NONE); + return 1; +} + +static int +external_account_creds_httpcli_post_failure_token_exchange_response_missing_access_token( + const grpc_httpcli_request* request, const char* body, size_t body_size, + grpc_millis /*deadline*/, grpc_closure* on_done, + grpc_httpcli_response* response) { + if (strcmp(request->http.path, "/token") == 0) { + *response = http_response(200, + "{\"not_access_token\":\"not_access_token\"," + "\"expires_in\":3599," + " \"token_type\":\"Bearer\"}"); + } else if (strcmp(request->http.path, "/service_account_impersonation") == + 0) { + *response = http_response( + 200, + valid_external_account_creds_service_account_impersonation_response); + } + grpc_core::ExecCtx::Run(DEBUG_LOCATION, on_done, GRPC_ERROR_NONE); + return 1; +} + +// The subclass of ExternalAccountCredentials for testing. +// ExternalAccountCredentials is an abstract class so we can't directly test +// against it. +class TestExternalAccountCredentials final + : public grpc_core::ExternalAccountCredentials { + public: + TestExternalAccountCredentials(ExternalAccountCredentialsOptions options, + std::vector scopes) + : ExternalAccountCredentials(std::move(options), std::move(scopes)) {} + + protected: + void RetrieveSubjectToken(const HTTPRequestContext* ctx, + const ExternalAccountCredentialsOptions& options, + std::function cb) { + cb("test_subject_token", GRPC_ERROR_NONE); + } +}; + +static void test_external_account_creds_success(void) { + expected_md emd[] = {{"authorization", "Bearer token_exchange_access_token"}}; + grpc_core::ExecCtx exec_ctx; + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + grpc_core::Json credential_source(""); + TestExternalAccountCredentials::ExternalAccountCredentialsOptions options = { + "external_account", // type; + "audience", // audience; + "subject_token_type", // subject_token_type; + "", // service_account_impersonation_url; + "https://foo.com:5555/token", // token_url; + "https://foo.com:5555/token_info", // token_info_url; + credential_source, // credential_source; + "quota_project_id", // quota_project_id; + "client_id", // client_id; + "client_secret", // client_secret; + }; + TestExternalAccountCredentials creds(options, {}); + /* Check security level. */ + GPR_ASSERT(creds.min_security_level() == GRPC_PRIVACY_AND_INTEGRITY); + /* First request: http put should be called. */ + request_metadata_state* state = + make_request_metadata_state(GRPC_ERROR_NONE, emd, GPR_ARRAY_SIZE(emd)); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + external_account_creds_httpcli_post_success); + run_request_metadata_test(&creds, auth_md_ctx, state); + grpc_core::ExecCtx::Get()->Flush(); + /* Second request: the cached token should be served directly. */ + state = + make_request_metadata_state(GRPC_ERROR_NONE, emd, GPR_ARRAY_SIZE(emd)); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + httpcli_post_should_not_be_called); + run_request_metadata_test(&creds, auth_md_ctx, state); + grpc_core::ExecCtx::Get()->Flush(); + grpc_httpcli_set_override(nullptr, nullptr); +} + +static void +test_external_account_creds_success_with_service_account_impersonation(void) { + expected_md emd[] = { + {"authorization", "Bearer service_account_impersonation_access_token"}}; + grpc_core::ExecCtx exec_ctx; + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + grpc_core::Json credential_source(""); + TestExternalAccountCredentials::ExternalAccountCredentialsOptions options = { + "external_account", // type; + "audience", // audience; + "subject_token_type", // subject_token_type; + "https://foo.com:5555/service_account_impersonation", // service_account_impersonation_url; + "https://foo.com:5555/token", // token_url; + "https://foo.com:5555/token_info", // token_info_url; + credential_source, // credential_source; + "quota_project_id", // quota_project_id; + "client_id", // client_id; + "client_secret", // client_secret; + }; + TestExternalAccountCredentials creds(options, {"scope_1", "scope_2"}); + /* Check security level. */ + GPR_ASSERT(creds.min_security_level() == GRPC_PRIVACY_AND_INTEGRITY); + /* First request: http put should be called. */ + request_metadata_state* state = + make_request_metadata_state(GRPC_ERROR_NONE, emd, GPR_ARRAY_SIZE(emd)); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + external_account_creds_httpcli_post_success); + run_request_metadata_test(&creds, auth_md_ctx, state); + grpc_core::ExecCtx::Get()->Flush(); + grpc_httpcli_set_override(nullptr, nullptr); +} + +static void test_external_account_creds_faiure_invalid_token_url(void) { + grpc_core::ExecCtx exec_ctx; + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + grpc_core::Json credential_source(""); + TestExternalAccountCredentials::ExternalAccountCredentialsOptions options = { + "external_account", // type; + "audience", // audience; + "subject_token_type", // subject_token_type; + "https://foo.com:5555/service_account_impersonation", // service_account_impersonation_url; + "invalid_token_url", // token_url; + "https://foo.com:5555/token_info", // token_info_url; + credential_source, // credential_source; + "quota_project_id", // quota_project_id; + "client_id", // client_id; + "client_secret", // client_secret; + }; + TestExternalAccountCredentials creds(options, {}); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + httpcli_post_should_not_be_called); + grpc_error* error = GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Invalid token url: invalid_token_url."); + grpc_error* expected_error = GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING( + "Error occurred when fetching oauth2 token.", &error, 1); + request_metadata_state* state = + make_request_metadata_state(expected_error, nullptr, 0); + run_request_metadata_test(&creds, auth_md_ctx, state); + GRPC_ERROR_UNREF(error); + grpc_core::ExecCtx::Get()->Flush(); + grpc_httpcli_set_override(nullptr, nullptr); +} + +static void +test_external_account_creds_faiure_invalid_service_account_impersonation_url( + void) { + grpc_core::ExecCtx exec_ctx; + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + grpc_core::Json credential_source(""); + TestExternalAccountCredentials::ExternalAccountCredentialsOptions options = { + "external_account", // type; + "audience", // audience; + "subject_token_type", // subject_token_type; + "invalid_service_account_impersonation_url", // service_account_impersonation_url; + "https://foo.com:5555/token", // token_url; + "https://foo.com:5555/token_info", // token_info_url; + credential_source, // credential_source; + "quota_project_id", // quota_project_id; + "client_id", // client_id; + "client_secret", // client_secret; + }; + TestExternalAccountCredentials creds(options, {}); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + external_account_creds_httpcli_post_success); + grpc_error* error = GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Invalid service account impersonation url: " + "invalid_service_account_impersonation_url."); + grpc_error* expected_error = GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING( + "Error occurred when fetching oauth2 token.", &error, 1); + request_metadata_state* state = + make_request_metadata_state(expected_error, nullptr, 0); + run_request_metadata_test(&creds, auth_md_ctx, state); + GRPC_ERROR_UNREF(error); + grpc_core::ExecCtx::Get()->Flush(); + grpc_httpcli_set_override(nullptr, nullptr); +} + +static void +test_external_account_creds_faiure_token_exchange_response_missing_access_token( + void) { + grpc_core::ExecCtx exec_ctx; + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + grpc_core::Json credential_source(""); + TestExternalAccountCredentials::ExternalAccountCredentialsOptions options = { + "external_account", // type; + "audience", // audience; + "subject_token_type", // subject_token_type; + "https://foo.com:5555/service_account_impersonation", // service_account_impersonation_url; + "https://foo.com:5555/token", // token_url; + "https://foo.com:5555/token_info", // token_info_url; + credential_source, // credential_source; + "quota_project_id", // quota_project_id; + "client_id", // client_id; + "client_secret", // client_secret; + }; + TestExternalAccountCredentials creds(options, {}); + grpc_httpcli_set_override( + httpcli_get_should_not_be_called, + external_account_creds_httpcli_post_failure_token_exchange_response_missing_access_token); + grpc_error* error = GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Missing or invalid access_token in " + "{\"not_access_token\":\"not_access_token\",\"expires_in\":3599,\"token_" + "type\":\"Bearer\"}."); + grpc_error* expected_error = GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING( + "Error occurred when fetching oauth2 token.", &error, 1); + request_metadata_state* state = + make_request_metadata_state(expected_error, nullptr, 0); + run_request_metadata_test(&creds, auth_md_ctx, state); + GRPC_ERROR_UNREF(error); + grpc_core::ExecCtx::Get()->Flush(); + grpc_httpcli_set_override(nullptr, nullptr); +} + int main(int argc, char** argv) { grpc::testing::TestEnvironment env(argc, argv); grpc_init(); @@ -1925,6 +2227,11 @@ int main(int argc, char** argv) { test_get_well_known_google_credentials_file_path(); test_channel_creds_duplicate_without_call_creds(); test_auth_metadata_context(); + test_external_account_creds_success(); + test_external_account_creds_success_with_service_account_impersonation(); + test_external_account_creds_faiure_invalid_token_url(); + test_external_account_creds_faiure_invalid_service_account_impersonation_url(); + test_external_account_creds_faiure_token_exchange_response_missing_access_token(); grpc_shutdown(); return 0; } diff --git a/tools/doxygen/Doxyfile.c++.internal b/tools/doxygen/Doxyfile.c++.internal index 45a8ace92c7..488cfb8b4c8 100644 --- a/tools/doxygen/Doxyfile.c++.internal +++ b/tools/doxygen/Doxyfile.c++.internal @@ -1751,6 +1751,8 @@ src/core/lib/security/credentials/composite/composite_credentials.h \ src/core/lib/security/credentials/credentials.cc \ src/core/lib/security/credentials/credentials.h \ src/core/lib/security/credentials/credentials_metadata.cc \ +src/core/lib/security/credentials/external/external_account_credentials.cc \ +src/core/lib/security/credentials/external/external_account_credentials.h \ src/core/lib/security/credentials/fake/fake_credentials.cc \ src/core/lib/security/credentials/fake/fake_credentials.h \ src/core/lib/security/credentials/google_default/credentials_generic.cc \ diff --git a/tools/doxygen/Doxyfile.core.internal b/tools/doxygen/Doxyfile.core.internal index 2c88021a1ff..166c68f0cee 100644 --- a/tools/doxygen/Doxyfile.core.internal +++ b/tools/doxygen/Doxyfile.core.internal @@ -1594,6 +1594,8 @@ src/core/lib/security/credentials/composite/composite_credentials.h \ src/core/lib/security/credentials/credentials.cc \ src/core/lib/security/credentials/credentials.h \ src/core/lib/security/credentials/credentials_metadata.cc \ +src/core/lib/security/credentials/external/external_account_credentials.cc \ +src/core/lib/security/credentials/external/external_account_credentials.h \ src/core/lib/security/credentials/fake/fake_credentials.cc \ src/core/lib/security/credentials/fake/fake_credentials.h \ src/core/lib/security/credentials/google_default/credentials_generic.cc \