Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 102 additions & 6 deletions google/cloud/internal/oauth2_external_account_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

#include "google/cloud/internal/oauth2_external_account_credentials.h"
#include "google/cloud/internal/base64_transforms.h"
Comment thread
scotthart marked this conversation as resolved.
#include "google/cloud/internal/external_account_token_source_aws.h"
#include "google/cloud/internal/external_account_token_source_file.h"
#include "google/cloud/internal/external_account_token_source_url.h"
Expand All @@ -26,6 +27,7 @@
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include <nlohmann/json.hpp>
#include <mutex>
#include <regex>

namespace google {
Expand Down Expand Up @@ -151,11 +153,48 @@ StatusOr<ExternalAccountInfo> ParseExternalAccountConfiguration(
}
auto type = ValidateStringField(json, "type", "credentials-file", ec);
if (!type) return std::move(type).status();
if (*type != "external_account") {
if (*type != "external_account" &&
*type != "external_account_authorized_user") {
return InvalidArgumentError(
"mismatched type (" + *type + ") in external account configuration",
GCP_ERROR_INFO().WithContext(ec));
}
auto token_url =
ValidateStringField(json, "token_url", "credentials-file", ec);
if (!token_url) return std::move(token_url).status();
auto universe_domain = GetUniverseDomainFromCredentialsJson(json);
if (!universe_domain) return std::move(universe_domain).status();

if (*type == "external_account_authorized_user") {
StatusOr<std::string> client_id =
ValidateStringField(json, "client_id", "credentials-file", ec);
if (!client_id) return std::move(client_id).status();
StatusOr<std::string> client_secret =
ValidateStringField(json, "client_secret", "credentials-file", ec);
if (!client_secret) return std::move(client_secret).status();
StatusOr<std::string> refresh_token =
ValidateStringField(json, "refresh_token", "credentials-file", ec);
if (!refresh_token) return std::move(refresh_token).status();
StatusOr<std::string> audience =
ValidateStringField(json, "audience", "credentials-file", "", ec);
if (!audience) return std::move(audience).status();
std::variant<std::monostate, WorkforceIdentityFederationInfo,
WorkloadIdentityFederationInfo>
identity_federation = IdentityFederationFromAudience(*audience);

return ExternalAccountInfo{
*std::move(audience),
{},
*std::move(token_url),
{},
std::nullopt,
*std::move(universe_domain),
std::nullopt,
std::move(identity_federation),
ExternalAccountAuthorizedUserInfo{*std::move(client_id),
*std::move(client_secret),
*std::move(refresh_token)}};
}

auto audience = ValidateStringField(json, "audience", "credentials-file", ec);
if (!audience) return std::move(audience).status();
Expand All @@ -164,11 +203,6 @@ StatusOr<ExternalAccountInfo> ParseExternalAccountConfiguration(
auto subject_token_type =
ValidateStringField(json, "subject_token_type", "credentials-file", ec);
if (!subject_token_type) return std::move(subject_token_type).status();
auto token_url =
ValidateStringField(json, "token_url", "credentials-file", ec);
if (!token_url) return std::move(token_url).status();
auto universe_domain = GetUniverseDomainFromCredentialsJson(json);
if (!universe_domain) return std::move(universe_domain).status();

auto credential_source = json.find("credential_source");
if (credential_source == json.end()) {
Expand Down Expand Up @@ -218,6 +252,9 @@ ExternalAccountCredentials::ExternalAccountCredentials(

StatusOr<AccessToken> ExternalAccountCredentials::GetToken(
std::chrono::system_clock::time_point tp) {
if (info_.authorized_user_info.has_value()) {
return GetTokenAuthorizedUser(tp);
}
auto subject_token = (info_.token_source)(client_factory_, options_);
if (!subject_token) return std::move(subject_token).status();

Expand Down Expand Up @@ -334,6 +371,65 @@ StatusOr<AccessToken> ExternalAccountCredentials::GetTokenImpersonation(
return ParseGenerateAccessTokenResponse(**response, ec);
}

StatusOr<AccessToken> ExternalAccountCredentials::GetTokenAuthorizedUser(
std::chrono::system_clock::time_point tp) {
std::string current_refresh_token;
{
std::lock_guard<std::mutex> lk(mu_);
current_refresh_token = info_.authorized_user_info->refresh_token;
}
internal::Base64Encoder encoder;
for (unsigned char const c :
absl::StrCat(info_.authorized_user_info->client_id, ":",
info_.authorized_user_info->client_secret)) {
encoder.PushBack(c);
}
rest_internal::RestRequest request =
rest_internal::RestRequest(info_.token_url)
.AddHeader("content-type", "application/x-www-form-urlencoded")
.AddHeader("Authorization",
absl::StrCat("Basic ", std::move(encoder).FlushAndPad()));

std::vector<std::pair<std::string, std::string>> form_data = {
{"grant_type", "refresh_token"},
{"refresh_token", std::move(current_refresh_token)},
};

std::unique_ptr<rest_internal::RestClient> client = client_factory_(options_);
rest_internal::RestContext unused;
StatusOr<std::unique_ptr<rest_internal::RestResponse>> response =
client->Post(unused, request, form_data);
if (!response) return std::move(response).status();
if (IsHttpError(**response)) return AsStatus(std::move(**response));
StatusOr<std::string> payload =
rest_internal::ReadAll(std::move(**response).ExtractPayload());
if (!payload) return std::move(payload).status();

internal::ErrorContext const ec(
internal::ErrorContext::Container{{"token_url", info_.token_url}});

nlohmann::json access = nlohmann::json::parse(*payload, nullptr, false);
if (!access.is_object()) {
return InvalidArgumentError(
"token refresh response cannot be parsed as JSON object",
GCP_ERROR_INFO().WithContext(ec));
}
StatusOr<std::string> token =
ValidateStringField(access, "access_token", "token-refresh-response", ec);
if (!token) return std::move(token).status();
StatusOr<std::int32_t> expires_in =
ValidateIntField(access, "expires_in", "token-refresh-response", ec);
if (!expires_in) return std::move(expires_in).status();
StatusOr<std::string> new_refresh_token = ValidateStringField(
access, "refresh_token", "token-refresh-response", "", ec);
if (!new_refresh_token) return std::move(new_refresh_token).status();
if (!new_refresh_token->empty()) {
std::lock_guard<std::mutex> lk(mu_);
info_.authorized_user_info->refresh_token = *std::move(new_refresh_token);
}
return AccessToken{*std::move(token), tp + std::chrono::seconds(*expires_in)};
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
} // namespace cloud
Expand Down
12 changes: 12 additions & 0 deletions google/cloud/internal/oauth2_external_account_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <nlohmann/json_fwd.hpp>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>

namespace google {
Expand Down Expand Up @@ -67,6 +68,12 @@ struct WorkloadIdentityFederationInfo {
std::string pool_id;
};

struct ExternalAccountAuthorizedUserInfo {
std::string client_id;
std::string client_secret;
std::string refresh_token;
};

/**
* An external account configuration.
*
Expand All @@ -84,6 +91,8 @@ struct ExternalAccountInfo {
std::variant<std::monostate, WorkforceIdentityFederationInfo,
WorkloadIdentityFederationInfo>
identity_federation_info;
std::optional<ExternalAccountAuthorizedUserInfo> authorized_user_info =
std::nullopt;
bool IsWorkforceIdentityFederation() const;
bool IsWorkloadIdentityFederation() const;
};
Expand Down Expand Up @@ -115,7 +124,10 @@ class ExternalAccountCredentials : public oauth2_internal::Credentials {
private:
StatusOr<AccessToken> GetTokenImpersonation(std::string const& access_token,
internal::ErrorContext const& ec);
StatusOr<AccessToken> GetTokenAuthorizedUser(
std::chrono::system_clock::time_point tp);

mutable std::mutex mu_;
ExternalAccountInfo info_;
HttpClientFactory client_factory_;
Options options_;
Expand Down
Loading
Loading