Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impl(common): token exchange for external accounts #10328

Merged
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
2 changes: 2 additions & 0 deletions google/cloud/google_cloud_cpp_rest_internal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ google_cloud_cpp_rest_internal_hdrs = [
"internal/oauth2_credential_constants.h",
"internal/oauth2_credentials.h",
"internal/oauth2_error_credentials.h",
"internal/oauth2_external_account_credentials.h",
"internal/oauth2_external_account_token_source.h",
"internal/oauth2_google_application_default_credentials_file.h",
"internal/oauth2_google_credentials.h",
Expand Down Expand Up @@ -79,6 +80,7 @@ google_cloud_cpp_rest_internal_srcs = [
"internal/oauth2_compute_engine_credentials.cc",
"internal/oauth2_credentials.cc",
"internal/oauth2_error_credentials.cc",
"internal/oauth2_external_account_credentials.cc",
"internal/oauth2_google_application_default_credentials_file.cc",
"internal/oauth2_google_credentials.cc",
"internal/oauth2_impersonate_service_account_credentials.cc",
Expand Down
3 changes: 3 additions & 0 deletions google/cloud/google_cloud_cpp_rest_internal.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ add_library(
internal/oauth2_credentials.h
internal/oauth2_error_credentials.cc
internal/oauth2_error_credentials.h
internal/oauth2_external_account_credentials.cc
internal/oauth2_external_account_credentials.h
internal/oauth2_external_account_token_source.h
internal/oauth2_google_application_default_credentials_file.cc
internal/oauth2_google_application_default_credentials_file.h
Expand Down Expand Up @@ -214,6 +216,7 @@ if (BUILD_TESTING)
internal/oauth2_cached_credentials_test.cc
internal/oauth2_compute_engine_credentials_test.cc
internal/oauth2_credentials_test.cc
internal/oauth2_external_account_credentials_test.cc
internal/oauth2_google_application_default_credentials_file_test.cc
internal/oauth2_google_credentials_test.cc
internal/oauth2_impersonate_service_account_credentials_test.cc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ google_cloud_cpp_rest_internal_unit_tests = [
"internal/oauth2_cached_credentials_test.cc",
"internal/oauth2_compute_engine_credentials_test.cc",
"internal/oauth2_credentials_test.cc",
"internal/oauth2_external_account_credentials_test.cc",
"internal/oauth2_google_application_default_credentials_file_test.cc",
"internal/oauth2_google_credentials_test.cc",
"internal/oauth2_impersonate_service_account_credentials_test.cc",
Expand Down
61 changes: 50 additions & 11 deletions google/cloud/internal/external_account_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#include "google/cloud/common_options.h"
#include "google/cloud/internal/external_account_parsing.h"
#include "google/cloud/internal/external_account_token_source_url.h"
#include "google/cloud/internal/getenv.h"
#include "google/cloud/internal/oauth2_external_account_credentials.h"
#include "google/cloud/internal/rest_client.h"
#include "google/cloud/testing_util/status_matchers.h"
#include <gmock/gmock.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include <thread>

namespace google {
namespace cloud {
Expand All @@ -27,6 +31,7 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace {

using ::google::cloud::internal::GetEnv;
using ::testing::IsEmpty;

TEST(ExternalAccountIntegrationTest, UrlSourced) {
auto filename = GetEnv("GOOGLE_CLOUD_CPP_EXTERNAL_ACCOUNT_FILE");
Expand All @@ -38,22 +43,56 @@ TEST(ExternalAccountIntegrationTest, UrlSourced) {
// TODO(#5915) - use higher-level abstractions once available
auto json = nlohmann::json::parse(contents, nullptr, false);
ASSERT_TRUE(json.is_object()) << "json=" << json.dump();

auto ec = internal::ErrorContext(
{{"GOOGLE_CLOUD_CPP_EXTERNAL_ACCOUNT_FILE", *filename},
{"program", "test"}});
auto type = ValidateStringField(json, "type", "credentials-file", ec);
ASSERT_STATUS_OK(type);
ASSERT_EQ(*type, "external_account");

auto audience = ValidateStringField(json, "audience", "credentials-file", ec);
ASSERT_STATUS_OK(audience);
auto subject_token_type =
ValidateStringField(json, "subject_token_type", "credentials-file", ec);
ASSERT_STATUS_OK(subject_token_type);
auto token_url =
ValidateStringField(json, "token_url", "credentials-file", ec);
ASSERT_STATUS_OK(token_url);

ASSERT_TRUE(json.contains("credential_source")) << "json=" << json.dump();
auto credential_source = json["credential_source"];
ASSERT_TRUE(credential_source.is_object()) << "json=" << json.dump();
auto make_client = []() {
return rest_internal::MakeDefaultRestClient("", Options{});

auto make_client = [](Options opts = {}) {
return rest_internal::MakeDefaultRestClient("", std::move(opts));
};
auto source = MakeExternalAccountTokenSourceUrl(
credential_source, make_client,
internal::ErrorContext(
{{"GOOGLE_CLOUD_CPP_EXTERNAL_ACCOUNT_FILE", *filename},
{"program", "test"}}));
auto source =
MakeExternalAccountTokenSourceUrl(credential_source, make_client, ec);
ASSERT_STATUS_OK(source);
auto subject_token = (*source)(Options{});
ASSERT_STATUS_OK(subject_token);
std::cout << "subject_token=" << subject_token->token.substr(0, 32)
<< "...<truncated>...\n";

auto info =
ExternalAccountInfo{*std::move(audience), *std::move(subject_token_type),
*std::move(token_url), *std::move(source)};
auto credentials = ExternalAccountCredentials(info, make_client);
// Anything involving HTTP requests may fail and needs a retry loop.
coryan marked this conversation as resolved.
Show resolved Hide resolved
auto now = std::chrono::system_clock::now();
auto access_token = [&]() -> StatusOr<internal::AccessToken> {
Status last_status;
auto delay = std::chrono::seconds(1);
for (int i = 0; i != 5; ++i) {
if (i != 0) std::this_thread::sleep_for(delay);
now = std::chrono::system_clock::now();
auto access_token = credentials.GetToken(now);
if (access_token) return access_token;
last_status = std::move(access_token).status();
delay *= 2;
}
return last_status;
}();
ASSERT_STATUS_OK(access_token);
EXPECT_GT(access_token->expiration, now);
EXPECT_THAT(access_token->token.substr(0, 32), Not(IsEmpty()));
}

} // namespace
Expand Down
110 changes: 110 additions & 0 deletions google/cloud/internal/oauth2_external_account_credentials.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2022 Google LLC
//
// 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
//
// https://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 "google/cloud/internal/oauth2_external_account_credentials.h"
#include "google/cloud/internal/external_account_parsing.h"
#include "google/cloud/internal/make_status.h"
#include "google/cloud/internal/rest_client.h"
#include <nlohmann/json.hpp>

namespace google {
namespace cloud {
namespace oauth2_internal {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

ExternalAccountCredentials::ExternalAccountCredentials(
ExternalAccountInfo info, HttpClientFactory client_factory, Options options)
: info_(std::move(info)),
client_factory_(std::move(client_factory)),
options_(std::move(options)) {}

StatusOr<internal::AccessToken> ExternalAccountCredentials::GetToken(
std::chrono::system_clock::time_point tp) {
auto subject_token = (info_.token_source)(Options{});
coryan marked this conversation as resolved.
Show resolved Hide resolved
if (!subject_token) return std::move(subject_token).status();

auto form_data = std::vector<std::pair<std::string, std::string>>{
{"grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"},
{"requested_token_type", "urn:ietf:params:oauth:token-type:access_token"},
{"scope", "https://www.googleapis.com/auth/cloud-platform"},
{"audience", info_.audience},
{"subject_token_type", info_.subject_token_type},
{"subject_token", subject_token->token},
};
auto request = rest_internal::RestRequest(info_.token_url);

auto client = client_factory_(options_);
auto response = client->Post(request, form_data);
if (!response) return std::move(response).status();
if (MapHttpCodeToStatus((*response)->StatusCode()) != StatusCode::kOk) {
return AsStatus(std::move(**response));
}
auto payload = rest_internal::ReadAll(std::move(**response).ExtractPayload());
if (!payload) return std::move(payload.status());

auto ec = internal::ErrorContext({
{"audience", info_.audience},
{"subject_token_type", info_.subject_token_type},
{"subject_token", subject_token->token.substr(0, 32)},
{"token_url", info_.token_url},
});

auto access = nlohmann::json::parse(*payload, nullptr, false);
if (!access.is_object()) {
return internal::InvalidArgumentError(
"token exchange response cannot be parsed as JSON object",
GCP_ERROR_INFO().WithContext(ec));
}
auto token = ValidateStringField(access, "access_token",
"token-exchange-response", ec);
if (!token) return std::move(token).status();
auto issued_token_type = ValidateStringField(access, "issued_token_type",
"token-exchange-response", ec);
if (!issued_token_type) return std::move(issued_token_type).status();
auto token_type =
ValidateStringField(access, "token_type", "token-exchange-response", ec);
if (!token_type) return std::move(token_type).status();

if (*issued_token_type != "urn:ietf:params:oauth:token-type:access_token" ||
*token_type != "Bearer") {
return internal::InvalidArgumentError(
"expected a Bearer access token in token exchange response",
GCP_ERROR_INFO()
.WithContext(std::move(ec))
.WithMetadata("token_type", *token_type)
.WithMetadata("issued_token_type", *issued_token_type));
}
auto it = access.find("expires_in");
if (it == access.end() || !it->is_number_integer()) {
return internal::InvalidArgumentError(
"expected a numeric `expires_in` field in the token exchange response",
GCP_ERROR_INFO()
.WithContext(std::move(ec))
.WithMetadata("expires_in",
it == access.end() ? "not-found" : it->type_name()));
}
return internal::AccessToken{
*token, tp + std::chrono::seconds(it->get<std::int32_t>())};
}

StatusOr<std::pair<std::string, std::string>>
ExternalAccountCredentials::AuthorizationHeader() {
return internal::UnimplementedError(
"WIP(#10316) - use decorator for credentials", GCP_ERROR_INFO());
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
} // namespace cloud
} // namespace google
63 changes: 63 additions & 0 deletions google/cloud/internal/oauth2_external_account_credentials.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2022 Google LLC
//
// 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
//
// https://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.

#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_EXTERNAL_ACCOUNT_CREDENTIALS_H
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_EXTERNAL_ACCOUNT_CREDENTIALS_H

#include "google/cloud/internal/oauth2_credentials.h"
#include "google/cloud/internal/oauth2_external_account_token_source.h"
#include "google/cloud/internal/rest_client.h"
#include "google/cloud/options.h"
#include "google/cloud/version.h"
#include <functional>
#include <memory>

namespace google {
namespace cloud {
namespace oauth2_internal {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

struct ExternalAccountInfo {
std::string audience;
std::string subject_token_type;
std::string token_url;
ExternalAccountTokenSource token_source;
};

class ExternalAccountCredentials : public oauth2_internal::Credentials {
public:
using HttpClientFactory =
std::function<std::unique_ptr<rest_internal::RestClient>(Options const&)>;

ExternalAccountCredentials(ExternalAccountInfo info,
HttpClientFactory client_factory,
Options options = {});
~ExternalAccountCredentials() override = default;

StatusOr<internal::AccessToken> GetToken(
std::chrono::system_clock::time_point tp) override;
StatusOr<std::pair<std::string, std::string>> AuthorizationHeader() override;

private:
ExternalAccountInfo info_;
HttpClientFactory client_factory_;
Options options_;
};

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
} // namespace cloud
} // namespace google

#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_EXTERNAL_ACCOUNT_CREDENTIALS_H
Loading