blob: ad63b01e754d7945ed93214c120601cbb46e3d21 [file] [log] [blame]
/*
*
* 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 <grpc/support/port_platform.h>
#include "src/core/lib/security/credentials/oauth2/oauth2_credentials.h"
#include <string.h>
#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 <grpc/support/alloc.h>
#include <grpc/support/log.h>
#include <grpc/support/string_util.h>
//
// 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.
//
grpc_oauth2_token_fetcher_credentials::
~grpc_oauth2_token_fetcher_credentials() {
GRPC_MDELEM_UNREF(access_token_md_);
gpr_mu_destroy(&mu_);
grpc_pollset_set_destroy(grpc_polling_entity_pollset_set(&pollent_));
grpc_httpcli_context_destroy(&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<char*>(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<grpc_credentials_metadata_request*>(user_data);
grpc_oauth2_token_fetcher_credentials* c =
reinterpret_cast<grpc_oauth2_token_fetcher_credentials*>(r->creds.get());
c->on_http_response(r, error);
}
void grpc_oauth2_token_fetcher_credentials::on_http_response(
grpc_credentials_metadata_request* r, grpc_error* error) {
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(&mu_);
token_fetch_pending_ = false;
access_token_md_ = GRPC_MDELEM_REF(access_token_md);
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 = pending_requests_;
pending_requests_ = nullptr;
gpr_mu_unlock(&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 occurred 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(&pollent_));
grpc_oauth2_pending_get_request_metadata* prev = pending_request;
pending_request = pending_request->next;
gpr_free(prev);
}
GRPC_MDELEM_UNREF(access_token_md);
Unref();
grpc_credentials_metadata_request_destroy(r);
}
bool grpc_oauth2_token_fetcher_credentials::get_request_metadata(
grpc_polling_entity* pollent, grpc_auth_metadata_context context,
grpc_credentials_mdelem_array* md_array, grpc_closure* on_request_metadata,
grpc_error** error) {
// 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(&mu_);
if (!GRPC_MDISNULL(access_token_md_) &&
gpr_time_cmp(
gpr_time_sub(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(access_token_md_);
}
if (!GRPC_MDISNULL(cached_access_token_md)) {
gpr_mu_unlock(&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 pending_requests_ and start a new fetch if needed.
grpc_oauth2_pending_get_request_metadata* pending_request =
static_cast<grpc_oauth2_pending_get_request_metadata*>(
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(&pollent_));
pending_request->next = pending_requests_;
pending_requests_ = pending_request;
bool start_fetch = false;
if (!token_fetch_pending_) {
token_fetch_pending_ = true;
start_fetch = true;
}
gpr_mu_unlock(&mu_);
if (start_fetch) {
Ref().release();
fetch_oauth2(grpc_credentials_metadata_request_create(this->Ref()),
&httpcli_context_, &pollent_,
on_oauth2_token_fetcher_http_response,
grpc_core::ExecCtx::Get()->Now() + refresh_threshold);
}
return false;
}
void grpc_oauth2_token_fetcher_credentials::cancel_get_request_metadata(
grpc_credentials_mdelem_array* md_array, grpc_error* error) {
gpr_mu_lock(&mu_);
grpc_oauth2_pending_get_request_metadata* prev = nullptr;
grpc_oauth2_pending_get_request_metadata* pending_request = 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 {
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(&mu_);
GRPC_ERROR_UNREF(error);
}
grpc_oauth2_token_fetcher_credentials::grpc_oauth2_token_fetcher_credentials()
: grpc_call_credentials(GRPC_CALL_CREDENTIALS_TYPE_OAUTH2),
token_expiration_(gpr_inf_past(GPR_CLOCK_MONOTONIC)),
pollent_(grpc_polling_entity_create_from_pollset_set(
grpc_pollset_set_create())) {
gpr_mu_init(&mu_);
grpc_httpcli_context_init(&httpcli_context_);
}
//
// Google Compute Engine credentials.
//
namespace {
class grpc_compute_engine_token_fetcher_credentials
: public grpc_oauth2_token_fetcher_credentials {
public:
grpc_compute_engine_token_fetcher_credentials() = default;
~grpc_compute_engine_token_fetcher_credentials() override = default;
protected:
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 {
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(http_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);
}
};
} // namespace
grpc_call_credentials* grpc_google_compute_engine_credentials_create(
void* reserved) {
GRPC_API_TRACE("grpc_compute_engine_credentials_create(reserved=%p)", 1,
(reserved));
GPR_ASSERT(reserved == nullptr);
return grpc_core::MakeRefCounted<
grpc_compute_engine_token_fetcher_credentials>()
.release();
}
//
// Google Refresh Token credentials.
//
grpc_google_refresh_token_credentials::
~grpc_google_refresh_token_credentials() {
grpc_auth_refresh_token_destruct(&refresh_token_);
}
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_httpcli_request request;
char* body = nullptr;
gpr_asprintf(&body, GRPC_REFRESH_TOKEN_POST_BODY_FORMAT_STRING,
refresh_token_.client_id, refresh_token_.client_secret,
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_google_refresh_token_credentials::grpc_google_refresh_token_credentials(
grpc_auth_refresh_token refresh_token)
: refresh_token_(refresh_token) {}
grpc_core::RefCountedPtr<grpc_call_credentials>
grpc_refresh_token_credentials_create_from_auth_refresh_token(
grpc_auth_refresh_token refresh_token) {
if (!grpc_auth_refresh_token_is_valid(&refresh_token)) {
gpr_log(GPR_ERROR, "Invalid input for refresh token credentials creation");
return nullptr;
}
return grpc_core::MakeRefCounted<grpc_google_refresh_token_credentials>(
refresh_token);
}
static char* create_loggable_refresh_token(grpc_auth_refresh_token* token) {
if (strcmp(token->type, GRPC_AUTH_JSON_TYPE_INVALID) == 0) {
return gpr_strdup("<Invalid json token>");
}
char* loggable_token = nullptr;
gpr_asprintf(&loggable_token,
"{\n type: %s\n client_id: %s\n client_secret: "
"<redacted>\n refresh_token: <redacted>\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)
.release();
}
//
// Oauth2 Access Token credentials.
//
grpc_access_token_credentials::~grpc_access_token_credentials() {
GRPC_MDELEM_UNREF(access_token_md_);
}
bool grpc_access_token_credentials::get_request_metadata(
grpc_polling_entity* pollent, grpc_auth_metadata_context context,
grpc_credentials_mdelem_array* md_array, grpc_closure* on_request_metadata,
grpc_error** error) {
grpc_credentials_mdelem_array_add(md_array, access_token_md_);
return true;
}
void grpc_access_token_credentials::cancel_get_request_metadata(
grpc_credentials_mdelem_array* md_array, grpc_error* error) {
GRPC_ERROR_UNREF(error);
}
grpc_access_token_credentials::grpc_access_token_credentials(
const char* access_token)
: grpc_call_credentials(GRPC_CALL_CREDENTIALS_TYPE_OAUTH2) {
char* token_md_value;
gpr_asprintf(&token_md_value, "Bearer %s", access_token);
grpc_core::ExecCtx exec_ctx;
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);
}
grpc_call_credentials* grpc_access_token_credentials_create(
const char* access_token, void* reserved) {
GRPC_API_TRACE(
"grpc_access_token_credentials_create(access_token=<redacted>, "
"reserved=%p)",
1, (reserved));
GPR_ASSERT(reserved == nullptr);
return grpc_core::MakeRefCounted<grpc_access_token_credentials>(access_token)
.release();
}