1 //
2 // Copyright 2020 gRPC authors.
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16 #include <grpc/support/port_platform.h>
17
18 #include "src/core/lib/security/credentials/external/external_account_credentials.h"
19
20 #include "absl/strings/str_format.h"
21 #include "absl/strings/str_join.h"
22 #include "absl/strings/str_split.h"
23 #include "absl/time/clock.h"
24 #include "absl/time/time.h"
25
26 #include "src/core/lib/http/parser.h"
27 #include "src/core/lib/security/util/json_util.h"
28 #include "src/core/lib/slice/b64.h"
29
30 #include "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
31 #include "src/core/lib/security/credentials/external/file_external_account_credentials.h"
32 #include "src/core/lib/security/credentials/external/url_external_account_credentials.h"
33
34 #define EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE \
35 "urn:ietf:params:oauth:grant-type:token-exchange"
36 #define EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE \
37 "urn:ietf:params:oauth:token-type:access_token"
38 #define GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE \
39 "https://www.googleapis.com/auth/cloud-platform"
40
41 namespace grpc_core {
42
43 namespace {
44
UrlEncode(const absl::string_view & s)45 std::string UrlEncode(const absl::string_view& s) {
46 const char* hex = "0123456789ABCDEF";
47 std::string result;
48 result.reserve(s.length());
49 for (auto c : s) {
50 if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
51 (c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
52 c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
53 result.push_back(c);
54 } else {
55 result.push_back('%');
56 result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
57 result.push_back(hex[static_cast<unsigned char>(c) & 15]);
58 }
59 }
60 return result;
61 }
62
63 } // namespace
64
Create(const Json & json,std::vector<std::string> scopes,grpc_error_handle * error)65 RefCountedPtr<ExternalAccountCredentials> ExternalAccountCredentials::Create(
66 const Json& json, std::vector<std::string> scopes,
67 grpc_error_handle* error) {
68 GPR_ASSERT(*error == GRPC_ERROR_NONE);
69 Options options;
70 options.type = GRPC_AUTH_JSON_TYPE_INVALID;
71 if (json.type() != Json::Type::OBJECT) {
72 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
73 "Invalid json to construct credentials options.");
74 return nullptr;
75 }
76 auto it = json.object_value().find("type");
77 if (it == json.object_value().end()) {
78 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING("type field not present.");
79 return nullptr;
80 }
81 if (it->second.type() != Json::Type::STRING) {
82 *error =
83 GRPC_ERROR_CREATE_FROM_STATIC_STRING("type field must be a string.");
84 return nullptr;
85 }
86 if (it->second.string_value() != GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT) {
87 *error =
88 GRPC_ERROR_CREATE_FROM_STATIC_STRING("Invalid credentials json type.");
89 return nullptr;
90 }
91 options.type = GRPC_AUTH_JSON_TYPE_EXTERNAL_ACCOUNT;
92 it = json.object_value().find("audience");
93 if (it == json.object_value().end()) {
94 *error =
95 GRPC_ERROR_CREATE_FROM_STATIC_STRING("audience field not present.");
96 return nullptr;
97 }
98 if (it->second.type() != Json::Type::STRING) {
99 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
100 "audience field must be a string.");
101 return nullptr;
102 }
103 options.audience = it->second.string_value();
104 it = json.object_value().find("subject_token_type");
105 if (it == json.object_value().end()) {
106 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
107 "subject_token_type field not present.");
108 return nullptr;
109 }
110 if (it->second.type() != Json::Type::STRING) {
111 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
112 "subject_token_type field must be a string.");
113 return nullptr;
114 }
115 options.subject_token_type = it->second.string_value();
116 it = json.object_value().find("service_account_impersonation_url");
117 if (it != json.object_value().end()) {
118 options.service_account_impersonation_url = it->second.string_value();
119 }
120 it = json.object_value().find("token_url");
121 if (it == json.object_value().end()) {
122 *error =
123 GRPC_ERROR_CREATE_FROM_STATIC_STRING("token_url field not present.");
124 return nullptr;
125 }
126 if (it->second.type() != Json::Type::STRING) {
127 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
128 "token_url field must be a string.");
129 return nullptr;
130 }
131 options.token_url = it->second.string_value();
132 it = json.object_value().find("token_info_url");
133 if (it != json.object_value().end()) {
134 options.token_info_url = it->second.string_value();
135 }
136 it = json.object_value().find("credential_source");
137 if (it == json.object_value().end()) {
138 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
139 "credential_source field not present.");
140 return nullptr;
141 }
142 options.credential_source = it->second;
143 it = json.object_value().find("quota_project_id");
144 if (it != json.object_value().end()) {
145 options.quota_project_id = it->second.string_value();
146 }
147 it = json.object_value().find("client_id");
148 if (it != json.object_value().end()) {
149 options.client_id = it->second.string_value();
150 }
151 it = json.object_value().find("client_secret");
152 if (it != json.object_value().end()) {
153 options.client_secret = it->second.string_value();
154 }
155 RefCountedPtr<ExternalAccountCredentials> creds;
156 if (options.credential_source.object_value().find("environment_id") !=
157 options.credential_source.object_value().end()) {
158 creds = MakeRefCounted<AwsExternalAccountCredentials>(
159 std::move(options), std::move(scopes), error);
160 } else if (options.credential_source.object_value().find("file") !=
161 options.credential_source.object_value().end()) {
162 creds = MakeRefCounted<FileExternalAccountCredentials>(
163 std::move(options), std::move(scopes), error);
164 } else if (options.credential_source.object_value().find("url") !=
165 options.credential_source.object_value().end()) {
166 creds = MakeRefCounted<UrlExternalAccountCredentials>(
167 std::move(options), std::move(scopes), error);
168 } else {
169 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
170 "Invalid options credential source to create "
171 "ExternalAccountCredentials.");
172 }
173 if (*error == GRPC_ERROR_NONE) {
174 return creds;
175 } else {
176 return nullptr;
177 }
178 }
179
ExternalAccountCredentials(Options options,std::vector<std::string> scopes)180 ExternalAccountCredentials::ExternalAccountCredentials(
181 Options options, std::vector<std::string> scopes)
182 : options_(std::move(options)) {
183 if (scopes.empty()) {
184 scopes.push_back(GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE);
185 }
186 scopes_ = std::move(scopes);
187 }
188
~ExternalAccountCredentials()189 ExternalAccountCredentials::~ExternalAccountCredentials() {}
190
debug_string()191 std::string ExternalAccountCredentials::debug_string() {
192 return absl::StrFormat("ExternalAccountCredentials{Audience:%s,%s}",
193 options_.audience,
194 grpc_oauth2_token_fetcher_credentials::debug_string());
195 }
196
197 // The token fetching flow:
198 // 1. Retrieve subject token - Subclass's RetrieveSubjectToken() gets called
199 // and the subject token is received in OnRetrieveSubjectTokenInternal().
200 // 2. Exchange token - ExchangeToken() gets called with the
201 // subject token from #1. Receive the response in OnExchangeTokenInternal().
202 // 3. (Optional) Impersonate service account - ImpersenateServiceAccount() gets
203 // called with the access token of the response from #2. Get an impersonated
204 // access token in OnImpersenateServiceAccountInternal().
205 // 4. Finish token fetch - Return back the response that contains an access
206 // token in FinishTokenFetch().
207 // TODO(chuanr): Avoid starting the remaining requests if the channel gets shut
208 // down.
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)209 void ExternalAccountCredentials::fetch_oauth2(
210 grpc_credentials_metadata_request* metadata_req,
211 grpc_httpcli_context* httpcli_context, grpc_polling_entity* pollent,
212 grpc_iomgr_cb_func response_cb, grpc_millis deadline) {
213 GPR_ASSERT(ctx_ == nullptr);
214 ctx_ = new HTTPRequestContext(httpcli_context, pollent, deadline);
215 metadata_req_ = metadata_req;
216 response_cb_ = response_cb;
217 auto cb = [this](std::string token, grpc_error_handle error) {
218 OnRetrieveSubjectTokenInternal(token, error);
219 };
220 RetrieveSubjectToken(ctx_, options_, cb);
221 }
222
OnRetrieveSubjectTokenInternal(absl::string_view subject_token,grpc_error_handle error)223 void ExternalAccountCredentials::OnRetrieveSubjectTokenInternal(
224 absl::string_view subject_token, grpc_error_handle error) {
225 if (error != GRPC_ERROR_NONE) {
226 FinishTokenFetch(error);
227 } else {
228 ExchangeToken(subject_token);
229 }
230 }
231
ExchangeToken(absl::string_view subject_token)232 void ExternalAccountCredentials::ExchangeToken(
233 absl::string_view subject_token) {
234 absl::StatusOr<URI> uri = URI::Parse(options_.token_url);
235 if (!uri.ok()) {
236 FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
237 absl::StrFormat("Invalid token url: %s. Error: %s", options_.token_url,
238 uri.status().ToString())
239 .c_str()));
240 return;
241 }
242 grpc_httpcli_request request;
243 memset(&request, 0, sizeof(grpc_httpcli_request));
244 request.host = const_cast<char*>(uri->authority().c_str());
245 request.http.path = gpr_strdup(uri->path().c_str());
246 grpc_http_header* headers = nullptr;
247 if (!options_.client_id.empty() && !options_.client_secret.empty()) {
248 request.http.hdr_count = 2;
249 headers = static_cast<grpc_http_header*>(
250 gpr_malloc(sizeof(grpc_http_header) * request.http.hdr_count));
251 headers[0].key = gpr_strdup("Content-Type");
252 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
253 std::string raw_cred =
254 absl::StrFormat("%s:%s", options_.client_id, options_.client_secret);
255 char* encoded_cred =
256 grpc_base64_encode(raw_cred.c_str(), raw_cred.length(), 0, 0);
257 std::string str = absl::StrFormat("Basic %s", std::string(encoded_cred));
258 headers[1].key = gpr_strdup("Authorization");
259 headers[1].value = gpr_strdup(str.c_str());
260 gpr_free(encoded_cred);
261 } else {
262 request.http.hdr_count = 1;
263 headers = static_cast<grpc_http_header*>(
264 gpr_malloc(sizeof(grpc_http_header) * request.http.hdr_count));
265 headers[0].key = gpr_strdup("Content-Type");
266 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
267 }
268 request.http.hdrs = headers;
269 request.handshaker =
270 uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
271 std::vector<std::string> body_parts;
272 body_parts.push_back(absl::StrFormat("%s=%s", "audience",
273 UrlEncode(options_.audience).c_str()));
274 body_parts.push_back(absl::StrFormat(
275 "%s=%s", "grant_type",
276 UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE).c_str()));
277 body_parts.push_back(absl::StrFormat(
278 "%s=%s", "requested_token_type",
279 UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE).c_str()));
280 body_parts.push_back(
281 absl::StrFormat("%s=%s", "subject_token_type",
282 UrlEncode(options_.subject_token_type).c_str()));
283 body_parts.push_back(absl::StrFormat("%s=%s", "subject_token",
284 UrlEncode(subject_token).c_str()));
285 std::string scope = GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE;
286 if (options_.service_account_impersonation_url.empty()) {
287 scope = absl::StrJoin(scopes_, " ");
288 }
289 body_parts.push_back(
290 absl::StrFormat("%s=%s", "scope", UrlEncode(scope).c_str()));
291 std::string body = absl::StrJoin(body_parts, "&");
292 grpc_resource_quota* resource_quota =
293 grpc_resource_quota_create("external_account_credentials");
294 grpc_http_response_destroy(&ctx_->response);
295 ctx_->response = {};
296 GRPC_CLOSURE_INIT(&ctx_->closure, OnExchangeToken, this, nullptr);
297 grpc_httpcli_post(ctx_->httpcli_context, ctx_->pollent, resource_quota,
298 &request, body.c_str(), body.size(), ctx_->deadline,
299 &ctx_->closure, &ctx_->response);
300 grpc_resource_quota_unref_internal(resource_quota);
301 grpc_http_request_destroy(&request.http);
302 }
303
OnExchangeToken(void * arg,grpc_error_handle error)304 void ExternalAccountCredentials::OnExchangeToken(void* arg,
305 grpc_error_handle error) {
306 ExternalAccountCredentials* self =
307 static_cast<ExternalAccountCredentials*>(arg);
308 self->OnExchangeTokenInternal(GRPC_ERROR_REF(error));
309 }
310
OnExchangeTokenInternal(grpc_error_handle error)311 void ExternalAccountCredentials::OnExchangeTokenInternal(
312 grpc_error_handle error) {
313 if (error != GRPC_ERROR_NONE) {
314 FinishTokenFetch(error);
315 } else {
316 if (options_.service_account_impersonation_url.empty()) {
317 metadata_req_->response = ctx_->response;
318 metadata_req_->response.body = gpr_strdup(
319 std::string(ctx_->response.body, ctx_->response.body_length).c_str());
320 metadata_req_->response.hdrs = static_cast<grpc_http_header*>(
321 gpr_malloc(sizeof(grpc_http_header) * ctx_->response.hdr_count));
322 for (size_t i = 0; i < ctx_->response.hdr_count; i++) {
323 metadata_req_->response.hdrs[i].key =
324 gpr_strdup(ctx_->response.hdrs[i].key);
325 metadata_req_->response.hdrs[i].value =
326 gpr_strdup(ctx_->response.hdrs[i].value);
327 }
328 FinishTokenFetch(GRPC_ERROR_NONE);
329 } else {
330 ImpersenateServiceAccount();
331 }
332 }
333 }
334
ImpersenateServiceAccount()335 void ExternalAccountCredentials::ImpersenateServiceAccount() {
336 grpc_error_handle error = GRPC_ERROR_NONE;
337 absl::string_view response_body(ctx_->response.body,
338 ctx_->response.body_length);
339 Json json = Json::Parse(response_body, &error);
340 if (error != GRPC_ERROR_NONE || json.type() != Json::Type::OBJECT) {
341 FinishTokenFetch(GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
342 "Invalid token exchange response.", &error, 1));
343 GRPC_ERROR_UNREF(error);
344 return;
345 }
346 auto it = json.object_value().find("access_token");
347 if (it == json.object_value().end() ||
348 it->second.type() != Json::Type::STRING) {
349 FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
350 absl::StrFormat("Missing or invalid access_token in %s.", response_body)
351 .c_str()));
352 return;
353 }
354 std::string access_token = it->second.string_value();
355 absl::StatusOr<URI> uri =
356 URI::Parse(options_.service_account_impersonation_url);
357 if (!uri.ok()) {
358 FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
359 absl::StrFormat(
360 "Invalid service account impersonation url: %s. Error: %s",
361 options_.service_account_impersonation_url, uri.status().ToString())
362 .c_str()));
363 return;
364 }
365 grpc_httpcli_request request;
366 memset(&request, 0, sizeof(grpc_httpcli_request));
367 request.host = const_cast<char*>(uri->authority().c_str());
368 request.http.path = gpr_strdup(uri->path().c_str());
369 request.http.hdr_count = 2;
370 grpc_http_header* headers = static_cast<grpc_http_header*>(
371 gpr_malloc(sizeof(grpc_http_header) * request.http.hdr_count));
372 headers[0].key = gpr_strdup("Content-Type");
373 headers[0].value = gpr_strdup("application/x-www-form-urlencoded");
374 std::string str = absl::StrFormat("Bearer %s", access_token);
375 headers[1].key = gpr_strdup("Authorization");
376 headers[1].value = gpr_strdup(str.c_str());
377 request.http.hdrs = headers;
378 request.handshaker =
379 uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
380 std::string scope = absl::StrJoin(scopes_, " ");
381 std::string body = absl::StrFormat("%s=%s", "scope", scope);
382 grpc_resource_quota* resource_quota =
383 grpc_resource_quota_create("external_account_credentials");
384 grpc_http_response_destroy(&ctx_->response);
385 ctx_->response = {};
386 GRPC_CLOSURE_INIT(&ctx_->closure, OnImpersenateServiceAccount, this, nullptr);
387 grpc_httpcli_post(ctx_->httpcli_context, ctx_->pollent, resource_quota,
388 &request, body.c_str(), body.size(), ctx_->deadline,
389 &ctx_->closure, &ctx_->response);
390 grpc_resource_quota_unref_internal(resource_quota);
391 grpc_http_request_destroy(&request.http);
392 }
393
OnImpersenateServiceAccount(void * arg,grpc_error_handle error)394 void ExternalAccountCredentials::OnImpersenateServiceAccount(
395 void* arg, grpc_error_handle error) {
396 ExternalAccountCredentials* self =
397 static_cast<ExternalAccountCredentials*>(arg);
398 self->OnImpersenateServiceAccountInternal(GRPC_ERROR_REF(error));
399 }
400
OnImpersenateServiceAccountInternal(grpc_error_handle error)401 void ExternalAccountCredentials::OnImpersenateServiceAccountInternal(
402 grpc_error_handle error) {
403 if (error != GRPC_ERROR_NONE) {
404 FinishTokenFetch(error);
405 return;
406 }
407 absl::string_view response_body(ctx_->response.body,
408 ctx_->response.body_length);
409 Json json = Json::Parse(response_body, &error);
410 if (error != GRPC_ERROR_NONE || json.type() != Json::Type::OBJECT) {
411 FinishTokenFetch(GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
412 "Invalid service account impersonation response.", &error, 1));
413 GRPC_ERROR_UNREF(error);
414 return;
415 }
416 auto it = json.object_value().find("accessToken");
417 if (it == json.object_value().end() ||
418 it->second.type() != Json::Type::STRING) {
419 FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
420 absl::StrFormat("Missing or invalid accessToken in %s.", response_body)
421 .c_str()));
422 return;
423 }
424 std::string access_token = it->second.string_value();
425 it = json.object_value().find("expireTime");
426 if (it == json.object_value().end() ||
427 it->second.type() != Json::Type::STRING) {
428 FinishTokenFetch(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
429 absl::StrFormat("Missing or invalid expireTime in %s.", response_body)
430 .c_str()));
431 return;
432 }
433 std::string expire_time = it->second.string_value();
434 absl::Time t;
435 if (!absl::ParseTime(absl::RFC3339_full, expire_time, &t, nullptr)) {
436 FinishTokenFetch(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
437 "Invalid expire time of service account impersonation response."));
438 return;
439 }
440 int expire_in = (t - absl::Now()) / absl::Seconds(1);
441 std::string body = absl::StrFormat(
442 "{\"access_token\":\"%s\",\"expires_in\":%d,\"token_type\":\"Bearer\"}",
443 access_token, expire_in);
444 metadata_req_->response = ctx_->response;
445 metadata_req_->response.body = gpr_strdup(body.c_str());
446 metadata_req_->response.body_length = body.length();
447 metadata_req_->response.hdrs = static_cast<grpc_http_header*>(
448 gpr_malloc(sizeof(grpc_http_header) * ctx_->response.hdr_count));
449 for (size_t i = 0; i < ctx_->response.hdr_count; i++) {
450 metadata_req_->response.hdrs[i].key =
451 gpr_strdup(ctx_->response.hdrs[i].key);
452 metadata_req_->response.hdrs[i].value =
453 gpr_strdup(ctx_->response.hdrs[i].value);
454 }
455 FinishTokenFetch(GRPC_ERROR_NONE);
456 }
457
FinishTokenFetch(grpc_error_handle error)458 void ExternalAccountCredentials::FinishTokenFetch(grpc_error_handle error) {
459 GRPC_LOG_IF_ERROR("Fetch external account credentials access token",
460 GRPC_ERROR_REF(error));
461 // Move object state into local variables.
462 auto* cb = response_cb_;
463 response_cb_ = nullptr;
464 auto* metadata_req = metadata_req_;
465 metadata_req_ = nullptr;
466 auto* ctx = ctx_;
467 ctx_ = nullptr;
468 // Invoke the callback.
469 cb(metadata_req, error);
470 // Delete context.
471 delete ctx;
472 GRPC_ERROR_UNREF(error);
473 }
474
475 } // namespace grpc_core
476
grpc_external_account_credentials_create(const char * json_string,const char * scopes_string)477 grpc_call_credentials* grpc_external_account_credentials_create(
478 const char* json_string, const char* scopes_string) {
479 grpc_error_handle error = GRPC_ERROR_NONE;
480 grpc_core::Json json = grpc_core::Json::Parse(json_string, &error);
481 if (error != GRPC_ERROR_NONE) {
482 gpr_log(GPR_ERROR,
483 "External account credentials creation failed. Error: %s.",
484 grpc_error_std_string(error).c_str());
485 GRPC_ERROR_UNREF(error);
486 return nullptr;
487 }
488 std::vector<std::string> scopes = absl::StrSplit(scopes_string, ',');
489 auto creds = grpc_core::ExternalAccountCredentials::Create(
490 json, std::move(scopes), &error)
491 .release();
492 if (error != GRPC_ERROR_NONE) {
493 gpr_log(GPR_ERROR,
494 "External account credentials creation failed. Error: %s.",
495 grpc_error_std_string(error).c_str());
496 GRPC_ERROR_UNREF(error);
497 return nullptr;
498 }
499 return creds;
500 }
501