/* * * Copyright 2015 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include #include "src/core/lib/security/credentials/oauth2/oauth2_credentials.h" #include #include "src/core/lib/security/util/json_util.h" #include "src/core/lib/surface/api_trace.h" #include #include #include // // Auth Refresh Token. // int grpc_auth_refresh_token_is_valid( const grpc_auth_refresh_token* refresh_token) { return (refresh_token != nullptr) && strcmp(refresh_token->type, GRPC_AUTH_JSON_TYPE_INVALID); } grpc_auth_refresh_token grpc_auth_refresh_token_create_from_json( const grpc_json* json) { grpc_auth_refresh_token result; const char* prop_value; int success = 0; memset(&result, 0, sizeof(grpc_auth_refresh_token)); result.type = GRPC_AUTH_JSON_TYPE_INVALID; if (json == nullptr) { gpr_log(GPR_ERROR, "Invalid json."); goto end; } prop_value = grpc_json_get_string_property(json, "type"); if (prop_value == nullptr || strcmp(prop_value, GRPC_AUTH_JSON_TYPE_AUTHORIZED_USER)) { goto end; } result.type = GRPC_AUTH_JSON_TYPE_AUTHORIZED_USER; if (!grpc_copy_json_string_property(json, "client_secret", &result.client_secret) || !grpc_copy_json_string_property(json, "client_id", &result.client_id) || !grpc_copy_json_string_property(json, "refresh_token", &result.refresh_token)) { goto end; } success = 1; end: if (!success) grpc_auth_refresh_token_destruct(&result); return result; } grpc_auth_refresh_token grpc_auth_refresh_token_create_from_string( const char* json_string) { char* scratchpad = gpr_strdup(json_string); grpc_json* json = grpc_json_parse_string(scratchpad); grpc_auth_refresh_token result = grpc_auth_refresh_token_create_from_json(json); if (json != nullptr) grpc_json_destroy(json); gpr_free(scratchpad); return result; } void grpc_auth_refresh_token_destruct(grpc_auth_refresh_token* refresh_token) { if (refresh_token == nullptr) return; refresh_token->type = GRPC_AUTH_JSON_TYPE_INVALID; if (refresh_token->client_id != nullptr) { gpr_free(refresh_token->client_id); refresh_token->client_id = nullptr; } if (refresh_token->client_secret != nullptr) { gpr_free(refresh_token->client_secret); refresh_token->client_secret = nullptr; } if (refresh_token->refresh_token != nullptr) { gpr_free(refresh_token->refresh_token); refresh_token->refresh_token = nullptr; } } // // Oauth2 Token Fetcher credentials. // static void oauth2_token_fetcher_destruct(grpc_call_credentials* creds) { grpc_oauth2_token_fetcher_credentials* c = reinterpret_cast(creds); GRPC_MDELEM_UNREF(c->access_token_md); gpr_mu_destroy(&c->mu); grpc_pollset_set_destroy(grpc_polling_entity_pollset_set(&c->pollent)); grpc_httpcli_context_destroy(&c->httpcli_context); } grpc_credentials_status grpc_oauth2_token_fetcher_credentials_parse_server_response( const grpc_http_response* response, grpc_mdelem* token_md, grpc_millis* token_lifetime) { char* null_terminated_body = nullptr; char* new_access_token = nullptr; grpc_credentials_status status = GRPC_CREDENTIALS_OK; grpc_json* json = nullptr; if (response == nullptr) { gpr_log(GPR_ERROR, "Received NULL response."); status = GRPC_CREDENTIALS_ERROR; goto end; } if (response->body_length > 0) { null_terminated_body = static_cast(gpr_malloc(response->body_length + 1)); null_terminated_body[response->body_length] = '\0'; memcpy(null_terminated_body, response->body, response->body_length); } if (response->status != 200) { gpr_log(GPR_ERROR, "Call to http server ended with error %d [%s].", response->status, null_terminated_body != nullptr ? null_terminated_body : ""); status = GRPC_CREDENTIALS_ERROR; goto end; } else { grpc_json* access_token = nullptr; grpc_json* token_type = nullptr; grpc_json* expires_in = nullptr; grpc_json* ptr; json = grpc_json_parse_string(null_terminated_body); if (json == nullptr) { gpr_log(GPR_ERROR, "Could not parse JSON from %s", null_terminated_body); status = GRPC_CREDENTIALS_ERROR; goto end; } if (json->type != GRPC_JSON_OBJECT) { gpr_log(GPR_ERROR, "Response should be a JSON object"); status = GRPC_CREDENTIALS_ERROR; goto end; } for (ptr = json->child; ptr; ptr = ptr->next) { if (strcmp(ptr->key, "access_token") == 0) { access_token = ptr; } else if (strcmp(ptr->key, "token_type") == 0) { token_type = ptr; } else if (strcmp(ptr->key, "expires_in") == 0) { expires_in = ptr; } } if (access_token == nullptr || access_token->type != GRPC_JSON_STRING) { gpr_log(GPR_ERROR, "Missing or invalid access_token in JSON."); status = GRPC_CREDENTIALS_ERROR; goto end; } if (token_type == nullptr || token_type->type != GRPC_JSON_STRING) { gpr_log(GPR_ERROR, "Missing or invalid token_type in JSON."); status = GRPC_CREDENTIALS_ERROR; goto end; } if (expires_in == nullptr || expires_in->type != GRPC_JSON_NUMBER) { gpr_log(GPR_ERROR, "Missing or invalid expires_in in JSON."); status = GRPC_CREDENTIALS_ERROR; goto end; } gpr_asprintf(&new_access_token, "%s %s", token_type->value, access_token->value); *token_lifetime = strtol(expires_in->value, nullptr, 10) * GPR_MS_PER_SEC; if (!GRPC_MDISNULL(*token_md)) GRPC_MDELEM_UNREF(*token_md); *token_md = grpc_mdelem_from_slices( grpc_slice_from_static_string(GRPC_AUTHORIZATION_METADATA_KEY), grpc_slice_from_copied_string(new_access_token)); status = GRPC_CREDENTIALS_OK; } end: if (status != GRPC_CREDENTIALS_OK && !GRPC_MDISNULL(*token_md)) { GRPC_MDELEM_UNREF(*token_md); *token_md = GRPC_MDNULL; } if (null_terminated_body != nullptr) gpr_free(null_terminated_body); if (new_access_token != nullptr) gpr_free(new_access_token); if (json != nullptr) grpc_json_destroy(json); return status; } static void on_oauth2_token_fetcher_http_response(void* user_data, grpc_error* error) { GRPC_LOG_IF_ERROR("oauth_fetch", GRPC_ERROR_REF(error)); grpc_credentials_metadata_request* r = static_cast(user_data); grpc_oauth2_token_fetcher_credentials* c = reinterpret_cast(r->creds); 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); // Update cache and grab list of pending requests. gpr_mu_lock(&c->mu); c->token_fetch_pending = false; c->access_token_md = GRPC_MDELEM_REF(access_token_md); c->token_expiration = status == GRPC_CREDENTIALS_OK ? gpr_time_add(gpr_now(GPR_CLOCK_MONOTONIC), gpr_time_from_millis(token_lifetime, GPR_TIMESPAN)) : gpr_inf_past(GPR_CLOCK_MONOTONIC); grpc_oauth2_pending_get_request_metadata* pending_request = c->pending_requests; c->pending_requests = nullptr; gpr_mu_unlock(&c->mu); // Invoke callbacks for all pending requests. while (pending_request != nullptr) { 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( "Error occured when fetching oauth2 token.", &error, 1); } GRPC_CLOSURE_SCHED(pending_request->on_request_metadata, error); grpc_polling_entity_del_from_pollset_set( pending_request->pollent, grpc_polling_entity_pollset_set(&c->pollent)); grpc_oauth2_pending_get_request_metadata* prev = pending_request; pending_request = pending_request->next; gpr_free(prev); } GRPC_MDELEM_UNREF(access_token_md); grpc_call_credentials_unref(r->creds); grpc_credentials_metadata_request_destroy(r); } static bool oauth2_token_fetcher_get_request_metadata( grpc_call_credentials* creds, grpc_polling_entity* pollent, grpc_auth_metadata_context context, grpc_credentials_mdelem_array* md_array, grpc_closure* on_request_metadata, grpc_error** error) { grpc_oauth2_token_fetcher_credentials* c = reinterpret_cast(creds); // Check if we can use the cached token. grpc_millis refresh_threshold = GRPC_SECURE_TOKEN_REFRESH_THRESHOLD_SECS * GPR_MS_PER_SEC; grpc_mdelem cached_access_token_md = GRPC_MDNULL; gpr_mu_lock(&c->mu); if (!GRPC_MDISNULL(c->access_token_md) && gpr_time_cmp( gpr_time_sub(c->token_expiration, gpr_now(GPR_CLOCK_MONOTONIC)), gpr_time_from_seconds(GRPC_SECURE_TOKEN_REFRESH_THRESHOLD_SECS, GPR_TIMESPAN)) > 0) { cached_access_token_md = GRPC_MDELEM_REF(c->access_token_md); } if (!GRPC_MDISNULL(cached_access_token_md)) { gpr_mu_unlock(&c->mu); grpc_credentials_mdelem_array_add(md_array, cached_access_token_md); GRPC_MDELEM_UNREF(cached_access_token_md); return true; } // Couldn't get the token from the cache. // Add request to c->pending_requests and start a new fetch if needed. grpc_oauth2_pending_get_request_metadata* pending_request = static_cast( gpr_malloc(sizeof(*pending_request))); pending_request->md_array = md_array; pending_request->on_request_metadata = on_request_metadata; pending_request->pollent = pollent; grpc_polling_entity_add_to_pollset_set( pollent, grpc_polling_entity_pollset_set(&c->pollent)); pending_request->next = c->pending_requests; c->pending_requests = pending_request; bool start_fetch = false; if (!c->token_fetch_pending) { c->token_fetch_pending = true; start_fetch = true; } gpr_mu_unlock(&c->mu); if (start_fetch) { grpc_call_credentials_ref(creds); c->fetch_func(grpc_credentials_metadata_request_create(creds), &c->httpcli_context, &c->pollent, on_oauth2_token_fetcher_http_response, grpc_core::ExecCtx::Get()->Now() + refresh_threshold); } return false; } static void oauth2_token_fetcher_cancel_get_request_metadata( grpc_call_credentials* creds, grpc_credentials_mdelem_array* md_array, grpc_error* error) { grpc_oauth2_token_fetcher_credentials* c = reinterpret_cast(creds); gpr_mu_lock(&c->mu); grpc_oauth2_pending_get_request_metadata* prev = nullptr; grpc_oauth2_pending_get_request_metadata* pending_request = c->pending_requests; while (pending_request != nullptr) { if (pending_request->md_array == md_array) { // Remove matching pending request from the list. if (prev != nullptr) { prev->next = pending_request->next; } else { c->pending_requests = pending_request->next; } // Invoke the callback immediately with an error. GRPC_CLOSURE_SCHED(pending_request->on_request_metadata, GRPC_ERROR_REF(error)); gpr_free(pending_request); break; } prev = pending_request; pending_request = pending_request->next; } gpr_mu_unlock(&c->mu); GRPC_ERROR_UNREF(error); } static void init_oauth2_token_fetcher(grpc_oauth2_token_fetcher_credentials* c, grpc_fetch_oauth2_func fetch_func) { memset(c, 0, sizeof(grpc_oauth2_token_fetcher_credentials)); c->base.type = GRPC_CALL_CREDENTIALS_TYPE_OAUTH2; gpr_ref_init(&c->base.refcount, 1); gpr_mu_init(&c->mu); c->token_expiration = gpr_inf_past(GPR_CLOCK_MONOTONIC); c->fetch_func = fetch_func; c->pollent = grpc_polling_entity_create_from_pollset_set(grpc_pollset_set_create()); grpc_httpcli_context_init(&c->httpcli_context); } // // Google Compute Engine credentials. // static grpc_call_credentials_vtable compute_engine_vtable = { oauth2_token_fetcher_destruct, oauth2_token_fetcher_get_request_metadata, oauth2_token_fetcher_cancel_get_request_metadata}; static void compute_engine_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*)"Metadata-Flavor", (char*)"Google"}; grpc_httpcli_request request; memset(&request, 0, sizeof(grpc_httpcli_request)); request.host = (char*)GRPC_COMPUTE_ENGINE_METADATA_HOST; request.http.path = (char*)GRPC_COMPUTE_ENGINE_METADATA_TOKEN_PATH; request.http.hdr_count = 1; request.http.hdrs = &header; /* 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"); grpc_httpcli_get( httpcli_context, pollent, resource_quota, &request, deadline, GRPC_CLOSURE_CREATE(response_cb, metadata_req, grpc_schedule_on_exec_ctx), &metadata_req->response); grpc_resource_quota_unref_internal(resource_quota); } grpc_call_credentials* grpc_google_compute_engine_credentials_create( void* reserved) { grpc_oauth2_token_fetcher_credentials* c = static_cast( gpr_malloc(sizeof(grpc_oauth2_token_fetcher_credentials))); GRPC_API_TRACE("grpc_compute_engine_credentials_create(reserved=%p)", 1, (reserved)); GPR_ASSERT(reserved == nullptr); init_oauth2_token_fetcher(c, compute_engine_fetch_oauth2); c->base.vtable = &compute_engine_vtable; return &c->base; } // // Google Refresh Token credentials. // static void refresh_token_destruct(grpc_call_credentials* creds) { grpc_google_refresh_token_credentials* c = reinterpret_cast(creds); grpc_auth_refresh_token_destruct(&c->refresh_token); oauth2_token_fetcher_destruct(&c->base.base); } static grpc_call_credentials_vtable refresh_token_vtable = { refresh_token_destruct, oauth2_token_fetcher_get_request_metadata, oauth2_token_fetcher_cancel_get_request_metadata}; static void refresh_token_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_google_refresh_token_credentials* c = reinterpret_cast( metadata_req->creds); grpc_http_header header = {(char*)"Content-Type", (char*)"application/x-www-form-urlencoded"}; grpc_httpcli_request request; char* body = nullptr; gpr_asprintf(&body, GRPC_REFRESH_TOKEN_POST_BODY_FORMAT_STRING, c->refresh_token.client_id, c->refresh_token.client_secret, c->refresh_token.refresh_token); memset(&request, 0, sizeof(grpc_httpcli_request)); request.host = (char*)GRPC_GOOGLE_OAUTH2_SERVICE_HOST; request.http.path = (char*)GRPC_GOOGLE_OAUTH2_SERVICE_TOKEN_PATH; request.http.hdr_count = 1; request.http.hdrs = &header; request.handshaker = &grpc_httpcli_ssl; /* 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( 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_resource_quota_unref_internal(resource_quota); gpr_free(body); } grpc_call_credentials* grpc_refresh_token_credentials_create_from_auth_refresh_token( grpc_auth_refresh_token refresh_token) { grpc_google_refresh_token_credentials* c; if (!grpc_auth_refresh_token_is_valid(&refresh_token)) { gpr_log(GPR_ERROR, "Invalid input for refresh token credentials creation"); return nullptr; } c = static_cast( gpr_zalloc(sizeof(grpc_google_refresh_token_credentials))); init_oauth2_token_fetcher(&c->base, refresh_token_fetch_oauth2); c->base.base.vtable = &refresh_token_vtable; c->refresh_token = refresh_token; return &c->base.base; } static char* create_loggable_refresh_token(grpc_auth_refresh_token* token) { if (strcmp(token->type, GRPC_AUTH_JSON_TYPE_INVALID) == 0) { return gpr_strdup(""); } char* loggable_token = nullptr; gpr_asprintf(&loggable_token, "{\n type: %s\n client_id: %s\n client_secret: " "\n refresh_token: \n}", token->type, token->client_id); return loggable_token; } grpc_call_credentials* grpc_google_refresh_token_credentials_create( const char* json_refresh_token, void* reserved) { grpc_auth_refresh_token token = grpc_auth_refresh_token_create_from_string(json_refresh_token); if (grpc_api_trace.enabled()) { char* loggable_token = create_loggable_refresh_token(&token); gpr_log(GPR_INFO, "grpc_refresh_token_credentials_create(json_refresh_token=%s, " "reserved=%p)", loggable_token, reserved); gpr_free(loggable_token); } GPR_ASSERT(reserved == nullptr); return grpc_refresh_token_credentials_create_from_auth_refresh_token(token); } // // Oauth2 Access Token credentials. // static void access_token_destruct(grpc_call_credentials* creds) { grpc_access_token_credentials* c = reinterpret_cast(creds); GRPC_MDELEM_UNREF(c->access_token_md); } static bool access_token_get_request_metadata( grpc_call_credentials* creds, grpc_polling_entity* pollent, grpc_auth_metadata_context context, grpc_credentials_mdelem_array* md_array, grpc_closure* on_request_metadata, grpc_error** error) { grpc_access_token_credentials* c = reinterpret_cast(creds); grpc_credentials_mdelem_array_add(md_array, c->access_token_md); return true; } static void access_token_cancel_get_request_metadata( grpc_call_credentials* c, grpc_credentials_mdelem_array* md_array, grpc_error* error) { GRPC_ERROR_UNREF(error); } static grpc_call_credentials_vtable access_token_vtable = { access_token_destruct, access_token_get_request_metadata, access_token_cancel_get_request_metadata}; grpc_call_credentials* grpc_access_token_credentials_create( const char* access_token, void* reserved) { grpc_access_token_credentials* c = static_cast( gpr_zalloc(sizeof(grpc_access_token_credentials))); GRPC_API_TRACE( "grpc_access_token_credentials_create(access_token=, " "reserved=%p)", 1, (reserved)); GPR_ASSERT(reserved == nullptr); c->base.type = GRPC_CALL_CREDENTIALS_TYPE_OAUTH2; c->base.vtable = &access_token_vtable; gpr_ref_init(&c->base.refcount, 1); char* token_md_value; gpr_asprintf(&token_md_value, "Bearer %s", access_token); grpc_core::ExecCtx exec_ctx; c->access_token_md = grpc_mdelem_from_slices( grpc_slice_from_static_string(GRPC_AUTHORIZATION_METADATA_KEY), grpc_slice_from_copied_string(token_md_value)); gpr_free(token_md_value); return &c->base; }