Skip to content
This repository has been archived by the owner on Nov 6, 2023. It is now read-only.

Commit

Permalink
Support user provided "scope" in JWT and GDC (grpc#26577)
Browse files Browse the repository at this point in the history
* support scope overriding in jwt and gdc

* fix formatting

* fix bazel build issue

* fix clang tidy
  • Loading branch information
yihuazhang authored Jul 1, 2021
1 parent f835f3f commit 6df9679
Show file tree
Hide file tree
Showing 17 changed files with 318 additions and 73 deletions.
23 changes: 18 additions & 5 deletions include/grpc/grpc_security.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,17 @@ GRPCAPI void grpc_channel_credentials_release(grpc_channel_credentials* creds);
If nullptr is supplied, the returned channel credentials object will use a
call credentials object based on the Application Default Credentials
mechanism.
user_provided_scope is an optional field for user to use in the JWT token to
represent the scope field. It will only be used if a service account JWT
access credential is created by the application default credentials
mechanism. If user_provided_scope is not nullptr, the audience (service URL)
field in the JWT token will be cleared, which is dictated by
https://google.aip.dev/auth/4111. Also note that user_provided_scope will be
ignored if the call_credentials is not nullptr.
*/
GRPCAPI grpc_channel_credentials* grpc_google_default_credentials_create(
grpc_call_credentials* call_credentials);
grpc_call_credentials* call_credentials, const char* user_provided_scope);

/** Callback for getting the SSL roots override from the application.
In case of success, *pem_roots_certs must be set to a NULL terminated string
Expand Down Expand Up @@ -324,11 +332,16 @@ GRPCAPI gpr_timespec grpc_max_auth_token_lifetime(void);
- json_key is the JSON key string containing the client's private key.
- token_lifetime is the lifetime of each Json Web Token (JWT) created with
this credentials. It should not exceed grpc_max_auth_token_lifetime or
will be cropped to this value. */
will be cropped to this value.
- user_provided_scope is an optional field for user to use in the JWT token
to represent the scope field.
- clear_audience dictating the clearance of audience field when it
is set to a non-zero value AND user_provided_scope is not nullptr.
*/
GRPCAPI grpc_call_credentials*
grpc_service_account_jwt_access_credentials_create(const char* json_key,
gpr_timespec token_lifetime,
void* reserved);
grpc_service_account_jwt_access_credentials_create(
const char* json_key, gpr_timespec token_lifetime,
const char* user_provided_scope, int clear_audience);

/** Builds External Account credentials.
- json_string is the JSON string containing the credentials options.
Expand Down
18 changes: 16 additions & 2 deletions include/grpcpp/security/credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,19 @@ struct SslCredentialsOptions {

/// Builds credentials with reasonable defaults.
///
/// user_provided_scope is an optional field for user to use to represent the
/// scope field in the JWT token. It will only be used if a service account JWT
/// access credential is created by the application default credentials
/// mechanism. If user_provided_scope is not empty, the audience (service URL)
/// field in the JWT token will be cleared, which is dictated by
/// https://google.aip.dev/auth/4111.
///
/// \warning Only use these credentials when connecting to a Google endpoint.
/// Using these credentials to connect to any other service may result in this
/// service being able to impersonate your client for requests to Google
/// services.
std::shared_ptr<ChannelCredentials> GoogleDefaultCredentials();
std::shared_ptr<ChannelCredentials> GoogleDefaultCredentials(
const grpc::string& user_provided_scope = "");

/// Builds SSL Credentials given SSL specific options
std::shared_ptr<ChannelCredentials> SslCredentials(
Expand All @@ -197,9 +205,15 @@ constexpr long kMaxAuthTokenLifetimeSecs = 3600;
/// token_lifetime_seconds is the lifetime in seconds of each Json Web Token
/// (JWT) created with this credentials. It should not exceed
/// \a kMaxAuthTokenLifetimeSecs or will be cropped to this value.
/// user_provided_scope is an optional field for user to use to represent the
/// scope field in the JWT token.
/// clear_audience is a boolean field that dictates clearing audience
/// field in the JWT token when it is set to true AND user_provided_scope is
/// not empty. */
std::shared_ptr<CallCredentials> ServiceAccountJWTAccessCredentials(
const grpc::string& json_key,
long token_lifetime_seconds = kMaxAuthTokenLifetimeSecs);
long token_lifetime_seconds = kMaxAuthTokenLifetimeSecs,
const grpc::string& user_provided_scope = "", bool clear_audience = false);

