diff --git a/grpc.def b/grpc.def index 32289dffeb7..f451775dba6 100644 --- a/grpc.def +++ b/grpc.def @@ -111,6 +111,7 @@ EXPORTS grpc_google_refresh_token_credentials_create grpc_access_token_credentials_create grpc_google_iam_credentials_create + grpc_sts_credentials_create grpc_metadata_credentials_create_from_plugin grpc_secure_channel_create grpc_server_credentials_release diff --git a/include/grpc/grpc_security.h b/include/grpc/grpc_security.h index 139c0c37a1f..8e4f26a2854 100644 --- a/include/grpc/grpc_security.h +++ b/include/grpc/grpc_security.h @@ -328,6 +328,31 @@ GRPCAPI grpc_call_credentials* grpc_google_iam_credentials_create( const char* authorization_token, const char* authority_selector, void* reserved); +/** Options for creating STS Oauth Token Exchange credentials following the IETF + draft https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16. + Optional fields may be set to NULL. It is the responsibility of the caller to + ensure that the subject and actor tokens are refreshed on disk at the + specified paths. This API is used for experimental purposes for now and may + change in the future. */ +typedef struct { + const char* sts_endpoint_url; /* Required. */ + const char* resource; /* Optional. */ + const char* audience; /* Optional. */ + const char* scope; /* Optional. */ + const char* requested_token_type; /* Optional. */ + const char* subject_token_path; /* Required. */ + const char* subject_token_type; /* Required. */ + const char* actor_token_path; /* Optional. */ + const char* actor_token_type; /* Optional. */ +} grpc_sts_credentials_options; + +/** Creates an STS credentials following the STS Token Exchanged specifed in the + IETF draft https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16. + This API is used for experimental purposes for now and may change in the + future. */ +GRPCAPI grpc_call_credentials* grpc_sts_credentials_create( + const grpc_sts_credentials_options* options, void* reserved); + /** Callback function to be called by the metadata credentials plugin implementation when the metadata is ready. - user_data is the opaque pointer that was passed in the get_metadata method diff --git a/src/core/lib/security/credentials/jwt/json_token.cc b/src/core/lib/security/credentials/jwt/json_token.cc index 113e2b83754..12b7c5368da 100644 --- a/src/core/lib/security/credentials/jwt/json_token.cc +++ b/src/core/lib/security/credentials/jwt/json_token.cc @@ -18,6 +18,7 @@ #include +#include "src/core/lib/iomgr/error.h" #include "src/core/lib/security/credentials/jwt/json_token.h" #include @@ -69,6 +70,7 @@ grpc_auth_json_key grpc_auth_json_key_create_from_json(const grpc_json* json) { BIO* bio = nullptr; const char* prop_value; int success = 0; + grpc_error* error = GRPC_ERROR_NONE; memset(&result, 0, sizeof(grpc_auth_json_key)); result.type = GRPC_AUTH_JSON_TYPE_INVALID; @@ -77,7 +79,8 @@ grpc_auth_json_key grpc_auth_json_key_create_from_json(const grpc_json* json) { goto end; } - prop_value = grpc_json_get_string_property(json, "type"); + prop_value = grpc_json_get_string_property(json, "type", &error); + GRPC_LOG_IF_ERROR("JSON key parsing", error); if (prop_value == nullptr || strcmp(prop_value, GRPC_AUTH_JSON_TYPE_SERVICE_ACCOUNT)) { goto end; @@ -92,7 +95,8 @@ grpc_auth_json_key grpc_auth_json_key_create_from_json(const grpc_json* json) { goto end; } - prop_value = grpc_json_get_string_property(json, "private_key"); + prop_value = grpc_json_get_string_property(json, "private_key", &error); + GRPC_LOG_IF_ERROR("JSON key parsing", error); if (prop_value == nullptr) { goto end; } diff --git a/src/core/lib/security/credentials/oauth2/oauth2_credentials.cc b/src/core/lib/security/credentials/oauth2/oauth2_credentials.cc index b001868b3d3..3d63ec12b49 100644 --- a/src/core/lib/security/credentials/oauth2/oauth2_credentials.cc +++ b/src/core/lib/security/credentials/oauth2/oauth2_credentials.cc @@ -22,14 +22,23 @@ #include -#include "src/core/lib/gprpp/ref_counted_ptr.h" -#include "src/core/lib/security/util/json_util.h" -#include "src/core/lib/surface/api_trace.h" - +#include +#include +#include #include #include #include +#include "src/core/lib/gpr/string.h" +#include "src/core/lib/gprpp/inlined_vector.h" +#include "src/core/lib/gprpp/ref_counted_ptr.h" +#include "src/core/lib/iomgr/error.h" +#include "src/core/lib/iomgr/load_file.h" +#include "src/core/lib/security/util/json_util.h" +#include "src/core/lib/slice/slice_internal.h" +#include "src/core/lib/surface/api_trace.h" +#include "src/core/lib/uri/uri_parser.h" + // // Auth Refresh Token. // @@ -45,6 +54,7 @@ grpc_auth_refresh_token grpc_auth_refresh_token_create_from_json( grpc_auth_refresh_token result; const char* prop_value; int success = 0; + grpc_error* error = GRPC_ERROR_NONE; memset(&result, 0, sizeof(grpc_auth_refresh_token)); result.type = GRPC_AUTH_JSON_TYPE_INVALID; @@ -53,7 +63,8 @@ grpc_auth_refresh_token grpc_auth_refresh_token_create_from_json( goto end; } - prop_value = grpc_json_get_string_property(json, "type"); + prop_value = grpc_json_get_string_property(json, "type", &error); + GRPC_LOG_IF_ERROR("Parsing refresh token", error); if (prop_value == nullptr || strcmp(prop_value, GRPC_AUTH_JSON_TYPE_AUTHORIZED_USER)) { goto end; @@ -218,8 +229,10 @@ void grpc_oauth2_token_fetcher_credentials::on_http_response( grpc_mdelem access_token_md = GRPC_MDNULL; grpc_millis token_lifetime; grpc_credentials_status status = - grpc_oauth2_token_fetcher_credentials_parse_server_response( - &r->response, &access_token_md, &token_lifetime); + error == GRPC_ERROR_NONE + ? grpc_oauth2_token_fetcher_credentials_parse_server_response( + &r->response, &access_token_md, &token_lifetime) + : GRPC_CREDENTIALS_ERROR; // Update cache and grab list of pending requests. gpr_mu_lock(&mu_); token_fetch_pending_ = false; @@ -234,14 +247,15 @@ void grpc_oauth2_token_fetcher_credentials::on_http_response( gpr_mu_unlock(&mu_); // Invoke callbacks for all pending requests. while (pending_request != nullptr) { + grpc_error* new_error = GRPC_ERROR_NONE; if (status == GRPC_CREDENTIALS_OK) { grpc_credentials_mdelem_array_add(pending_request->md_array, access_token_md); } else { - error = GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING( + new_error = GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING( "Error occurred when fetching oauth2 token.", &error, 1); } - GRPC_CLOSURE_SCHED(pending_request->on_request_metadata, error); + GRPC_CLOSURE_SCHED(pending_request->on_request_metadata, new_error); grpc_polling_entity_del_from_pollset_set( pending_request->pollent, grpc_polling_entity_pollset_set(&pollent_)); grpc_oauth2_pending_get_request_metadata* prev = pending_request; @@ -356,7 +370,8 @@ class grpc_compute_engine_token_fetcher_credentials grpc_polling_entity* pollent, grpc_iomgr_cb_func response_cb, grpc_millis deadline) override { - grpc_http_header header = {(char*)"Metadata-Flavor", (char*)"Google"}; + grpc_http_header header = {const_cast("Metadata-Flavor"), + const_cast("Google")}; grpc_httpcli_request request; memset(&request, 0, sizeof(grpc_httpcli_request)); request.host = (char*)GRPC_COMPUTE_ENGINE_METADATA_HOST; @@ -369,11 +384,14 @@ class grpc_compute_engine_token_fetcher_credentials grpc_resource_quota* resource_quota = grpc_resource_quota_create("oauth2_credentials"); grpc_httpcli_get(http_context, pollent, resource_quota, &request, deadline, - GRPC_CLOSURE_CREATE(response_cb, metadata_req, - grpc_schedule_on_exec_ctx), + GRPC_CLOSURE_INIT(&http_get_cb_closure_, response_cb, + metadata_req, grpc_schedule_on_exec_ctx), &metadata_req->response); grpc_resource_quota_unref_internal(resource_quota); } + + private: + grpc_closure http_get_cb_closure_; }; } // namespace @@ -401,8 +419,9 @@ void grpc_google_refresh_token_credentials::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) { - grpc_http_header header = {(char*)"Content-Type", - (char*)"application/x-www-form-urlencoded"}; + grpc_http_header header = { + const_cast("Content-Type"), + const_cast("application/x-www-form-urlencoded")}; grpc_httpcli_request request; char* body = nullptr; gpr_asprintf(&body, GRPC_REFRESH_TOKEN_POST_BODY_FORMAT_STRING, @@ -419,11 +438,11 @@ void grpc_google_refresh_token_credentials::fetch_oauth2( extreme memory pressure. */ grpc_resource_quota* resource_quota = grpc_resource_quota_create("oauth2_credentials_refresh"); - grpc_httpcli_post( - httpcli_context, pollent, resource_quota, &request, body, strlen(body), - deadline, - GRPC_CLOSURE_CREATE(response_cb, metadata_req, grpc_schedule_on_exec_ctx), - &metadata_req->response); + grpc_httpcli_post(httpcli_context, pollent, resource_quota, &request, body, + strlen(body), deadline, + GRPC_CLOSURE_INIT(&http_post_cb_closure_, response_cb, + metadata_req, grpc_schedule_on_exec_ctx), + &metadata_req->response); grpc_resource_quota_unref_internal(resource_quota); gpr_free(body); } @@ -472,6 +491,207 @@ grpc_call_credentials* grpc_google_refresh_token_credentials_create( .release(); } +// +// STS credentials. +// + +namespace grpc_core { + +namespace { + +void MaybeAddToBody(gpr_strvec* body_strvec, const char* field_name, + const char* field) { + if (field == nullptr || strlen(field) == 0) return; + char* new_query; + gpr_asprintf(&new_query, "&%s=%s", field_name, field); + gpr_strvec_add(body_strvec, new_query); +} + +grpc_error* LoadTokenFile(const char* path, gpr_slice* token) { + grpc_error* err = grpc_load_file(path, 1, token); + if (err != GRPC_ERROR_NONE) return err; + if (GRPC_SLICE_LENGTH(*token) == 0) { + gpr_log(GPR_ERROR, "Token file %s is empty", path); + err = GRPC_ERROR_CREATE_FROM_STATIC_STRING("Token file is empty."); + } + return err; +} + +class StsTokenFetcherCredentials + : public grpc_oauth2_token_fetcher_credentials { + public: + StsTokenFetcherCredentials(grpc_uri* sts_url, // Ownership transfered. + const grpc_sts_credentials_options* options) + : sts_url_(sts_url), + resource_(gpr_strdup(options->resource)), + audience_(gpr_strdup(options->audience)), + scope_(gpr_strdup(options->scope)), + requested_token_type_(gpr_strdup(options->requested_token_type)), + subject_token_path_(gpr_strdup(options->subject_token_path)), + subject_token_type_(gpr_strdup(options->subject_token_type)), + actor_token_path_(gpr_strdup(options->actor_token_path)), + actor_token_type_(gpr_strdup(options->actor_token_type)) {} + + ~StsTokenFetcherCredentials() override { grpc_uri_destroy(sts_url_); } + + private: + void fetch_oauth2(grpc_credentials_metadata_request* metadata_req, + grpc_httpcli_context* http_context, + grpc_polling_entity* pollent, + grpc_iomgr_cb_func response_cb, + grpc_millis deadline) override { + char* body = nullptr; + size_t body_length = 0; + grpc_error* err = FillBody(&body, &body_length); + if (err != GRPC_ERROR_NONE) { + response_cb(metadata_req, err); + GRPC_ERROR_UNREF(err); + return; + } + grpc_http_header header = { + const_cast("Content-Type"), + const_cast("application/x-www-form-urlencoded")}; + grpc_httpcli_request request; + memset(&request, 0, sizeof(grpc_httpcli_request)); + request.host = (char*)sts_url_->authority; + request.http.path = (char*)sts_url_->path; + request.http.hdr_count = 1; + request.http.hdrs = &header; + request.handshaker = (strcmp(sts_url_->scheme, "https") == 0) + ? &grpc_httpcli_ssl + : &grpc_httpcli_plaintext; + /* TODO(ctiller): Carry the resource_quota in ctx and share it with the host + channel. This would allow us to cancel an authentication query when under + extreme memory pressure. */ + grpc_resource_quota* resource_quota = + grpc_resource_quota_create("oauth2_credentials_refresh"); + grpc_httpcli_post( + http_context, pollent, resource_quota, &request, body, body_length, + deadline, + GRPC_CLOSURE_INIT(&http_post_cb_closure_, response_cb, metadata_req, + grpc_schedule_on_exec_ctx), + &metadata_req->response); + grpc_resource_quota_unref_internal(resource_quota); + gpr_free(body); + } + + grpc_error* FillBody(char** body, size_t* body_length) { + *body = nullptr; + gpr_strvec body_strvec; + gpr_strvec_init(&body_strvec); + grpc_slice subject_token = grpc_empty_slice(); + grpc_slice actor_token = grpc_empty_slice(); + grpc_error* err = GRPC_ERROR_NONE; + + auto cleanup = [&body, &body_length, &body_strvec, &subject_token, + &actor_token, &err]() { + if (err == GRPC_ERROR_NONE) { + *body = gpr_strvec_flatten(&body_strvec, body_length); + } else { + gpr_free(*body); + } + gpr_strvec_destroy(&body_strvec); + grpc_slice_unref_internal(subject_token); + grpc_slice_unref_internal(actor_token); + return err; + }; + + err = LoadTokenFile(subject_token_path_.get(), &subject_token); + if (err != GRPC_ERROR_NONE) return cleanup(); + gpr_asprintf( + body, GRPC_STS_POST_MINIMAL_BODY_FORMAT_STRING, + reinterpret_cast(GRPC_SLICE_START_PTR(subject_token)), + subject_token_type_.get()); + gpr_strvec_add(&body_strvec, *body); + MaybeAddToBody(&body_strvec, "resource", resource_.get()); + MaybeAddToBody(&body_strvec, "audience", audience_.get()); + MaybeAddToBody(&body_strvec, "scope", scope_.get()); + MaybeAddToBody(&body_strvec, "requested_token_type", + requested_token_type_.get()); + if (actor_token_path_ != nullptr) { + err = LoadTokenFile(actor_token_path_.get(), &actor_token); + if (err != GRPC_ERROR_NONE) return cleanup(); + MaybeAddToBody( + &body_strvec, "actor_token", + reinterpret_cast(GRPC_SLICE_START_PTR(subject_token))); + MaybeAddToBody(&body_strvec, "actor_token_type", actor_token_type_.get()); + } + return cleanup(); + } + + grpc_uri* sts_url_; + grpc_closure http_post_cb_closure_; + grpc_core::UniquePtr resource_; + grpc_core::UniquePtr audience_; + grpc_core::UniquePtr scope_; + grpc_core::UniquePtr requested_token_type_; + grpc_core::UniquePtr subject_token_path_; + grpc_core::UniquePtr subject_token_type_; + grpc_core::UniquePtr actor_token_path_; + grpc_core::UniquePtr actor_token_type_; +}; + +} // namespace + +grpc_error* ValidateStsCredentialsOptions( + const grpc_sts_credentials_options* options, grpc_uri** sts_url_out) { + struct GrpcUriDeleter { + void operator()(grpc_uri* uri) { grpc_uri_destroy(uri); } + }; + *sts_url_out = nullptr; + InlinedVector error_list; + UniquePtr sts_url( + options->sts_endpoint_url != nullptr + ? grpc_uri_parse(options->sts_endpoint_url, false) + : nullptr); + if (sts_url == nullptr) { + error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Invalid or missing STS endpoint URL")); + } else { + if (strcmp(sts_url->scheme, "https") != 0 && + strcmp(sts_url->scheme, "http") != 0) { + error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Invalid URI scheme, must be https to http.")); + } + } + if (options->subject_token_path == nullptr || + strlen(options->subject_token_path) == 0) { + error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "subject_token needs to be specified")); + } + if (options->subject_token_type == nullptr || + strlen(options->subject_token_type) == 0) { + error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "subject_token_type needs to be specified")); + } + if (error_list.empty()) { + *sts_url_out = sts_url.release(); + return GRPC_ERROR_NONE; + } else { + return GRPC_ERROR_CREATE_FROM_VECTOR("Invalid STS Credentials Options", + &error_list); + } +} + +} // namespace grpc_core + +grpc_call_credentials* grpc_sts_credentials_create( + const grpc_sts_credentials_options* options, void* reserved) { + GPR_ASSERT(reserved == nullptr); + grpc_uri* sts_url; + grpc_error* error = + grpc_core::ValidateStsCredentialsOptions(options, &sts_url); + if (error != GRPC_ERROR_NONE) { + gpr_log(GPR_ERROR, "STS Credentials creation failed. Error: %s.", + grpc_error_string(error)); + GRPC_ERROR_UNREF(error); + return nullptr; + } + return grpc_core::MakeRefCounted( + sts_url, options) + .release(); +} + // // Oauth2 Access Token credentials. // diff --git a/src/core/lib/security/credentials/oauth2/oauth2_credentials.h b/src/core/lib/security/credentials/oauth2/oauth2_credentials.h index 510a78b484a..c9b2d1decac 100644 --- a/src/core/lib/security/credentials/oauth2/oauth2_credentials.h +++ b/src/core/lib/security/credentials/oauth2/oauth2_credentials.h @@ -21,8 +21,15 @@ #include +#include #include "src/core/lib/json/json.h" #include "src/core/lib/security/credentials/credentials.h" +#include "src/core/lib/uri/uri_parser.h" + +// Constants. +#define GRPC_STS_POST_MINIMAL_BODY_FORMAT_STRING \ + "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=%" \ + "s&subject_token_type=%s" // auth_refresh_token parsing. typedef struct { @@ -115,6 +122,7 @@ class grpc_google_refresh_token_credentials final private: grpc_auth_refresh_token refresh_token_; + grpc_closure http_post_cb_closure_; }; // Access token credentials. @@ -148,4 +156,12 @@ grpc_oauth2_token_fetcher_credentials_parse_server_response( const struct grpc_http_response* response, grpc_mdelem* token_md, grpc_millis* token_lifetime); +namespace grpc_core { +// Exposed for testing only. This function validates the options, ensuring that +// the required fields are set, and outputs the parsed URL of the STS token +// exchanged service. +grpc_error* ValidateStsCredentialsOptions( + const grpc_sts_credentials_options* options, grpc_uri** sts_url); +} // namespace grpc_core + #endif /* GRPC_CORE_LIB_SECURITY_CREDENTIALS_OAUTH2_OAUTH2_CREDENTIALS_H */ diff --git a/src/core/lib/security/util/json_util.cc b/src/core/lib/security/util/json_util.cc index fe9f5fe3d35..8a1db636139 100644 --- a/src/core/lib/security/util/json_util.cc +++ b/src/core/lib/security/util/json_util.cc @@ -18,6 +18,7 @@ #include +#include "src/core/lib/iomgr/error.h" #include "src/core/lib/security/util/json_util.h" #include @@ -26,17 +27,27 @@ #include const char* grpc_json_get_string_property(const grpc_json* json, - const char* prop_name) { - grpc_json* child; + const char* prop_name, + grpc_error** error) { + grpc_json* child = nullptr; + if (error != nullptr) *error = GRPC_ERROR_NONE; for (child = json->child; child != nullptr; child = child->next) { if (child->key == nullptr) { - gpr_log(GPR_ERROR, "Invalid (null) JSON key encountered"); + if (error != nullptr) { + *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Invalid (null) JSON key encountered"); + } return nullptr; } if (strcmp(child->key, prop_name) == 0) break; } if (child == nullptr || child->type != GRPC_JSON_STRING) { - gpr_log(GPR_ERROR, "Invalid or missing %s property.", prop_name); + if (error != nullptr) { + char* error_msg; + gpr_asprintf(&error_msg, "Invalid or missing %s property.", prop_name); + *error = GRPC_ERROR_CREATE_FROM_COPIED_STRING(error_msg); + gpr_free(error_msg); + } return nullptr; } return child->value; @@ -45,7 +56,10 @@ const char* grpc_json_get_string_property(const grpc_json* json, bool grpc_copy_json_string_property(const grpc_json* json, const char* prop_name, char** copied_value) { - const char* prop_value = grpc_json_get_string_property(json, prop_name); + grpc_error* error = GRPC_ERROR_NONE; + const char* prop_value = + grpc_json_get_string_property(json, prop_name, &error); + GRPC_LOG_IF_ERROR("Could not copy JSON property", error); if (prop_value == nullptr) return false; *copied_value = gpr_strdup(prop_value); return true; diff --git a/src/core/lib/security/util/json_util.h b/src/core/lib/security/util/json_util.h index 89deffcc081..44d9eccd545 100644 --- a/src/core/lib/security/util/json_util.h +++ b/src/core/lib/security/util/json_util.h @@ -23,6 +23,7 @@ #include +#include "src/core/lib/iomgr/error.h" #include "src/core/lib/json/json.h" // Constants. @@ -32,7 +33,8 @@ // Gets a child property from a json node. const char* grpc_json_get_string_property(const grpc_json* json, - const char* prop_name); + const char* prop_name, + grpc_error** error); // Copies the value of the json child property specified by prop_name. // Returns false if the property was not found. diff --git a/src/ruby/ext/grpc/rb_grpc_imports.generated.c b/src/ruby/ext/grpc/rb_grpc_imports.generated.c index b1165a6d6eb..fa275c75419 100644 --- a/src/ruby/ext/grpc/rb_grpc_imports.generated.c +++ b/src/ruby/ext/grpc/rb_grpc_imports.generated.c @@ -134,6 +134,7 @@ grpc_service_account_jwt_access_credentials_create_type grpc_service_account_jwt grpc_google_refresh_token_credentials_create_type grpc_google_refresh_token_credentials_create_import; grpc_access_token_credentials_create_type grpc_access_token_credentials_create_import; grpc_google_iam_credentials_create_type grpc_google_iam_credentials_create_import; +grpc_sts_credentials_create_type grpc_sts_credentials_create_import; grpc_metadata_credentials_create_from_plugin_type grpc_metadata_credentials_create_from_plugin_import; grpc_secure_channel_create_type grpc_secure_channel_create_import; grpc_server_credentials_release_type grpc_server_credentials_release_import; @@ -404,6 +405,7 @@ void grpc_rb_load_imports(HMODULE library) { grpc_google_refresh_token_credentials_create_import = (grpc_google_refresh_token_credentials_create_type) GetProcAddress(library, "grpc_google_refresh_token_credentials_create"); grpc_access_token_credentials_create_import = (grpc_access_token_credentials_create_type) GetProcAddress(library, "grpc_access_token_credentials_create"); grpc_google_iam_credentials_create_import = (grpc_google_iam_credentials_create_type) GetProcAddress(library, "grpc_google_iam_credentials_create"); + grpc_sts_credentials_create_import = (grpc_sts_credentials_create_type) GetProcAddress(library, "grpc_sts_credentials_create"); grpc_metadata_credentials_create_from_plugin_import = (grpc_metadata_credentials_create_from_plugin_type) GetProcAddress(library, "grpc_metadata_credentials_create_from_plugin"); grpc_secure_channel_create_import = (grpc_secure_channel_create_type) GetProcAddress(library, "grpc_secure_channel_create"); grpc_server_credentials_release_import = (grpc_server_credentials_release_type) GetProcAddress(library, "grpc_server_credentials_release"); diff --git a/src/ruby/ext/grpc/rb_grpc_imports.generated.h b/src/ruby/ext/grpc/rb_grpc_imports.generated.h index 5809194f1ae..1389c301728 100644 --- a/src/ruby/ext/grpc/rb_grpc_imports.generated.h +++ b/src/ruby/ext/grpc/rb_grpc_imports.generated.h @@ -377,6 +377,9 @@ extern grpc_access_token_credentials_create_type grpc_access_token_credentials_c typedef grpc_call_credentials*(*grpc_google_iam_credentials_create_type)(const char* authorization_token, const char* authority_selector, void* reserved); extern grpc_google_iam_credentials_create_type grpc_google_iam_credentials_create_import; #define grpc_google_iam_credentials_create grpc_google_iam_credentials_create_import +typedef grpc_call_credentials*(*grpc_sts_credentials_create_type)(const grpc_sts_credentials_options* options, void* reserved); +extern grpc_sts_credentials_create_type grpc_sts_credentials_create_import; +#define grpc_sts_credentials_create grpc_sts_credentials_create_import typedef grpc_call_credentials*(*grpc_metadata_credentials_create_from_plugin_type)(grpc_metadata_credentials_plugin plugin, void* reserved); extern grpc_metadata_credentials_create_from_plugin_type grpc_metadata_credentials_create_from_plugin_import; #define grpc_metadata_credentials_create_from_plugin grpc_metadata_credentials_create_from_plugin_import diff --git a/test/core/security/credentials_test.cc b/test/core/security/credentials_test.cc index 141346bca94..cbce595c354 100644 --- a/test/core/security/credentials_test.cc +++ b/test/core/security/credentials_test.cc @@ -24,8 +24,8 @@ #include #include +#include #include - #include #include #include @@ -34,13 +34,16 @@ #include "src/core/lib/gpr/env.h" #include "src/core/lib/gpr/string.h" #include "src/core/lib/gpr/tmpfile.h" +#include "src/core/lib/gprpp/host_port.h" #include "src/core/lib/http/httpcli.h" +#include "src/core/lib/iomgr/error.h" #include "src/core/lib/security/credentials/composite/composite_credentials.h" #include "src/core/lib/security/credentials/fake/fake_credentials.h" #include "src/core/lib/security/credentials/google_default/google_default_credentials.h" #include "src/core/lib/security/credentials/jwt/jwt_credentials.h" #include "src/core/lib/security/credentials/oauth2/oauth2_credentials.h" #include "src/core/lib/security/transport/auth_filters.h" +#include "src/core/lib/uri/uri_parser.h" #include "test/core/util/test_config.h" using grpc_core::internal::grpc_flush_cached_google_default_credentials; @@ -99,15 +102,27 @@ static const char valid_oauth2_json_response[] = " \"expires_in\":3599, " " \"token_type\":\"Bearer\"}"; +static const char valid_sts_json_response[] = + "{\"access_token\":\"ya29.AHES6ZRN3-HlhAPya30GnW_bHSb_\"," + " \"expires_in\":3599, " + " \"issued_token_type\":\"urn:ietf:params:oauth:token-type:access_token\", " + " \"token_type\":\"Bearer\"}"; + static const char test_scope[] = "perm1 perm2"; static const char test_signed_jwt[] = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImY0OTRkN2M1YWU2MGRmOTcyNmM4YW" "U0MDcyZTViYTdmZDkwODg2YzcifQ"; +static const char test_signed_jwt_token_type[] = + "urn:ietf:params:oauth:token-type:id_token"; +static const char test_signed_jwt_path_prefix[] = "test_sign_jwt"; static const char test_service_url[] = "https://foo.com/foo.v1"; static const char other_test_service_url[] = "https://bar.com/bar.v1"; +static const char test_sts_endpoint_url[] = + "https://foo.com:5555/v1/token-exchange"; + static const char test_method[] = "ThisIsNotAMethod"; /* -- Global state flags. -- */ @@ -657,11 +672,11 @@ static int refresh_token_httpcli_post_success( return 1; } -static int refresh_token_httpcli_post_failure( - const grpc_httpcli_request* request, const char* body, size_t body_size, - grpc_millis deadline, grpc_closure* on_done, - grpc_httpcli_response* response) { - validate_refresh_token_http_request(request, body, body_size); +static int token_httpcli_post_failure(const grpc_httpcli_request* request, + const char* body, size_t body_size, + grpc_millis deadline, + grpc_closure* on_done, + grpc_httpcli_response* response) { *response = http_response(403, "Not Authorized."); GRPC_CLOSURE_SCHED(on_done, GRPC_ERROR_NONE); return 1; @@ -676,7 +691,7 @@ static void test_refresh_token_creds_success(void) { grpc_call_credentials* creds = grpc_google_refresh_token_credentials_create( test_refresh_token_str, nullptr); - /* First request: http get should be called. */ + /* First request: http put should be called. */ request_metadata_state* state = make_request_metadata_state(GRPC_ERROR_NONE, emd, GPR_ARRAY_SIZE(emd)); grpc_httpcli_set_override(httpcli_get_should_not_be_called, @@ -707,10 +722,279 @@ static void test_refresh_token_creds_failure(void) { grpc_call_credentials* creds = grpc_google_refresh_token_credentials_create( test_refresh_token_str, nullptr); grpc_httpcli_set_override(httpcli_get_should_not_be_called, - refresh_token_httpcli_post_failure); + token_httpcli_post_failure); + run_request_metadata_test(creds, auth_md_ctx, state); + creds->Unref(); + grpc_httpcli_set_override(nullptr, nullptr); +} + +static void test_valid_sts_creds_options(void) { + grpc_sts_credentials_options valid_options = { + test_sts_endpoint_url, // sts_endpoint_url + nullptr, // resource + nullptr, // audience + nullptr, // scope + nullptr, // requested_token_type + test_signed_jwt_path_prefix, // subject_token_path + test_signed_jwt_token_type, // subject_token_type + nullptr, // actor_token_path + nullptr // actor_token_type + }; + grpc_uri* sts_url; + grpc_error* error = + grpc_core::ValidateStsCredentialsOptions(&valid_options, &sts_url); + GPR_ASSERT(error == GRPC_ERROR_NONE); + GPR_ASSERT(sts_url != nullptr); + grpc_core::StringView host; + grpc_core::StringView port; + GPR_ASSERT(grpc_core::SplitHostPort(sts_url->authority, &host, &port)); + GPR_ASSERT(host.cmp("foo.com") == 0); + GPR_ASSERT(port.cmp("5555") == 0); + grpc_uri_destroy(sts_url); +} + +static void test_invalid_sts_creds_options(void) { + grpc_sts_credentials_options invalid_options = { + test_sts_endpoint_url, // sts_endpoint_url + nullptr, // resource + nullptr, // audience + nullptr, // scope + nullptr, // requested_token_type + nullptr, // subject_token_path (Required) + test_signed_jwt_token_type, // subject_token_type + nullptr, // actor_token_path + nullptr // actor_token_type + }; + grpc_uri* url_should_be_null; + grpc_error* error = grpc_core::ValidateStsCredentialsOptions( + &invalid_options, &url_should_be_null); + GPR_ASSERT(error != GRPC_ERROR_NONE); + GRPC_ERROR_UNREF(error); + GPR_ASSERT(url_should_be_null == nullptr); + + invalid_options = { + test_sts_endpoint_url, // sts_endpoint_url + nullptr, // resource + nullptr, // audience + nullptr, // scope + nullptr, // requested_token_type + test_signed_jwt_path_prefix, // subject_token_path + nullptr, // subject_token_type (Required) + nullptr, // actor_token_path + nullptr // actor_token_type + }; + error = grpc_core::ValidateStsCredentialsOptions(&invalid_options, + &url_should_be_null); + GPR_ASSERT(error != GRPC_ERROR_NONE); + GRPC_ERROR_UNREF(error); + GPR_ASSERT(url_should_be_null == nullptr); + + invalid_options = { + nullptr, // sts_endpoint_url (Required) + nullptr, // resource + nullptr, // audience + nullptr, // scope + nullptr, // requested_token_type + test_signed_jwt_path_prefix, // subject_token_path + test_signed_jwt_token_type, // subject_token_type (Required) + nullptr, // actor_token_path + nullptr // actor_token_type + }; + error = grpc_core::ValidateStsCredentialsOptions(&invalid_options, + &url_should_be_null); + GPR_ASSERT(error != GRPC_ERROR_NONE); + GRPC_ERROR_UNREF(error); + GPR_ASSERT(url_should_be_null == nullptr); + + invalid_options = { + "not_a_valid_uri", // sts_endpoint_url + nullptr, // resource + nullptr, // audience + nullptr, // scope + nullptr, // requested_token_type + test_signed_jwt_path_prefix, // subject_token_path + test_signed_jwt_token_type, // subject_token_type (Required) + nullptr, // actor_token_path + nullptr // actor_token_type + }; + error = grpc_core::ValidateStsCredentialsOptions(&invalid_options, + &url_should_be_null); + GPR_ASSERT(error != GRPC_ERROR_NONE); + GRPC_ERROR_UNREF(error); + GPR_ASSERT(url_should_be_null == nullptr); + + invalid_options = { + "ftp://ftp.is.not.a.valid.scheme/bar", // sts_endpoint_url + nullptr, // resource + nullptr, // audience + nullptr, // scope + nullptr, // requested_token_type + test_signed_jwt_path_prefix, // subject_token_path + test_signed_jwt_token_type, // subject_token_type (Required) + nullptr, // actor_token_path + nullptr // actor_token_type + }; + error = grpc_core::ValidateStsCredentialsOptions(&invalid_options, + &url_should_be_null); + GPR_ASSERT(error != GRPC_ERROR_NONE); + GRPC_ERROR_UNREF(error); + GPR_ASSERT(url_should_be_null == nullptr); +} + +static void validate_sts_token_http_request(const grpc_httpcli_request* request, + const char* body, + size_t body_size) { + // Check that the body is constructed properly. + GPR_ASSERT(body != nullptr); + GPR_ASSERT(body_size != 0); + GPR_ASSERT(request->handshaker == &grpc_httpcli_ssl); + char* get_url_equivalent; + gpr_asprintf(&get_url_equivalent, "%s?%s", test_sts_endpoint_url, body); + grpc_uri* url = grpc_uri_parse(get_url_equivalent, false); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(url, "resource"), "resource") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(url, "audience"), "audience") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(url, "scope"), "scope") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(url, "requested_token_type"), + "requested_token_type") == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(url, "subject_token"), + test_signed_jwt) == 0); + GPR_ASSERT(strcmp(grpc_uri_get_query_arg(url, "subject_token_type"), + test_signed_jwt_token_type) == 0); + GPR_ASSERT(grpc_uri_get_query_arg(url, "actor_token") == nullptr); + GPR_ASSERT(grpc_uri_get_query_arg(url, "actor_token_type") == nullptr); + grpc_uri_destroy(url); + gpr_free(get_url_equivalent); + + // Check the rest of the request. + GPR_ASSERT(strcmp(request->host, "foo.com:5555") == 0); + GPR_ASSERT(strcmp(request->http.path, "/v1/token-exchange") == 0); + GPR_ASSERT(request->http.hdr_count == 1); + GPR_ASSERT(strcmp(request->http.hdrs[0].key, "Content-Type") == 0); + GPR_ASSERT(strcmp(request->http.hdrs[0].value, + "application/x-www-form-urlencoded") == 0); +} + +static int sts_token_httpcli_post_success(const grpc_httpcli_request* request, + const char* body, size_t body_size, + grpc_millis deadline, + grpc_closure* on_done, + grpc_httpcli_response* response) { + validate_sts_token_http_request(request, body, body_size); + *response = http_response(200, valid_sts_json_response); + GRPC_CLOSURE_SCHED(on_done, GRPC_ERROR_NONE); + return 1; +} + +static char* write_tmp_jwt_file(void) { + char* path; + FILE* tmp = gpr_tmpfile(test_signed_jwt_path_prefix, &path); + GPR_ASSERT(path != nullptr); + GPR_ASSERT(tmp != nullptr); + size_t jwt_length = strlen(test_signed_jwt); + GPR_ASSERT(fwrite(test_signed_jwt, 1, jwt_length, tmp) == jwt_length); + fclose(tmp); + return path; +} + +static void test_sts_creds_success(void) { + grpc_core::ExecCtx exec_ctx; + expected_md emd[] = { + {"authorization", "Bearer ya29.AHES6ZRN3-HlhAPya30GnW_bHSb_"}}; + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + char* test_signed_jwt_path = write_tmp_jwt_file(); + grpc_sts_credentials_options valid_options = { + test_sts_endpoint_url, // sts_endpoint_url + "resource", // resource + "audience", // audience + "scope", // scope + "requested_token_type", // requested_token_type + test_signed_jwt_path, // subject_token_path + test_signed_jwt_token_type, // subject_token_type + nullptr, // actor_token_path + nullptr // actor_token_type + }; + grpc_call_credentials* creds = + grpc_sts_credentials_create(&valid_options, nullptr); + + /* First request: http put should be called. */ + request_metadata_state* state = + make_request_metadata_state(GRPC_ERROR_NONE, emd, GPR_ARRAY_SIZE(emd)); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + sts_token_httpcli_post_success); + run_request_metadata_test(creds, auth_md_ctx, state); + grpc_core::ExecCtx::Get()->Flush(); + + /* Second request: the cached token should be served directly. */ + state = + make_request_metadata_state(GRPC_ERROR_NONE, emd, GPR_ARRAY_SIZE(emd)); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + httpcli_post_should_not_be_called); + run_request_metadata_test(creds, auth_md_ctx, state); + grpc_core::ExecCtx::Get()->Flush(); + + creds->Unref(); + grpc_httpcli_set_override(nullptr, nullptr); + gpr_free(test_signed_jwt_path); +} + +static void test_sts_creds_load_token_failure(void) { + grpc_core::ExecCtx exec_ctx; + request_metadata_state* state = make_request_metadata_state( + GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Error occurred when fetching oauth2 token."), + nullptr, 0); + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + char* test_signed_jwt_path = write_tmp_jwt_file(); + grpc_sts_credentials_options options = { + test_sts_endpoint_url, // sts_endpoint_url + "resource", // resource + "audience", // audience + "scope", // scope + "requested_token_type", // requested_token_type + "invalid_path", // subject_token_path + test_signed_jwt_token_type, // subject_token_type + nullptr, // actor_token_path + nullptr // actor_token_type + }; + grpc_call_credentials* creds = grpc_sts_credentials_create(&options, nullptr); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + httpcli_post_should_not_be_called); + run_request_metadata_test(creds, auth_md_ctx, state); + creds->Unref(); + grpc_httpcli_set_override(nullptr, nullptr); + gpr_free(test_signed_jwt_path); +} + +static void test_sts_creds_http_failure(void) { + grpc_core::ExecCtx exec_ctx; + request_metadata_state* state = make_request_metadata_state( + GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "Error occurred when fetching oauth2 token."), + nullptr, 0); + grpc_auth_metadata_context auth_md_ctx = {test_service_url, test_method, + nullptr, nullptr}; + char* test_signed_jwt_path = write_tmp_jwt_file(); + grpc_sts_credentials_options valid_options = { + test_sts_endpoint_url, // sts_endpoint_url + "resource", // resource + "audience", // audience + "scope", // scope + "requested_token_type", // requested_token_type + test_signed_jwt_path, // subject_token_path + test_signed_jwt_token_type, // subject_token_type + nullptr, // actor_token_path + nullptr // actor_token_type + }; + grpc_call_credentials* creds = + grpc_sts_credentials_create(&valid_options, nullptr); + grpc_httpcli_set_override(httpcli_get_should_not_be_called, + token_httpcli_post_failure); run_request_metadata_test(creds, auth_md_ctx, state); creds->Unref(); grpc_httpcli_set_override(nullptr, nullptr); + gpr_free(test_signed_jwt_path); } static void validate_jwt_encode_and_sign_params( @@ -1288,6 +1572,11 @@ int main(int argc, char** argv) { test_compute_engine_creds_failure(); test_refresh_token_creds_success(); test_refresh_token_creds_failure(); + test_valid_sts_creds_options(); + test_invalid_sts_creds_options(); + test_sts_creds_success(); + test_sts_creds_load_token_failure(); + test_sts_creds_http_failure(); test_jwt_creds_lifetime(); test_jwt_creds_success(); test_jwt_creds_signing_failure(); diff --git a/test/core/security/fetch_oauth2.cc b/test/core/security/fetch_oauth2.cc index 82efe682be0..d404368b8b9 100644 --- a/test/core/security/fetch_oauth2.cc +++ b/test/core/security/fetch_oauth2.cc @@ -26,33 +26,82 @@ #include #include +#include "src/core/lib/iomgr/error.h" #include "src/core/lib/iomgr/load_file.h" #include "src/core/lib/security/credentials/credentials.h" +#include "src/core/lib/security/util/json_util.h" #include "test/core/security/oauth2_utils.h" #include "test/core/util/cmdline.h" +static grpc_sts_credentials_options sts_options_from_json(grpc_json* json) { + grpc_sts_credentials_options options; + memset(&options, 0, sizeof(options)); + grpc_error* error = GRPC_ERROR_NONE; + options.sts_endpoint_url = + grpc_json_get_string_property(json, "sts_endpoint_url", &error); + GRPC_LOG_IF_ERROR("STS credentials parsing", error); + options.resource = grpc_json_get_string_property(json, "resource", nullptr); + options.audience = grpc_json_get_string_property(json, "audience", nullptr); + options.scope = grpc_json_get_string_property(json, "scope", nullptr); + options.requested_token_type = + grpc_json_get_string_property(json, "requested_token_type", nullptr); + options.subject_token_path = + grpc_json_get_string_property(json, "subject_token_path", &error); + GRPC_LOG_IF_ERROR("STS credentials parsing", error); + options.subject_token_type = + grpc_json_get_string_property(json, "subject_token_type", &error); + GRPC_LOG_IF_ERROR("STS credentials parsing", error); + options.actor_token_path = + grpc_json_get_string_property(json, "actor_token_path", nullptr); + options.actor_token_type = + grpc_json_get_string_property(json, "actor_token_type", nullptr); + return options; +} + +static grpc_call_credentials* create_sts_creds(const char* json_file_path) { + grpc_slice sts_options_slice; + GPR_ASSERT(GRPC_LOG_IF_ERROR( + "load_file", grpc_load_file(json_file_path, 1, &sts_options_slice))); + grpc_json* json = grpc_json_parse_string( + reinterpret_cast(GRPC_SLICE_START_PTR(sts_options_slice))); + if (json == nullptr) { + gpr_log(GPR_ERROR, "Invalid json"); + return nullptr; + } + grpc_sts_credentials_options options = sts_options_from_json(json); + grpc_call_credentials* result = + grpc_sts_credentials_create(&options, nullptr); + grpc_json_destroy(json); + gpr_slice_unref(sts_options_slice); + return result; +} + static grpc_call_credentials* create_refresh_token_creds( const char* json_refresh_token_file_path) { grpc_slice refresh_token; GPR_ASSERT(GRPC_LOG_IF_ERROR( "load_file", grpc_load_file(json_refresh_token_file_path, 1, &refresh_token))); - return grpc_google_refresh_token_credentials_create( + grpc_call_credentials* result = grpc_google_refresh_token_credentials_create( reinterpret_cast GRPC_SLICE_START_PTR(refresh_token), nullptr); + gpr_slice_unref(refresh_token); + return result; } int main(int argc, char** argv) { grpc_call_credentials* creds = nullptr; - char* json_key_file_path = nullptr; + const char* json_sts_options_file_path = nullptr; const char* json_refresh_token_file_path = nullptr; char* token = nullptr; int use_gce = 0; - char* scope = nullptr; gpr_cmdline* cl = gpr_cmdline_create("fetch_oauth2"); gpr_cmdline_add_string(cl, "json_refresh_token", "File path of the json refresh token.", &json_refresh_token_file_path); + gpr_cmdline_add_string(cl, "json_sts_options", + "File path of the json sts options.", + &json_sts_options_file_path); gpr_cmdline_add_flag( cl, "gce", "Get a token from the GCE metadata server (only works in GCE).", @@ -61,18 +110,20 @@ int main(int argc, char** argv) { grpc_init(); - if (json_key_file_path != nullptr && + if (json_sts_options_file_path != nullptr && json_refresh_token_file_path != nullptr) { - gpr_log(GPR_ERROR, - "--json_key and --json_refresh_token are mutually exclusive."); + gpr_log( + GPR_ERROR, + "--json_sts_options and --json_refresh_token are mutually exclusive."); exit(1); } if (use_gce) { - if (json_key_file_path != nullptr || scope != nullptr) { + if (json_sts_options_file_path != nullptr || + json_refresh_token_file_path != nullptr) { gpr_log(GPR_INFO, - "Ignoring json key and scope to get a token from the GCE " - "metadata server."); + "Ignoring json refresh token or sts options to get a token from " + "the GCE metadata server."); } creds = grpc_google_compute_engine_credentials_create(nullptr); if (creds == nullptr) { @@ -88,8 +139,19 @@ int main(int argc, char** argv) { json_refresh_token_file_path); exit(1); } + } else if (json_sts_options_file_path != nullptr) { + creds = create_sts_creds(json_sts_options_file_path); + if (creds == nullptr) { + gpr_log(GPR_ERROR, + "Could not create sts creds. %s does probably not contain a " + "valid json for sts options.", + json_sts_options_file_path); + exit(1); + } } else { - gpr_log(GPR_ERROR, "Missing --gce or --json_refresh_token option."); + gpr_log( + GPR_ERROR, + "Missing --gce, --json_sts_options, or --json_refresh_token option."); exit(1); } GPR_ASSERT(creds != nullptr); diff --git a/test/core/security/oauth2_utils.cc b/test/core/security/oauth2_utils.cc index c9e205ab743..b24e7c180e1 100644 --- a/test/core/security/oauth2_utils.cc +++ b/test/core/security/oauth2_utils.cc @@ -63,14 +63,17 @@ static void on_oauth2_response(void* arg, grpc_error* error) { gpr_mu_unlock(request->mu); } -static void do_nothing(void* unused, grpc_error* error) {} +static void destroy_after_shutdown(void* pollset, grpc_error* error) { + grpc_pollset_destroy(reinterpret_cast(pollset)); + gpr_free(pollset); +} char* grpc_test_fetch_oauth2_token_with_credentials( grpc_call_credentials* creds) { oauth2_request request; memset(&request, 0, sizeof(request)); grpc_core::ExecCtx exec_ctx; - grpc_closure do_nothing_closure; + grpc_closure destroy_after_shutdown_closure; grpc_auth_metadata_context null_ctx = {"", "", nullptr, nullptr}; grpc_pollset* pollset = @@ -79,8 +82,8 @@ char* grpc_test_fetch_oauth2_token_with_credentials( request.pops = grpc_polling_entity_create_from_pollset(pollset); request.is_done = false; - GRPC_CLOSURE_INIT(&do_nothing_closure, do_nothing, nullptr, - grpc_schedule_on_exec_ctx); + GRPC_CLOSURE_INIT(&destroy_after_shutdown_closure, destroy_after_shutdown, + pollset, grpc_schedule_on_exec_ctx); GRPC_CLOSURE_INIT(&request.closure, on_oauth2_response, &request, grpc_schedule_on_exec_ctx); @@ -107,8 +110,6 @@ char* grpc_test_fetch_oauth2_token_with_credentials( gpr_mu_unlock(request.mu); grpc_pollset_shutdown(grpc_polling_entity_pollset(&request.pops), - &do_nothing_closure); - - gpr_free(grpc_polling_entity_pollset(&request.pops)); + &destroy_after_shutdown_closure); return request.token; } diff --git a/test/core/surface/public_headers_must_be_c89.c b/test/core/surface/public_headers_must_be_c89.c index 3aaa1e709ee..e537e482936 100644 --- a/test/core/surface/public_headers_must_be_c89.c +++ b/test/core/surface/public_headers_must_be_c89.c @@ -171,6 +171,7 @@ int main(int argc, char **argv) { printf("%lx", (unsigned long) grpc_google_refresh_token_credentials_create); printf("%lx", (unsigned long) grpc_access_token_credentials_create); printf("%lx", (unsigned long) grpc_google_iam_credentials_create); + printf("%lx", (unsigned long) grpc_sts_credentials_create); printf("%lx", (unsigned long) grpc_metadata_credentials_create_from_plugin); printf("%lx", (unsigned long) grpc_secure_channel_create); printf("%lx", (unsigned long) grpc_server_credentials_release);