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