/// Builds refresh token credentials.
/// json_refresh_token is the JSON string containing the refresh token along
Expand Down
2 changes: 1 addition & 1 deletion src/core/ext/xds/xds_bootstrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ RefCountedPtr<grpc_channel_credentials>
XdsChannelCredsRegistry::MakeChannelCreds(const std::string& creds_type,
const Json& /*config*/) {
if (creds_type == "google_default") {
return grpc_google_default_credentials_create(nullptr);
return grpc_google_default_credentials_create(nullptr, nullptr);
} else if (creds_type == "insecure") {
return grpc_insecure_credentials_create();
} else if (creds_type == "fake") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ static int is_metadata_server_reachable() {

/* Takes ownership of creds_path if not NULL. */
static grpc_error_handle create_default_creds_from_path(
const std::string& creds_path,
const std::string& creds_path, const char* user_provided_scope,
grpc_core::RefCountedPtr<grpc_call_credentials>* creds) {
grpc_auth_json_key key;
grpc_auth_refresh_token token;
Expand All @@ -250,9 +250,11 @@ static grpc_error_handle create_default_creds_from_path(
/* First, try an auth json key. */
key = grpc_auth_json_key_create_from_json(json);
if (grpc_auth_json_key_is_valid(&key)) {
if (user_provided_scope == nullptr) user_provided_scope = "";
result =
grpc_service_account_jwt_access_credentials_create_from_auth_json_key(
key, grpc_max_auth_token_lifetime());
key, grpc_max_auth_token_lifetime(), user_provided_scope,
true /* clear_audience */);
if (result == nullptr) {
error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"grpc_service_account_jwt_access_credentials_create_from_auth_json_"
Expand Down Expand Up @@ -306,22 +308,24 @@ static bool metadata_server_available() {
}

static grpc_core::RefCountedPtr<grpc_call_credentials> make_default_call_creds(
grpc_error_handle* error) {
const char* user_provided_scope, grpc_error_handle* error) {
grpc_core::RefCountedPtr<grpc_call_credentials> call_creds;
grpc_error_handle err;

/* First, try the environment variable. */
char* path_from_env = gpr_getenv(GRPC_GOOGLE_CREDENTIALS_ENV_VAR);
if (path_from_env != nullptr) {
err = create_default_creds_from_path(path_from_env, &call_creds);
err = create_default_creds_from_path(path_from_env, user_provided_scope,
&call_creds);
gpr_free(path_from_env);
if (err == GRPC_ERROR_NONE) return call_creds;
*error = grpc_error_add_child(*error, err);
}

/* Then the well-known file. */
err = create_default_creds_from_path(
grpc_get_well_known_google_credentials_file_path(), &call_creds);
grpc_get_well_known_google_credentials_file_path(), user_provided_scope,
&call_creds);
if (err == GRPC_ERROR_NONE) return call_creds;
*error = grpc_error_add_child(*error, err);

Expand All @@ -343,7 +347,7 @@ static grpc_core::RefCountedPtr<grpc_call_credentials> make_default_call_creds(
}

grpc_channel_credentials* grpc_google_default_credentials_create(
grpc_call_credentials* call_credentials) {
grpc_call_credentials* call_credentials, const char* user_provided_scope) {
grpc_channel_credentials* result = nullptr;
grpc_core::RefCountedPtr<grpc_call_credentials> call_creds(call_credentials);
grpc_error_handle error = GRPC_ERROR_NONE;
Expand All @@ -353,7 +357,7 @@ grpc_channel_credentials* grpc_google_default_credentials_create(
(call_credentials));

if (call_creds == nullptr) {
call_creds = make_default_call_creds(&error);
call_creds = make_default_call_creds(user_provided_scope, &error);
}

if (call_creds != nullptr) {
Expand Down
15 changes: 11 additions & 4 deletions src/core/lib/security/credentials/jwt/json_token.cc
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ static char* encoded_jwt_header(const char* key_id, const char* algorithm) {

static char* encoded_jwt_claim(const grpc_auth_json_key* json_key,
const char* audience,
gpr_timespec token_lifetime, const char* scope) {
gpr_timespec token_lifetime, const char* scope,
bool clear_audience) {
gpr_timespec now = gpr_now(GPR_CLOCK_REALTIME);
gpr_timespec expiration = gpr_time_add(now, token_lifetime);
if (gpr_time_cmp(token_lifetime, grpc_max_auth_token_lifetime()) > 0) {
Expand All @@ -175,15 +176,19 @@ static char* encoded_jwt_claim(const grpc_auth_json_key* json_key,

Json::Object object = {
{"iss", json_key->client_email},
{"aud", audience},
{"iat", now.tv_sec},
{"exp", expiration.tv_sec},
};

if (scope != nullptr) {
object["scope"] = scope;
if (!clear_audience) {
object["aud"] = audience;
}
} else {
/* Unscoped JWTs need a sub field. */
object["sub"] = json_key->client_email;
object["aud"] = audience;
}

Json json(object);
Expand Down Expand Up @@ -264,15 +269,17 @@ char* compute_and_encode_signature(const grpc_auth_json_key* json_key,

char* grpc_jwt_encode_and_sign(const grpc_auth_json_key* json_key,
const char* audience,
gpr_timespec token_lifetime, const char* scope) {
gpr_timespec token_lifetime, const char* scope,
bool clear_audience) {
if (g_jwt_encode_and_sign_override != nullptr) {
return g_jwt_encode_and_sign_override(json_key, audience, token_lifetime,
scope);
} else {
const char* sig_algo = GRPC_JWT_RSA_SHA256_ALGORITHM;
char* to_sign = dot_concat_and_free_strings(
encoded_jwt_header(json_key->private_key_id, sig_algo),
encoded_jwt_claim(json_key, audience, token_lifetime, scope));
encoded_jwt_claim(json_key, audience, token_lifetime, scope,
clear_audience));
char* sig = compute_and_encode_signature(json_key, sig_algo, to_sign);
if (sig == nullptr) {
gpr_free(to_sign);
Expand Down
7 changes: 5 additions & 2 deletions src/core/lib/security/credentials/jwt/json_token.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ void grpc_auth_json_key_destruct(grpc_auth_json_key* json_key);
/* --- json token encoding and signing. --- */

/* Caller is responsible for calling gpr_free on the returned value. May return
NULL on invalid input. The scope parameter may be NULL. */
NULL on invalid input. The scope parameter may be NULL. The
clear_audience parameter dictates the clearing of audience field when
it is set to a non-zero value AND scope is not nullptr. */
char* grpc_jwt_encode_and_sign(const grpc_auth_json_key* json_key,
const char* audience,
gpr_timespec token_lifetime, const char* scope);
gpr_timespec token_lifetime, const char* scope,
bool clear_audience);

/* Override encode_and_sign function for testing. */
typedef char* (*grpc_jwt_encode_and_sign_override)(
Expand Down
39 changes: 27 additions & 12 deletions src/core/lib/security/credentials/jwt/jwt_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,17 @@ bool grpc_service_account_jwt_access_credentials::get_request_metadata(
grpc_closure* /*on_request_metadata*/, grpc_error_handle* error) {
gpr_timespec refresh_threshold = gpr_time_from_seconds(
GRPC_SECURE_TOKEN_REFRESH_THRESHOLD_SECS, GPR_TIMESPAN);

absl::string_view scope = user_provided_scope_;
/* See if we can return a cached jwt. */
grpc_mdelem jwt_md = GRPC_MDNULL;
{
gpr_mu_lock(&cache_mu_);
// We use the cached token if
// 1) cached_.service_url is equal to context.service_url OR
// 2) scope is not emtpy AND clear_audience_ is a non-zero value.
if (cached_.service_url != nullptr &&
strcmp(cached_.service_url, context.service_url) == 0 &&
(strcmp(cached_.service_url, context.service_url) == 0 ||
(!scope.empty() && clear_audience_)) &&
!GRPC_MDISNULL(cached_.jwt_md) &&
(gpr_time_cmp(
gpr_time_sub(cached_.jwt_expiration, gpr_now(GPR_CLOCK_REALTIME)),
Expand All @@ -84,7 +88,8 @@ bool grpc_service_account_jwt_access_credentials::get_request_metadata(
gpr_mu_lock(&cache_mu_);
reset_cache();
jwt = grpc_jwt_encode_and_sign(&key_, context.service_url, jwt_lifetime_,
nullptr);
scope.empty() ? nullptr : scope.data(),
clear_audience_);
if (jwt != nullptr) {
std::string md_value = absl::StrCat("Bearer ", jwt);
gpr_free(jwt);
Expand Down Expand Up @@ -115,8 +120,13 @@ void grpc_service_account_jwt_access_credentials::cancel_get_request_metadata(

grpc_service_account_jwt_access_credentials::
grpc_service_account_jwt_access_credentials(grpc_auth_json_key key,
gpr_timespec token_lifetime)
: grpc_call_credentials(GRPC_CALL_CREDENTIALS_TYPE_JWT), key_(key) {
gpr_timespec token_lifetime,
std::string user_provided_scope,
bool clear_audience)
: grpc_call_credentials(GRPC_CALL_CREDENTIALS_TYPE_JWT),
key_(key),
user_provided_scope_(std::move(user_provided_scope)),
clear_audience_(clear_audience) {
gpr_timespec max_token_lifetime = grpc_max_auth_token_lifetime();
if (gpr_time_cmp(token_lifetime, max_token_lifetime) > 0) {
gpr_log(GPR_INFO,
Expand All @@ -131,13 +141,14 @@ grpc_service_account_jwt_access_credentials::

grpc_core::RefCountedPtr<grpc_call_credentials>
grpc_service_account_jwt_access_credentials_create_from_auth_json_key(
grpc_auth_json_key key, gpr_timespec token_lifetime) {
grpc_auth_json_key key, gpr_timespec token_lifetime,
std::string user_provided_scope, bool clear_audience) {
if (!grpc_auth_json_key_is_valid(&key)) {
gpr_log(GPR_ERROR, "Invalid input for jwt credentials creation");
return nullptr;
}
return grpc_core::MakeRefCounted<grpc_service_account_jwt_access_credentials>(
key, token_lifetime);
key, token_lifetime, std::move(user_provided_scope), clear_audience);
}

static char* redact_private_key(const char* json_key) {
Expand All @@ -152,7 +163,8 @@ static char* redact_private_key(const char* json_key) {
}

grpc_call_credentials* grpc_service_account_jwt_access_credentials_create(
const char* json_key, gpr_timespec token_lifetime, void* reserved) {
const char* json_key, gpr_timespec token_lifetime,
const char* user_provided_scope, int clear_audience) {
if (GRPC_TRACE_FLAG_ENABLED(grpc_api_trace)) {
char* clean_json = redact_private_key(json_key);
gpr_log(GPR_INFO,
Expand All @@ -161,15 +173,18 @@ grpc_call_credentials* grpc_service_account_jwt_access_credentials_create(
"token_lifetime="
"gpr_timespec { tv_sec: %" PRId64
", tv_nsec: %d, clock_type: %d }, "
"reserved=%p)",
"user_provided_scope=%s, "
"clear_audience=%d)",
clean_json, token_lifetime.tv_sec, token_lifetime.tv_nsec,
static_cast<int>(token_lifetime.clock_type), reserved);
static_cast<int>(token_lifetime.clock_type), user_provided_scope,
clear_audience);
gpr_free(clean_json);
}
GPR_ASSERT(reserved == nullptr);
grpc_core::ApplicationCallbackExecCtx callback_exec_ctx;
grpc_core::ExecCtx exec_ctx;
if (user_provided_scope == nullptr) user_provided_scope = "";
return grpc_service_account_jwt_access_credentials_create_from_auth_json_key(
grpc_auth_json_key_create_from_string(json_key), token_lifetime)
grpc_auth_json_key_create_from_string(json_key), token_lifetime,
user_provided_scope, clear_audience)
.release();
}
13 changes: 10 additions & 3 deletions src/core/lib/security/credentials/jwt/jwt_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class grpc_service_account_jwt_access_credentials
: public grpc_call_credentials {
public:
grpc_service_account_jwt_access_credentials(grpc_auth_json_key key,
gpr_timespec token_lifetime);
gpr_timespec token_lifetime,
std::string user_provided_scope,
bool clear_audience);
~grpc_service_account_jwt_access_credentials() override;

bool get_request_metadata(grpc_polling_entity* pollent,
Expand All @@ -48,7 +50,9 @@ class grpc_service_account_jwt_access_credentials

const gpr_timespec& jwt_lifetime() const { return jwt_lifetime_; }
const grpc_auth_json_key& key() const { return key_; }

const std::string& user_provided_scope() const {
return user_provided_scope_;
}
std::string debug_string() override {
return absl::StrFormat(
"JWTAccessCredentials{ExpirationTime:%s}",
Expand All @@ -70,12 +74,15 @@ class grpc_service_account_jwt_access_credentials

grpc_auth_json_key key_;
gpr_timespec jwt_lifetime_;
std::string user_provided_scope_;
bool clear_audience_;
};

// Private constructor for jwt credentials from an already parsed json key.
// Takes ownership of the key.
grpc_core::RefCountedPtr<grpc_call_credentials>
grpc_service_account_jwt_access_credentials_create_from_auth_json_key(
grpc_auth_json_key key, gpr_timespec token_lifetime);
grpc_auth_json_key key, gpr_timespec token_lifetime,
std::string user_provided_scope, bool clear_audience);

#endif /* GRPC_CORE_LIB_SECURITY_CREDENTIALS_JWT_JWT_CREDENTIALS_H */
14 changes: 10 additions & 4 deletions src/cpp/client/secure_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,13 @@ std::shared_ptr<CallCredentials> WrapCallCredentials(
}
} // namespace

std::shared_ptr<ChannelCredentials> GoogleDefaultCredentials() {
std::shared_ptr<ChannelCredentials> GoogleDefaultCredentials(
const grpc::string& user_provided_scope) {
grpc::GrpcLibraryCodegen init; // To call grpc_init().
return internal::WrapChannelCredentials(
grpc_google_default_credentials_create(nullptr));
grpc_google_default_credentials_create(
nullptr,
user_provided_scope.empty() ? nullptr : user_provided_scope.c_str()));
}

std::shared_ptr<CallCredentials> ExternalAccountCredentials(
Expand Down Expand Up @@ -320,7 +323,8 @@ std::shared_ptr<CallCredentials> GoogleComputeEngineCredentials() {

// Builds JWT credentials.
std::shared_ptr<CallCredentials> ServiceAccountJWTAccessCredentials(
const std::string& json_key, long token_lifetime_seconds) {
const std::string& json_key, long token_lifetime_seconds,
const grpc::string& user_provided_scope, bool clear_audience) {
grpc::GrpcLibraryCodegen init; // To call grpc_init().
if (token_lifetime_seconds <= 0) {
gpr_log(GPR_ERROR,
Expand All @@ -330,7 +334,9 @@ std::shared_ptr<CallCredentials> ServiceAccountJWTAccessCredentials(
gpr_timespec lifetime =
gpr_time_from_seconds(token_lifetime_seconds, GPR_TIMESPAN);
return WrapCallCredentials(grpc_service_account_jwt_access_credentials_create(
json_key.c_str(), lifetime, nullptr));
json_key.c_str(), lifetime,
user_provided_scope.empty() ? nullptr : user_provided_scope.c_str(),
clear_audience));
}

// Builds refresh token credentials.
Expand Down
Loading

0 comments on commit 6df9679

Please sign in to comment.