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