1 // Copyright 2020 gRPC authors.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 //
15
16 #include "src/core/lib/security/credentials/external/external_account_credentials.h"
17
18 #include <grpc/credentials.h>
19 #include <grpc/grpc.h>
20 #include <grpc/grpc_security.h>
21 #include <grpc/support/alloc.h>
22 #include <grpc/support/json.h>
23 #include <grpc/support/port_platform.h>
24 #include <grpc/support/string_util.h>
25 #include <stdint.h>
26 #include <string.h>
27
28 #include <map>
29 #include <memory>
30 #include <utility>
31
32 #include "absl/log/check.h"
33 #include "absl/log/log.h"
34 #include "absl/status/status.h"
35 #include "absl/status/statusor.h"
36 #include "absl/strings/escaping.h"
37 #include "absl/strings/match.h"
38 #include "absl/strings/numbers.h"
39 #include "absl/strings/str_cat.h"
40 #include "absl/strings/str_format.h"
41 #include "absl/strings/str_join.h"
42 #include "absl/strings/str_split.h"
43 #include "absl/strings/strip.h"
44 #include "absl/time/clock.h"
45 #include "absl/time/time.h"
46 #include "src/core/lib/security/credentials/credentials.h"
47 #include "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
48 #include "src/core/lib/security/credentials/external/file_external_account_credentials.h"
49 #include "src/core/lib/security/credentials/external/url_external_account_credentials.h"
50 #include "src/core/lib/security/util/json_util.h"
51 #include "src/core/util/http_client/httpcli_ssl_credentials.h"
52 #include "src/core/util/http_client/parser.h"
53 #include "src/core/util/json/json_reader.h"
54 #include "src/core/util/json/json_writer.h"
55 #include "src/core/util/status_helper.h"
56 #include "src/core/util/uri.h"
57
58 #define EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE \
59 "urn:ietf:params:oauth:grant-type:token-exchange"
60 #define EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE \
61 "urn:ietf:params:oauth:token-type:access_token"
62 #define GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE \
63 "https://www.googleapis.com/auth/cloud-platform"
64 #define IMPERSONATED_CRED_DEFAULT_LIFETIME_IN_SECONDS 3600 // 1 hour
65 #define IMPERSONATED_CRED_MIN_LIFETIME_IN_SECONDS 600 // 10 mins
66 #define IMPERSONATED_CRED_MAX_LIFETIME_IN_SECONDS 43200 // 12 hours
67
68 namespace grpc_core {
69
70 //
71 // ExternalAccountCredentials::NoOpFetchBody
72 //
73
NoOpFetchBody(grpc_event_engine::experimental::EventEngine & event_engine,absl::AnyInvocable<void (absl::StatusOr<std::string>)> on_done,absl::StatusOr<std::string> result)74 ExternalAccountCredentials::NoOpFetchBody::NoOpFetchBody(
75 grpc_event_engine::experimental::EventEngine& event_engine,
76 absl::AnyInvocable<void(absl::StatusOr<std::string>)> on_done,
77 absl::StatusOr<std::string> result)
78 : FetchBody(std::move(on_done)) {
79 event_engine.Run([self = RefAsSubclass<NoOpFetchBody>(),
80 result = std::move(result)]() mutable {
81 ApplicationCallbackExecCtx application_exec_ctx;
82 ExecCtx exec_ctx;
83 self->Finish(std::move(result));
84 });
85 }
86
87 //
88 // ExternalAccountCredentials::HttpFetchBody
89 //
90
HttpFetchBody(absl::FunctionRef<OrphanablePtr<HttpRequest> (grpc_http_response *,grpc_closure *)> start_http_request,absl::AnyInvocable<void (absl::StatusOr<std::string>)> on_done)91 ExternalAccountCredentials::HttpFetchBody::HttpFetchBody(
92 absl::FunctionRef<OrphanablePtr<HttpRequest>(grpc_http_response*,
93 grpc_closure*)>
94 start_http_request,
95 absl::AnyInvocable<void(absl::StatusOr<std::string>)> on_done)
96 : FetchBody(std::move(on_done)) {
97 GRPC_CLOSURE_INIT(&on_http_response_, OnHttpResponse, this, nullptr);
98 Ref().release(); // Ref held by HTTP request callback.
99 http_request_ = start_http_request(&response_, &on_http_response_);
100 }
101
OnHttpResponse(void * arg,grpc_error_handle error)102 void ExternalAccountCredentials::HttpFetchBody::OnHttpResponse(
103 void* arg, grpc_error_handle error) {
104 RefCountedPtr<HttpFetchBody> self(static_cast<HttpFetchBody*>(arg));
105 if (!error.ok()) {
106 self->Finish(std::move(error));
107 return;
108 }
109 absl::string_view response_body(self->response_.body,
110 self->response_.body_length);
111 if (self->response_.status != 200) {
112 self->Finish(absl::UnavailableError(
113 absl::StrCat("Call to HTTP server ended with status ",
114 self->response_.status, " [", response_body, "]")));
115 return;
116 }
117 self->Finish(std::string(response_body));
118 }
119
120 //
121 // ExternalAccountCredentials::ExternalFetchRequest
122 //
123
124 // The token fetching flow:
125 // 1. Retrieve subject token - Subclass's RetrieveSubjectToken() gets called
126 // and the subject token is received in ExchangeToken().
127 // 2. Exchange token - ExchangeToken() gets called with the
128 // subject token from #1.
129 // 3. (Optional) Impersonate service account - ImpersonateServiceAccount() gets
130 // called with the access token of the response from #2. Get an impersonated
131 // access token in OnImpersonateServiceAccountInternal().
132 // 4. Finish token fetch - Return back the response that contains an access
133 // token in FinishTokenFetch().
ExternalFetchRequest(ExternalAccountCredentials * creds,Timestamp deadline,absl::AnyInvocable<void (absl::StatusOr<RefCountedPtr<TokenFetcherCredentials::Token>>)> on_done)134 ExternalAccountCredentials::ExternalFetchRequest::ExternalFetchRequest(
135 ExternalAccountCredentials* creds, Timestamp deadline,
136 absl::AnyInvocable<
137 void(absl::StatusOr<RefCountedPtr<TokenFetcherCredentials::Token>>)>
138 on_done)
139 : creds_(creds), deadline_(deadline), on_done_(std::move(on_done)) {
140 fetch_body_ = creds_->RetrieveSubjectToken(
141 deadline, [self = RefAsSubclass<ExternalFetchRequest>()](
142 absl::StatusOr<std::string> result) {
143 self->ExchangeToken(std::move(result));
144 });
145 }
146
Orphan()147 void ExternalAccountCredentials::ExternalFetchRequest::Orphan() {
148 {
149 MutexLock lock(&mu_);
150 fetch_body_.reset();
151 }
152 Unref();
153 }
154
155 namespace {
156
UrlEncode(const absl::string_view s)157 std::string UrlEncode(const absl::string_view s) {
158 const char* hex = "0123456789ABCDEF";
159 std::string result;
160 result.reserve(s.length());
161 for (auto c : s) {
162 if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
163 (c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
164 c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
165 result.push_back(c);
166 } else {
167 result.push_back('%');
168 result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
169 result.push_back(hex[static_cast<unsigned char>(c) & 15]);
170 }
171 }
172 return result;
173 }
174
175 } // namespace
176
ExchangeToken(absl::StatusOr<std::string> subject_token)177 void ExternalAccountCredentials::ExternalFetchRequest::ExchangeToken(
178 absl::StatusOr<std::string> subject_token) {
179 MutexLock lock(&mu_);
180 if (MaybeFailLocked(subject_token.status())) return;
181 // Parse URI.
182 absl::StatusOr<URI> uri = URI::Parse(options().token_url);
183 if (!uri.ok()) {
184 return FinishTokenFetch(GRPC_ERROR_CREATE(
185 absl::StrFormat("Invalid token url: %s. Error: %s", options().token_url,
186 uri.status().ToString())));
187 }
188 // Start HTTP request.
189 fetch_body_ = MakeOrphanable<HttpFetchBody>(
190 [&](grpc_http_response* response, grpc_closure* on_http_response) {
191 grpc_http_request request;
192 memset(&request, 0, sizeof(grpc_http_request));
193 const bool add_authorization_header =
194 !options().client_id.empty() && !options().client_secret.empty();
195 request.hdr_count = add_authorization_header ? 3 : 2;
196 auto* headers = static_cast<grpc_http_header*>(
197 gpr_malloc(sizeof(grpc_http_header) * request.hdr_count));
198 headers[0].key = gpr_strdup("Content-Type");
199 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
200 headers[1].key = gpr_strdup("x-goog-api-client");
201 headers[1].value = gpr_strdup(creds_->MetricsHeaderValue().c_str());
202 if (add_authorization_header) {
203 std::string raw_cred = absl::StrFormat("%s:%s", options().client_id,
204 options().client_secret);
205 std::string str =
206 absl::StrFormat("Basic %s", absl::Base64Escape(raw_cred));
207 headers[2].key = gpr_strdup("Authorization");
208 headers[2].value = gpr_strdup(str.c_str());
209 }
210 request.hdrs = headers;
211 std::vector<std::string> body_parts;
212 body_parts.push_back(absl::StrFormat(
213 "audience=%s", UrlEncode(options().audience).c_str()));
214 body_parts.push_back(absl::StrFormat(
215 "grant_type=%s",
216 UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE).c_str()));
217 body_parts.push_back(absl::StrFormat(
218 "requested_token_type=%s",
219 UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE)
220 .c_str()));
221 body_parts.push_back(
222 absl::StrFormat("subject_token_type=%s",
223 UrlEncode(options().subject_token_type).c_str()));
224 body_parts.push_back(absl::StrFormat(
225 "subject_token=%s", UrlEncode(*subject_token).c_str()));
226 std::string scope = GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE;
227 if (options().service_account_impersonation_url.empty()) {
228 scope = absl::StrJoin(creds_->scopes_, " ");
229 }
230 body_parts.push_back(
231 absl::StrFormat("scope=%s", UrlEncode(scope).c_str()));
232 Json::Object additional_options_json_object;
233 if (options().client_id.empty() && options().client_secret.empty()) {
234 additional_options_json_object["userProject"] =
235 Json::FromString(options().workforce_pool_user_project);
236 }
237 Json additional_options_json =
238 Json::FromObject(std::move(additional_options_json_object));
239 body_parts.push_back(absl::StrFormat(
240 "options=%s",
241 UrlEncode(JsonDump(additional_options_json)).c_str()));
242 std::string body = absl::StrJoin(body_parts, "&");
243 request.body = const_cast<char*>(body.c_str());
244 request.body_length = body.size();
245 RefCountedPtr<grpc_channel_credentials> http_request_creds;
246 if (uri->scheme() == "http") {
247 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
248 grpc_insecure_credentials_create());
249 } else {
250 http_request_creds = CreateHttpRequestSSLCredentials();
251 }
252 auto http_request = HttpRequest::Post(
253 std::move(*uri), /*args=*/nullptr, pollent(), &request, deadline(),
254 on_http_response, response, std::move(http_request_creds));
255 http_request->Start();
256 request.body = nullptr;
257 grpc_http_request_destroy(&request);
258 return http_request;
259 },
260 [self = RefAsSubclass<ExternalFetchRequest>()](
261 absl::StatusOr<std::string> result) {
262 self->MaybeImpersonateServiceAccount(std::move(result));
263 });
264 }
265
266 void ExternalAccountCredentials::ExternalFetchRequest::
MaybeImpersonateServiceAccount(absl::StatusOr<std::string> response_body)267 MaybeImpersonateServiceAccount(absl::StatusOr<std::string> response_body) {
268 MutexLock lock(&mu_);
269 if (MaybeFailLocked(response_body.status())) return;
270 // If not doing impersonation, response_body contains oauth token.
271 if (options().service_account_impersonation_url.empty()) {
272 return FinishTokenFetch(std::move(response_body));
273 }
274 // Do impersonation.
275 auto json = JsonParse(*response_body);
276 if (!json.ok()) {
277 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrCat(
278 "Invalid token exchange response: ", json.status().ToString())));
279 return;
280 }
281 if (json->type() != Json::Type::kObject) {
282 FinishTokenFetch(GRPC_ERROR_CREATE(
283 "Invalid token exchange response: JSON type is not object"));
284 return;
285 }
286 auto it = json->object().find("access_token");
287 if (it == json->object().end() || it->second.type() != Json::Type::kString) {
288 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
289 "Missing or invalid access_token in %s.", *response_body)));
290 return;
291 }
292 absl::string_view access_token = it->second.string();
293 absl::StatusOr<URI> uri =
294 URI::Parse(options().service_account_impersonation_url);
295 if (!uri.ok()) {
296 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
297 "Invalid service account impersonation url: %s. Error: %s",
298 options().service_account_impersonation_url, uri.status().ToString())));
299 return;
300 }
301 // Start HTTP request.
302 fetch_body_ = MakeOrphanable<HttpFetchBody>(
303 [&](grpc_http_response* response, grpc_closure* on_http_response) {
304 grpc_http_request request;
305 memset(&request, 0, sizeof(grpc_http_request));
306 request.hdr_count = 2;
307 grpc_http_header* headers = static_cast<grpc_http_header*>(
308 gpr_malloc(sizeof(grpc_http_header) * request.hdr_count));
309 headers[0].key = gpr_strdup("Content-Type");
310 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
311 std::string str = absl::StrFormat("Bearer %s", access_token);
312 headers[1].key = gpr_strdup("Authorization");
313 headers[1].value = gpr_strdup(str.c_str());
314 request.hdrs = headers;
315 std::vector<std::string> body_members;
316 std::string scope = absl::StrJoin(creds_->scopes_, " ");
317 body_members.push_back(
318 absl::StrFormat("scope=%s", UrlEncode(scope).c_str()));
319 body_members.push_back(absl::StrFormat(
320 "lifetime=%ds",
321 options().service_account_impersonation.token_lifetime_seconds));
322 std::string body = absl::StrJoin(body_members, "&");
323 request.body = const_cast<char*>(body.c_str());
324 request.body_length = body.size();
325 // TODO(ctiller): Use the callers resource quota.
326 RefCountedPtr<grpc_channel_credentials> http_request_creds;
327 if (uri->scheme() == "http") {
328 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
329 grpc_insecure_credentials_create());
330 } else {
331 http_request_creds = CreateHttpRequestSSLCredentials();
332 }
333 auto http_request = HttpRequest::Post(
334 std::move(*uri), nullptr, pollent(), &request, deadline(),
335 on_http_response, response, std::move(http_request_creds));
336 http_request->Start();
337 request.body = nullptr;
338 grpc_http_request_destroy(&request);
339 return http_request;
340 },
341 [self = RefAsSubclass<ExternalFetchRequest>()](
342 absl::StatusOr<std::string> result) {
343 self->OnImpersonateServiceAccount(std::move(result));
344 });
345 }
346
347 void ExternalAccountCredentials::ExternalFetchRequest::
OnImpersonateServiceAccount(absl::StatusOr<std::string> response_body)348 OnImpersonateServiceAccount(absl::StatusOr<std::string> response_body) {
349 MutexLock lock(&mu_);
350 if (MaybeFailLocked(response_body.status())) return;
351 auto json = JsonParse(*response_body);
352 if (!json.ok()) {
353 FinishTokenFetch(GRPC_ERROR_CREATE(
354 absl::StrCat("Invalid service account impersonation response: ",
355 json.status().ToString())));
356 return;
357 }
358 if (json->type() != Json::Type::kObject) {
359 FinishTokenFetch(
360 GRPC_ERROR_CREATE("Invalid service account impersonation response: "
361 "JSON type is not object"));
362 return;
363 }
364 auto it = json->object().find("accessToken");
365 if (it == json->object().end() || it->second.type() != Json::Type::kString) {
366 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
367 "Missing or invalid accessToken in %s.", *response_body)));
368 return;
369 }
370 absl::string_view access_token = it->second.string();
371 it = json->object().find("expireTime");
372 if (it == json->object().end() || it->second.type() != Json::Type::kString) {
373 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
374 "Missing or invalid expireTime in %s.", *response_body)));
375 return;
376 }
377 absl::string_view expire_time = it->second.string();
378 absl::Time t;
379 if (!absl::ParseTime(absl::RFC3339_full, expire_time, &t, nullptr)) {
380 FinishTokenFetch(GRPC_ERROR_CREATE(
381 "Invalid expire time of service account impersonation response."));
382 return;
383 }
384 int64_t expire_in = (t - absl::Now()) / absl::Seconds(1);
385 std::string body = absl::StrFormat(
386 "{\"access_token\":\"%s\",\"expires_in\":%d,\"token_type\":\"Bearer\"}",
387 access_token, expire_in);
388 FinishTokenFetch(std::move(body));
389 }
390
FinishTokenFetch(absl::StatusOr<std::string> response_body)391 void ExternalAccountCredentials::ExternalFetchRequest::FinishTokenFetch(
392 absl::StatusOr<std::string> response_body) {
393 absl::StatusOr<RefCountedPtr<Token>> result;
394 if (!response_body.ok()) {
395 LOG(ERROR) << "Fetch external account credentials access token: "
396 << response_body.status();
397 result = absl::Status(response_body.status().code(),
398 absl::StrCat("error fetching oauth2 token: ",
399 response_body.status().message()));
400 } else {
401 absl::optional<Slice> token_value;
402 Duration token_lifetime;
403 if (grpc_oauth2_token_fetcher_credentials_parse_server_response_body(
404 *response_body, &token_value, &token_lifetime) !=
405 GRPC_CREDENTIALS_OK) {
406 result = GRPC_ERROR_CREATE("Could not parse oauth token");
407 } else {
408 result = MakeRefCounted<Token>(std::move(*token_value),
409 Timestamp::Now() + token_lifetime);
410 }
411 }
412 creds_->event_engine().Run([on_done = std::exchange(on_done_, nullptr),
413 result = std::move(result)]() mutable {
414 ApplicationCallbackExecCtx application_exec_ctx;
415 ExecCtx exec_ctx;
416 std::exchange(on_done, nullptr)(std::move(result));
417 });
418 }
419
MaybeFailLocked(absl::Status status)420 bool ExternalAccountCredentials::ExternalFetchRequest::MaybeFailLocked(
421 absl::Status status) {
422 if (!status.ok()) {
423 FinishTokenFetch(std::move(status));
424 return true;
425 }
426 if (fetch_body_ == nullptr) { // Will be set by Orphan() on cancellation.
427 FinishTokenFetch(
428 absl::CancelledError("external account credentials fetch cancelled"));
429 return true;
430 }
431 return false;
432 }
433
434 //
435 // ExternalAccountCredentials
436 //
437
438 namespace {
439
440 // Expression to match:
441 // //iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+
MatchWorkforcePoolAudience(absl::string_view audience)442 bool MatchWorkforcePoolAudience(absl::string_view audience) {
443 // Match "//iam.googleapis.com/locations/"
444 if (!absl::ConsumePrefix(&audience, "//iam.googleapis.com")) return false;
445 if (!absl::ConsumePrefix(&audience, "/locations/")) return false;
446 // Match "[^/]+/workforcePools/"
447 std::pair<absl::string_view, absl::string_view> workforce_pools_split_result =
448 absl::StrSplit(audience, absl::MaxSplits("/workforcePools/", 1));
449 if (absl::StrContains(workforce_pools_split_result.first, '/')) return false;
450 // Match "[^/]+/providers/.+"
451 std::pair<absl::string_view, absl::string_view> providers_split_result =
452 absl::StrSplit(workforce_pools_split_result.second,
453 absl::MaxSplits("/providers/", 1));
454 return !absl::StrContains(providers_split_result.first, '/');
455 }
456
457 } // namespace
458
459 absl::StatusOr<RefCountedPtr<ExternalAccountCredentials>>
Create(const Json & json,std::vector<std::string> scopes,std::shared_ptr<grpc_event_engine::experimental::EventEngine> event_engine)460 ExternalAccountCredentials::Create(
461 const Json& json, std::vector<std::string> scopes,
462 std::shared_ptr<grpc_event_engine::experimental::EventEngine>
463 event_engine) {
464 Options options;
465 options.type = GRPC_AUTH_JSON_TYPE_INVALID;
466 if (json.type() != Json::Type::kObject) {
467 return GRPC_ERROR_CREATE("Invalid json to construct credentials options.");
468 }
469 auto it = json.object().find("type");
470 if (it == json.object().end()) {
471 return GRPC_ERROR_CREATE("type field not present.");
472 }
473 if (it->second.type() != Json::Type::kString) {
474 return GRPC_ERROR_CREATE("type field must be a string.");
475 }
476 if (it->second.string() != GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT) {
477 return GRPC_ERROR_CREATE("Invalid credentials json type.");
478 }
479 options.type = GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT;
480 it = json.object().find("audience");
481 if (it == json.object().end()) {
482 return GRPC_ERROR_CREATE("audience field not present.");
483 }
484 if (it->second.type() != Json::Type::kString) {
485 return GRPC_ERROR_CREATE("audience field must be a string.");
486 }
487 options.audience = it->second.string();
488 it = json.object().find("subject_token_type");
489 if (it == json.object().end()) {
490 return GRPC_ERROR_CREATE("subject_token_type field not present.");
491 }
492 if (it->second.type() != Json::Type::kString) {
493 return GRPC_ERROR_CREATE("subject_token_type field must be a string.");
494 }
495 options.subject_token_type = it->second.string();
496 it = json.object().find("service_account_impersonation_url");
497 if (it != json.object().end()) {
498 options.service_account_impersonation_url = it->second.string();
499 }
500 it = json.object().find("token_url");
501 if (it == json.object().end()) {
502 return GRPC_ERROR_CREATE("token_url field not present.");
503 }
504 if (it->second.type() != Json::Type::kString) {
505 return GRPC_ERROR_CREATE("token_url field must be a string.");
506 }
507 options.token_url = it->second.string();
508 it = json.object().find("token_info_url");
509 if (it != json.object().end()) {
510 options.token_info_url = it->second.string();
511 }
512 it = json.object().find("credential_source");
513 if (it == json.object().end()) {
514 return GRPC_ERROR_CREATE("credential_source field not present.");
515 }
516 options.credential_source = it->second;
517 it = json.object().find("quota_project_id");
518 if (it != json.object().end()) {
519 options.quota_project_id = it->second.string();
520 }
521 it = json.object().find("client_id");
522 if (it != json.object().end()) {
523 options.client_id = it->second.string();
524 }
525 it = json.object().find("client_secret");
526 if (it != json.object().end()) {
527 options.client_secret = it->second.string();
528 }
529 it = json.object().find("workforce_pool_user_project");
530 if (it != json.object().end()) {
531 if (MatchWorkforcePoolAudience(options.audience)) {
532 options.workforce_pool_user_project = it->second.string();
533 } else {
534 return GRPC_ERROR_CREATE(
535 "workforce_pool_user_project should not be set for non-workforce "
536 "pool credentials");
537 }
538 }
539 it = json.object().find("service_account_impersonation");
540 options.service_account_impersonation.token_lifetime_seconds =
541 IMPERSONATED_CRED_DEFAULT_LIFETIME_IN_SECONDS;
542 if (it != json.object().end() && it->second.type() == Json::Type::kObject) {
543 auto service_acc_imp_json = it->second;
544 auto service_acc_imp_obj_it =
545 service_acc_imp_json.object().find("token_lifetime_seconds");
546 if (service_acc_imp_obj_it != service_acc_imp_json.object().end()) {
547 if (!absl::SimpleAtoi(
548 service_acc_imp_obj_it->second.string(),
549 &options.service_account_impersonation.token_lifetime_seconds)) {
550 return GRPC_ERROR_CREATE("token_lifetime_seconds must be a number");
551 }
552 if (options.service_account_impersonation.token_lifetime_seconds >
553 IMPERSONATED_CRED_MAX_LIFETIME_IN_SECONDS) {
554 return GRPC_ERROR_CREATE(
555 absl::StrFormat("token_lifetime_seconds must be less than %ds",
556 IMPERSONATED_CRED_MAX_LIFETIME_IN_SECONDS));
557 }
558 if (options.service_account_impersonation.token_lifetime_seconds <
559 IMPERSONATED_CRED_MIN_LIFETIME_IN_SECONDS) {
560 return GRPC_ERROR_CREATE(
561 absl::StrFormat("token_lifetime_seconds must be more than %ds",
562 IMPERSONATED_CRED_MIN_LIFETIME_IN_SECONDS));
563 }
564 }
565 }
566 RefCountedPtr<ExternalAccountCredentials> creds;
567 grpc_error_handle error;
568 if (options.credential_source.object().find("environment_id") !=
569 options.credential_source.object().end()) {
570 creds = MakeRefCounted<AwsExternalAccountCredentials>(
571 std::move(options), std::move(scopes), std::move(event_engine), &error);
572 } else if (options.credential_source.object().find("file") !=
573 options.credential_source.object().end()) {
574 creds = MakeRefCounted<FileExternalAccountCredentials>(
575 std::move(options), std::move(scopes), std::move(event_engine), &error);
576 } else if (options.credential_source.object().find("url") !=
577 options.credential_source.object().end()) {
578 creds = MakeRefCounted<UrlExternalAccountCredentials>(
579 std::move(options), std::move(scopes), std::move(event_engine), &error);
580 } else {
581 return GRPC_ERROR_CREATE(
582 "Invalid options credential source to create "
583 "ExternalAccountCredentials.");
584 }
585 if (!error.ok()) return error;
586 return creds;
587 }
588
ExternalAccountCredentials(Options options,std::vector<std::string> scopes,std::shared_ptr<grpc_event_engine::experimental::EventEngine> event_engine)589 ExternalAccountCredentials::ExternalAccountCredentials(
590 Options options, std::vector<std::string> scopes,
591 std::shared_ptr<grpc_event_engine::experimental::EventEngine> event_engine)
592 : TokenFetcherCredentials(std::move(event_engine)),
593 options_(std::move(options)) {
594 if (scopes.empty()) {
595 scopes.push_back(GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE);
596 }
597 scopes_ = std::move(scopes);
598 }
599
~ExternalAccountCredentials()600 ExternalAccountCredentials::~ExternalAccountCredentials() {}
601
MetricsHeaderValue()602 std::string ExternalAccountCredentials::MetricsHeaderValue() {
603 return absl::StrFormat(
604 "gl-cpp/unknown auth/%s google-byoid-sdk source/%s sa-impersonation/%v "
605 "config-lifetime/%v",
606 grpc_version_string(), CredentialSourceType(),
607 !options_.service_account_impersonation_url.empty(),
608 options_.service_account_impersonation.token_lifetime_seconds !=
609 IMPERSONATED_CRED_DEFAULT_LIFETIME_IN_SECONDS);
610 }
611
CredentialSourceType()612 absl::string_view ExternalAccountCredentials::CredentialSourceType() {
613 return "unknown";
614 }
615
616 OrphanablePtr<ExternalAccountCredentials::FetchRequest>
FetchToken(Timestamp deadline,absl::AnyInvocable<void (absl::StatusOr<RefCountedPtr<Token>>)> on_done)617 ExternalAccountCredentials::FetchToken(
618 Timestamp deadline,
619 absl::AnyInvocable<void(absl::StatusOr<RefCountedPtr<Token>>)> on_done) {
620 return MakeOrphanable<ExternalFetchRequest>(this, deadline,
621 std::move(on_done));
622 }
623
624 } // namespace grpc_core
625
grpc_external_account_credentials_create(const char * json_string,const char * scopes_string)626 grpc_call_credentials* grpc_external_account_credentials_create(
627 const char* json_string, const char* scopes_string) {
628 auto json = grpc_core::JsonParse(json_string);
629 if (!json.ok()) {
630 LOG(ERROR) << "External account credentials creation failed. Error: "
631 << json.status();
632 return nullptr;
633 }
634 std::vector<std::string> scopes = absl::StrSplit(scopes_string, ',');
635 auto creds =
636 grpc_core::ExternalAccountCredentials::Create(*json, std::move(scopes));
637 if (!creds.ok()) {
638 LOG(ERROR) << "External account credentials creation failed. Error: "
639 << grpc_core::StatusToString(creds.status());
640 return nullptr;
641 }
642 return creds->release();
643 }
644