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 <grpc/support/port_platform.h>
17
18 #include "src/core/lib/security/credentials/external/external_account_credentials.h"
19
20 #include <stdint.h>
21 #include <string.h>
22
23 #include <algorithm>
24 #include <initializer_list>
25 #include <map>
26 #include <memory>
27 #include <utility>
28
29 #include "absl/status/status.h"
30 #include "absl/status/statusor.h"
31 #include "absl/strings/match.h"
32 #include "absl/strings/str_cat.h"
33 #include "absl/strings/str_format.h"
34 #include "absl/strings/str_join.h"
35 #include "absl/strings/str_split.h"
36 #include "absl/strings/strip.h"
37 #include "absl/time/clock.h"
38 #include "absl/time/time.h"
39
40 #include <grpc/grpc.h>
41 #include <grpc/grpc_security.h>
42 #include <grpc/support/alloc.h>
43 #include <grpc/support/json.h>
44 #include <grpc/support/log.h>
45 #include <grpc/support/string_util.h>
46
47 #include "src/core/lib/gprpp/status_helper.h"
48 #include "src/core/lib/http/httpcli_ssl_credentials.h"
49 #include "src/core/lib/http/parser.h"
50 #include "src/core/lib/json/json_reader.h"
51 #include "src/core/lib/json/json_writer.h"
52 #include "src/core/lib/security/credentials/credentials.h"
53 #include "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
54 #include "src/core/lib/security/credentials/external/file_external_account_credentials.h"
55 #include "src/core/lib/security/credentials/external/url_external_account_credentials.h"
56 #include "src/core/lib/security/util/json_util.h"
57 #include "src/core/lib/slice/b64.h"
58 #include "src/core/lib/uri/uri_parser.h"
59
60 #define EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE \
61 "urn:ietf:params:oauth:grant-type:token-exchange"
62 #define EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE \
63 "urn:ietf:params:oauth:token-type:access_token"
64 #define GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE \
65 "https://www.googleapis.com/auth/cloud-platform"
66
67 namespace grpc_core {
68
69 namespace {
70
UrlEncode(const absl::string_view & s)71 std::string UrlEncode(const absl::string_view& s) {
72 const char* hex = "0123456789ABCDEF";
73 std::string result;
74 result.reserve(s.length());
75 for (auto c : s) {
76 if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
77 (c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
78 c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
79 result.push_back(c);
80 } else {
81 result.push_back('%');
82 result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
83 result.push_back(hex[static_cast<unsigned char>(c) & 15]);
84 }
85 }
86 return result;
87 }
88
89 // Expression to match:
90 // //iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+
MatchWorkforcePoolAudience(absl::string_view audience)91 bool MatchWorkforcePoolAudience(absl::string_view audience) {
92 // Match "//iam.googleapis.com/locations/"
93 if (!absl::ConsumePrefix(&audience, "//iam.googleapis.com")) return false;
94 if (!absl::ConsumePrefix(&audience, "/locations/")) return false;
95 // Match "[^/]+/workforcePools/"
96 std::pair<absl::string_view, absl::string_view> workforce_pools_split_result =
97 absl::StrSplit(audience, absl::MaxSplits("/workforcePools/", 1));
98 if (absl::StrContains(workforce_pools_split_result.first, '/')) return false;
99 // Match "[^/]+/providers/.+"
100 std::pair<absl::string_view, absl::string_view> providers_split_result =
101 absl::StrSplit(workforce_pools_split_result.second,
102 absl::MaxSplits("/providers/", 1));
103 return !absl::StrContains(providers_split_result.first, '/');
104 }
105
106 } // namespace
107
Create(const Json & json,std::vector<std::string> scopes,grpc_error_handle * error)108 RefCountedPtr<ExternalAccountCredentials> ExternalAccountCredentials::Create(
109 const Json& json, std::vector<std::string> scopes,
110 grpc_error_handle* error) {
111 GPR_ASSERT(error->ok());
112 Options options;
113 options.type = GRPC_AUTH_JSON_TYPE_INVALID;
114 if (json.type() != Json::Type::kObject) {
115 *error =
116 GRPC_ERROR_CREATE("Invalid json to construct credentials options.");
117 return nullptr;
118 }
119 auto it = json.object().find("type");
120 if (it == json.object().end()) {
121 *error = GRPC_ERROR_CREATE("type field not present.");
122 return nullptr;
123 }
124 if (it->second.type() != Json::Type::kString) {
125 *error = GRPC_ERROR_CREATE("type field must be a string.");
126 return nullptr;
127 }
128 if (it->second.string() != GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT) {
129 *error = GRPC_ERROR_CREATE("Invalid credentials json type.");
130 return nullptr;
131 }
132 options.type = GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT;
133 it = json.object().find("audience");
134 if (it == json.object().end()) {
135 *error = GRPC_ERROR_CREATE("audience field not present.");
136 return nullptr;
137 }
138 if (it->second.type() != Json::Type::kString) {
139 *error = GRPC_ERROR_CREATE("audience field must be a string.");
140 return nullptr;
141 }
142 options.audience = it->second.string();
143 it = json.object().find("subject_token_type");
144 if (it == json.object().end()) {
145 *error = GRPC_ERROR_CREATE("subject_token_type field not present.");
146 return nullptr;
147 }
148 if (it->second.type() != Json::Type::kString) {
149 *error = GRPC_ERROR_CREATE("subject_token_type field must be a string.");
150 return nullptr;
151 }
152 options.subject_token_type = it->second.string();
153 it = json.object().find("service_account_impersonation_url");
154 if (it != json.object().end()) {
155 options.service_account_impersonation_url = it->second.string();
156 }
157 it = json.object().find("token_url");
158 if (it == json.object().end()) {
159 *error = GRPC_ERROR_CREATE("token_url field not present.");
160 return nullptr;
161 }
162 if (it->second.type() != Json::Type::kString) {
163 *error = GRPC_ERROR_CREATE("token_url field must be a string.");
164 return nullptr;
165 }
166 options.token_url = it->second.string();
167 it = json.object().find("token_info_url");
168 if (it != json.object().end()) {
169 options.token_info_url = it->second.string();
170 }
171 it = json.object().find("credential_source");
172 if (it == json.object().end()) {
173 *error = GRPC_ERROR_CREATE("credential_source field not present.");
174 return nullptr;
175 }
176 options.credential_source = it->second;
177 it = json.object().find("quota_project_id");
178 if (it != json.object().end()) {
179 options.quota_project_id = it->second.string();
180 }
181 it = json.object().find("client_id");
182 if (it != json.object().end()) {
183 options.client_id = it->second.string();
184 }
185 it = json.object().find("client_secret");
186 if (it != json.object().end()) {
187 options.client_secret = it->second.string();
188 }
189 it = json.object().find("workforce_pool_user_project");
190 if (it != json.object().end()) {
191 if (MatchWorkforcePoolAudience(options.audience)) {
192 options.workforce_pool_user_project = it->second.string();
193 } else {
194 *error = GRPC_ERROR_CREATE(
195 "workforce_pool_user_project should not be set for non-workforce "
196 "pool credentials");
197 return nullptr;
198 }
199 }
200 RefCountedPtr<ExternalAccountCredentials> creds;
201 if (options.credential_source.object().find("environment_id") !=
202 options.credential_source.object().end()) {
203 creds = MakeRefCounted<AwsExternalAccountCredentials>(
204 std::move(options), std::move(scopes), error);
205 } else if (options.credential_source.object().find("file") !=
206 options.credential_source.object().end()) {
207 creds = MakeRefCounted<FileExternalAccountCredentials>(
208 std::move(options), std::move(scopes), error);
209 } else if (options.credential_source.object().find("url") !=
210 options.credential_source.object().end()) {
211 creds = MakeRefCounted<UrlExternalAccountCredentials>(
212 std::move(options), std::move(scopes), error);
213 } else {
214 *error = GRPC_ERROR_CREATE(
215 "Invalid options credential source to create "
216 "ExternalAccountCredentials.");
217 }
218 if (error->ok()) {
219 return creds;
220 } else {
221 return nullptr;
222 }
223 }
224
ExternalAccountCredentials(Options options,std::vector<std::string> scopes)225 ExternalAccountCredentials::ExternalAccountCredentials(
226 Options options, std::vector<std::string> scopes)
227 : options_(std::move(options)) {
228 if (scopes.empty()) {
229 scopes.push_back(GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE);
230 }
231 scopes_ = std::move(scopes);
232 }
233
~ExternalAccountCredentials()234 ExternalAccountCredentials::~ExternalAccountCredentials() {}
235
debug_string()236 std::string ExternalAccountCredentials::debug_string() {
237 return absl::StrFormat("ExternalAccountCredentials{Audience:%s,%s}",
238 options_.audience,
239 grpc_oauth2_token_fetcher_credentials::debug_string());
240 }
241
242 // The token fetching flow:
243 // 1. Retrieve subject token - Subclass's RetrieveSubjectToken() gets called
244 // and the subject token is received in OnRetrieveSubjectTokenInternal().
245 // 2. Exchange token - ExchangeToken() gets called with the
246 // subject token from #1. Receive the response in OnExchangeTokenInternal().
247 // 3. (Optional) Impersonate service account - ImpersenateServiceAccount() gets
248 // called with the access token of the response from #2. Get an impersonated
249 // access token in OnImpersenateServiceAccountInternal().
250 // 4. Finish token fetch - Return back the response that contains an access
251 // token in FinishTokenFetch().
252 // TODO(chuanr): Avoid starting the remaining requests if the channel gets shut
253 // down.
fetch_oauth2(grpc_credentials_metadata_request * metadata_req,grpc_polling_entity * pollent,grpc_iomgr_cb_func response_cb,Timestamp deadline)254 void ExternalAccountCredentials::fetch_oauth2(
255 grpc_credentials_metadata_request* metadata_req,
256 grpc_polling_entity* pollent, grpc_iomgr_cb_func response_cb,
257 Timestamp deadline) {
258 GPR_ASSERT(ctx_ == nullptr);
259 ctx_ = new HTTPRequestContext(pollent, deadline);
260 metadata_req_ = metadata_req;
261 response_cb_ = response_cb;
262 auto cb = [this](std::string token, grpc_error_handle error) {
263 OnRetrieveSubjectTokenInternal(token, error);
264 };
265 RetrieveSubjectToken(ctx_, options_, cb);
266 }
267
OnRetrieveSubjectTokenInternal(absl::string_view subject_token,grpc_error_handle error)268 void ExternalAccountCredentials::OnRetrieveSubjectTokenInternal(
269 absl::string_view subject_token, grpc_error_handle error) {
270 if (!error.ok()) {
271 FinishTokenFetch(error);
272 } else {
273 ExchangeToken(subject_token);
274 }
275 }
276
ExchangeToken(absl::string_view subject_token)277 void ExternalAccountCredentials::ExchangeToken(
278 absl::string_view subject_token) {
279 absl::StatusOr<URI> uri = URI::Parse(options_.token_url);
280 if (!uri.ok()) {
281 FinishTokenFetch(GRPC_ERROR_CREATE(
282 absl::StrFormat("Invalid token url: %s. Error: %s", options_.token_url,
283 uri.status().ToString())));
284 return;
285 }
286 grpc_http_request request;
287 memset(&request, 0, sizeof(grpc_http_request));
288 grpc_http_header* headers = nullptr;
289 if (!options_.client_id.empty() && !options_.client_secret.empty()) {
290 request.hdr_count = 2;
291 headers = static_cast<grpc_http_header*>(
292 gpr_malloc(sizeof(grpc_http_header) * request.hdr_count));
293 headers[0].key = gpr_strdup("Content-Type");
294 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
295 std::string raw_cred =
296 absl::StrFormat("%s:%s", options_.client_id, options_.client_secret);
297 char* encoded_cred =
298 grpc_base64_encode(raw_cred.c_str(), raw_cred.length(), 0, 0);
299 std::string str = absl::StrFormat("Basic %s", std::string(encoded_cred));
300 headers[1].key = gpr_strdup("Authorization");
301 headers[1].value = gpr_strdup(str.c_str());
302 gpr_free(encoded_cred);
303 } else {
304 request.hdr_count = 1;
305 headers = static_cast<grpc_http_header*>(
306 gpr_malloc(sizeof(grpc_http_header) * request.hdr_count));
307 headers[0].key = gpr_strdup("Content-Type");
308 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
309 }
310 request.hdrs = headers;
311 std::vector<std::string> body_parts;
312 body_parts.push_back(
313 absl::StrFormat("audience=%s", UrlEncode(options_.audience).c_str()));
314 body_parts.push_back(absl::StrFormat(
315 "grant_type=%s",
316 UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE).c_str()));
317 body_parts.push_back(absl::StrFormat(
318 "requested_token_type=%s",
319 UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE).c_str()));
320 body_parts.push_back(absl::StrFormat(
321 "subject_token_type=%s", UrlEncode(options_.subject_token_type).c_str()));
322 body_parts.push_back(
323 absl::StrFormat("subject_token=%s", UrlEncode(subject_token).c_str()));
324 std::string scope = GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE;
325 if (options_.service_account_impersonation_url.empty()) {
326 scope = absl::StrJoin(scopes_, " ");
327 }
328 body_parts.push_back(absl::StrFormat("scope=%s", UrlEncode(scope).c_str()));
329 Json::Object addtional_options_json_object;
330 if (options_.client_id.empty() && options_.client_secret.empty()) {
331 addtional_options_json_object["userProject"] =
332 Json::FromString(options_.workforce_pool_user_project);
333 }
334 Json addtional_options_json =
335 Json::FromObject(std::move(addtional_options_json_object));
336 body_parts.push_back(absl::StrFormat(
337 "options=%s", UrlEncode(JsonDump(addtional_options_json)).c_str()));
338 std::string body = absl::StrJoin(body_parts, "&");
339 request.body = const_cast<char*>(body.c_str());
340 request.body_length = body.size();
341 grpc_http_response_destroy(&ctx_->response);
342 ctx_->response = {};
343 GRPC_CLOSURE_INIT(&ctx_->closure, OnExchangeToken, this, nullptr);
344 GPR_ASSERT(http_request_ == nullptr);
345 RefCountedPtr<grpc_channel_credentials> http_request_creds;
346 if (uri->scheme() == "http") {
347 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
348 grpc_insecure_credentials_create());
349 } else {
350 http_request_creds = CreateHttpRequestSSLCredentials();
351 }
352 http_request_ =
353 HttpRequest::Post(std::move(*uri), nullptr /* channel args */,
354 ctx_->pollent, &request, ctx_->deadline, &ctx_->closure,
355 &ctx_->response, std::move(http_request_creds));
356 http_request_->Start();
357 request.body = nullptr;
358 grpc_http_request_destroy(&request);
359 }
360
OnExchangeToken(void * arg,grpc_error_handle error)361 void ExternalAccountCredentials::OnExchangeToken(void* arg,
362 grpc_error_handle error) {
363 ExternalAccountCredentials* self =
364 static_cast<ExternalAccountCredentials*>(arg);
365 self->OnExchangeTokenInternal(error);
366 }
367
OnExchangeTokenInternal(grpc_error_handle error)368 void ExternalAccountCredentials::OnExchangeTokenInternal(
369 grpc_error_handle error) {
370 http_request_.reset();
371 if (!error.ok()) {
372 FinishTokenFetch(error);
373 } else {
374 if (options_.service_account_impersonation_url.empty()) {
375 metadata_req_->response = ctx_->response;
376 metadata_req_->response.body = gpr_strdup(
377 std::string(ctx_->response.body, ctx_->response.body_length).c_str());
378 metadata_req_->response.hdrs = static_cast<grpc_http_header*>(
379 gpr_malloc(sizeof(grpc_http_header) * ctx_->response.hdr_count));
380 for (size_t i = 0; i < ctx_->response.hdr_count; i++) {
381 metadata_req_->response.hdrs[i].key =
382 gpr_strdup(ctx_->response.hdrs[i].key);
383 metadata_req_->response.hdrs[i].value =
384 gpr_strdup(ctx_->response.hdrs[i].value);
385 }
386 FinishTokenFetch(absl::OkStatus());
387 } else {
388 ImpersenateServiceAccount();
389 }
390 }
391 }
392
ImpersenateServiceAccount()393 void ExternalAccountCredentials::ImpersenateServiceAccount() {
394 absl::string_view response_body(ctx_->response.body,
395 ctx_->response.body_length);
396 auto json = JsonParse(response_body);
397 if (!json.ok()) {
398 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrCat(
399 "Invalid token exchange response: ", json.status().ToString())));
400 return;
401 }
402 if (json->type() != Json::Type::kObject) {
403 FinishTokenFetch(GRPC_ERROR_CREATE(
404 "Invalid token exchange response: JSON type is not object"));
405 return;
406 }
407 auto it = json->object().find("access_token");
408 if (it == json->object().end() || it->second.type() != Json::Type::kString) {
409 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
410 "Missing or invalid access_token in %s.", response_body)));
411 return;
412 }
413 std::string access_token = it->second.string();
414 absl::StatusOr<URI> uri =
415 URI::Parse(options_.service_account_impersonation_url);
416 if (!uri.ok()) {
417 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
418 "Invalid service account impersonation url: %s. Error: %s",
419 options_.service_account_impersonation_url, uri.status().ToString())));
420 return;
421 }
422 grpc_http_request request;
423 memset(&request, 0, sizeof(grpc_http_request));
424 request.hdr_count = 2;
425 grpc_http_header* headers = static_cast<grpc_http_header*>(
426 gpr_malloc(sizeof(grpc_http_header) * request.hdr_count));
427 headers[0].key = gpr_strdup("Content-Type");
428 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
429 std::string str = absl::StrFormat("Bearer %s", access_token);
430 headers[1].key = gpr_strdup("Authorization");
431 headers[1].value = gpr_strdup(str.c_str());
432 request.hdrs = headers;
433 std::string scope = absl::StrJoin(scopes_, " ");
434 std::string body = absl::StrFormat("scope=%s", scope);
435 request.body = const_cast<char*>(body.c_str());
436 request.body_length = body.size();
437 grpc_http_response_destroy(&ctx_->response);
438 ctx_->response = {};
439 GRPC_CLOSURE_INIT(&ctx_->closure, OnImpersenateServiceAccount, this, nullptr);
440 // TODO(ctiller): Use the callers resource quota.
441 GPR_ASSERT(http_request_ == nullptr);
442 RefCountedPtr<grpc_channel_credentials> http_request_creds;
443 if (uri->scheme() == "http") {
444 http_request_creds = RefCountedPtr<grpc_channel_credentials>(
445 grpc_insecure_credentials_create());
446 } else {
447 http_request_creds = CreateHttpRequestSSLCredentials();
448 }
449 http_request_ = HttpRequest::Post(
450 std::move(*uri), nullptr, ctx_->pollent, &request, ctx_->deadline,
451 &ctx_->closure, &ctx_->response, std::move(http_request_creds));
452 http_request_->Start();
453 request.body = nullptr;
454 grpc_http_request_destroy(&request);
455 }
456
OnImpersenateServiceAccount(void * arg,grpc_error_handle error)457 void ExternalAccountCredentials::OnImpersenateServiceAccount(
458 void* arg, grpc_error_handle error) {
459 ExternalAccountCredentials* self =
460 static_cast<ExternalAccountCredentials*>(arg);
461 self->OnImpersenateServiceAccountInternal(error);
462 }
463
OnImpersenateServiceAccountInternal(grpc_error_handle error)464 void ExternalAccountCredentials::OnImpersenateServiceAccountInternal(
465 grpc_error_handle error) {
466 http_request_.reset();
467 if (!error.ok()) {
468 FinishTokenFetch(error);
469 return;
470 }
471 absl::string_view response_body(ctx_->response.body,
472 ctx_->response.body_length);
473 auto json = JsonParse(response_body);
474 if (!json.ok()) {
475 FinishTokenFetch(GRPC_ERROR_CREATE(
476 absl::StrCat("Invalid service account impersonation response: ",
477 json.status().ToString())));
478 return;
479 }
480 if (json->type() != Json::Type::kObject) {
481 FinishTokenFetch(
482 GRPC_ERROR_CREATE("Invalid service account impersonation response: "
483 "JSON type is not object"));
484 return;
485 }
486 auto it = json->object().find("accessToken");
487 if (it == json->object().end() || it->second.type() != Json::Type::kString) {
488 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
489 "Missing or invalid accessToken in %s.", response_body)));
490 return;
491 }
492 std::string access_token = it->second.string();
493 it = json->object().find("expireTime");
494 if (it == json->object().end() || it->second.type() != Json::Type::kString) {
495 FinishTokenFetch(GRPC_ERROR_CREATE(absl::StrFormat(
496 "Missing or invalid expireTime in %s.", response_body)));
497 return;
498 }
499 std::string expire_time = it->second.string();
500 absl::Time t;
501 if (!absl::ParseTime(absl::RFC3339_full, expire_time, &t, nullptr)) {
502 FinishTokenFetch(GRPC_ERROR_CREATE(
503 "Invalid expire time of service account impersonation response."));
504 return;
505 }
506 int64_t expire_in = (t - absl::Now()) / absl::Seconds(1);
507 std::string body = absl::StrFormat(
508 "{\"access_token\":\"%s\",\"expires_in\":%d,\"token_type\":\"Bearer\"}",
509 access_token, expire_in);
510 metadata_req_->response = ctx_->response;
511 metadata_req_->response.body = gpr_strdup(body.c_str());
512 metadata_req_->response.body_length = body.length();
513 metadata_req_->response.hdrs = static_cast<grpc_http_header*>(
514 gpr_malloc(sizeof(grpc_http_header) * ctx_->response.hdr_count));
515 for (size_t i = 0; i < ctx_->response.hdr_count; i++) {
516 metadata_req_->response.hdrs[i].key =
517 gpr_strdup(ctx_->response.hdrs[i].key);
518 metadata_req_->response.hdrs[i].value =
519 gpr_strdup(ctx_->response.hdrs[i].value);
520 }
521 FinishTokenFetch(absl::OkStatus());
522 }
523
FinishTokenFetch(grpc_error_handle error)524 void ExternalAccountCredentials::FinishTokenFetch(grpc_error_handle error) {
525 GRPC_LOG_IF_ERROR("Fetch external account credentials access token", error);
526 // Move object state into local variables.
527 auto* cb = response_cb_;
528 response_cb_ = nullptr;
529 auto* metadata_req = metadata_req_;
530 metadata_req_ = nullptr;
531 auto* ctx = ctx_;
532 ctx_ = nullptr;
533 // Invoke the callback.
534 cb(metadata_req, error);
535 // Delete context.
536 delete ctx;
537 }
538
539 } // namespace grpc_core
540
grpc_external_account_credentials_create(const char * json_string,const char * scopes_string)541 grpc_call_credentials* grpc_external_account_credentials_create(
542 const char* json_string, const char* scopes_string) {
543 auto json = grpc_core::JsonParse(json_string);
544 if (!json.ok()) {
545 gpr_log(GPR_ERROR,
546 "External account credentials creation failed. Error: %s.",
547 json.status().ToString().c_str());
548 return nullptr;
549 }
550 std::vector<std::string> scopes = absl::StrSplit(scopes_string, ',');
551 grpc_error_handle error;
552 auto creds = grpc_core::ExternalAccountCredentials::Create(
553 *json, std::move(scopes), &error)
554 .release();
555 if (!error.ok()) {
556 gpr_log(GPR_ERROR,
557 "External account credentials creation failed. Error: %s.",
558 grpc_core::StatusToString(error).c_str());
559 return nullptr;
560 }
561 return creds;
562 }
563