Skip to content
Open
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
10 changes: 6 additions & 4 deletions google/cloud/internal/curl_rest_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,12 @@ StatusOr<std::unique_ptr<CurlImpl>> CurlRestClient::CreateCurlImpl(
auto impl =
std::make_unique<CurlImpl>(std::move(handle), handle_factory_, options);
if (credentials_) {
auto auth_header =
credentials_->AuthenticationHeader(std::chrono::system_clock::now());
if (!auth_header.ok()) return std::move(auth_header).status();
impl->SetHeader(HttpHeader(auth_header->first, auth_header->second));
auto auth_headers = credentials_->AuthenticationHeaders(
std::chrono::system_clock::now(), endpoint_address_);
if (!auth_headers.ok()) return std::move(auth_headers).status();
for (auto& header : *auth_headers) {
impl->SetHeader(std::move(header));
}
}
impl->SetHeader(HostHeader(options, endpoint_address_));
impl->SetHeaders(context.headers());
Expand Down
9 changes: 6 additions & 3 deletions google/cloud/internal/oauth2_api_key_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ StatusOr<AccessToken> ApiKeyCredentials::GetToken(
return AccessToken{std::string{}, tp};
}

StatusOr<std::pair<std::string, std::string>>
ApiKeyCredentials::AuthenticationHeader(std::chrono::system_clock::time_point) {
return std::make_pair(std::string{"x-goog-api-key"}, api_key_);
StatusOr<std::vector<rest_internal::HttpHeader>>
ApiKeyCredentials::AuthenticationHeaders(std::chrono::system_clock::time_point,
std::string_view) {
std::vector<rest_internal::HttpHeader> headers;
headers.emplace_back("x-goog-api-key", api_key_);
return headers;
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
Expand Down
5 changes: 3 additions & 2 deletions google/cloud/internal/oauth2_api_key_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ class ApiKeyCredentials : public oauth2_internal::Credentials {
StatusOr<AccessToken> GetToken(
std::chrono::system_clock::time_point tp) override;

StatusOr<std::pair<std::string, std::string>> AuthenticationHeader(
std::chrono::system_clock::time_point) override;
StatusOr<std::vector<rest_internal::HttpHeader>> AuthenticationHeaders(
std::chrono::system_clock::time_point,
std::string_view endpoint) override;

private:
std::string api_key_;
Expand Down
8 changes: 5 additions & 3 deletions google/cloud/internal/oauth2_api_key_credentials_test.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_api_key_credentials.h"
#include "google/cloud/internal/http_header.h"
#include "google/cloud/testing_util/status_matchers.h"
#include <gmock/gmock.h>
#include <chrono>
Expand All @@ -24,8 +25,8 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace {

using ::google::cloud::testing_util::IsOkAndHolds;
using ::testing::Contains;
using ::testing::IsEmpty;
using ::testing::Pair;

TEST(ApiKeyCredentials, EmptyToken) {
ApiKeyCredentials creds("api-key");
Expand All @@ -38,8 +39,9 @@ TEST(ApiKeyCredentials, EmptyToken) {
TEST(ApiKeyCredentials, SetsXGoogApiKeyHeader) {
ApiKeyCredentials creds("api-key");
auto const now = std::chrono::system_clock::now();
EXPECT_THAT(creds.AuthenticationHeader(now),
IsOkAndHolds(Pair("x-goog-api-key", "api-key")));
EXPECT_THAT(creds.AuthenticationHeaders(now, ""),
IsOkAndHolds(Contains(
rest_internal::HttpHeader("x-goog-api-key", "api-key"))));
}

} // namespace
Expand Down
34 changes: 24 additions & 10 deletions google/cloud/internal/oauth2_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ namespace cloud {
namespace oauth2_internal {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

StatusOr<std::vector<rest_internal::HttpHeader>>
Credentials::AuthenticationHeaders(std::chrono::system_clock::time_point tp,
std::string_view endpoint) {
std::vector<rest_internal::HttpHeader> headers;
auto authorization = Authorization(tp);
if (!authorization) return std::move(authorization).status();
headers.push_back(*std::move(authorization));

auto allowed_locations = AllowedLocations(tp, endpoint);
// Not all credential types support the x-allowed-locations header. For those
// that do, if there is a problem retrieving the header, omit the header.
if (allowed_locations.ok() && !allowed_locations->empty()) {
headers.push_back(*std::move(allowed_locations));
}
return headers;
}

StatusOr<std::vector<std::uint8_t>> Credentials::SignBlob(
absl::optional<std::string> const&, std::string const&) const {
return internal::UnimplementedError(
Expand All @@ -46,21 +63,18 @@ StatusOr<std::string> Credentials::project_id(
return project_id();
}

StatusOr<std::pair<std::string, std::string>> Credentials::AuthenticationHeader(
StatusOr<rest_internal::HttpHeader> Credentials::Authorization(
std::chrono::system_clock::time_point tp) {
auto token = GetToken(tp);
if (!token) return std::move(token).status();
if (token->token.empty()) return std::make_pair(std::string{}, std::string{});
return std::make_pair(std::string{"Authorization"},
absl::StrCat("Bearer ", token->token));
if (token->token.empty()) return rest_internal::HttpHeader{};
return rest_internal::HttpHeader{"authorization",
absl::StrCat("Bearer ", token->token)};
}

StatusOr<std::string> AuthenticationHeaderJoined(
Credentials& credentials, std::chrono::system_clock::time_point tp) {
auto header = credentials.AuthenticationHeader(tp);
if (!header) return std::move(header).status();
if (header->first.empty()) return std::string{};
return absl::StrCat(header->first, ": ", header->second);
StatusOr<rest_internal::HttpHeader> Credentials::AllowedLocations(
std::chrono::system_clock::time_point, std::string_view) {
return {};
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
Expand Down
89 changes: 56 additions & 33 deletions google/cloud/internal/oauth2_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_CREDENTIALS_H

#include "google/cloud/access_token.h"
#include "google/cloud/internal/http_header.h"
#include "google/cloud/options.h"
#include "google/cloud/status.h"
#include "google/cloud/status_or.h"
#include "google/cloud/version.h"
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>

Expand All @@ -46,18 +45,33 @@ class Credentials {
virtual ~Credentials() = default;

/**
* Obtains an access token.
* Returns header pairs used for authentication.
*
* Most implementations will cache the access token and (if possible) refresh
* the token before it expires. Refreshing the token may fail, as it often
* requires making HTTP requests. In that case, the last error is returned.
* This is the correct method to call for authentication headers for use in
* making an RPC to a GCP service. All the necessary headers are returned
* for whatever the combination of underlying Credential type and RPC
* endpoint.
*
* In most cases, this is the "Authorization" HTTP header. For API key
* credentials, it is the "X-Goog-Api-Key" header. It may also include the
* "x-allowed-locations" header if applicable.
*
* If unable to obtain a value for the header, which could happen for
* `Credentials` that need to be periodically refreshed, the underlying
* `Status` will indicate failure details from the refresh HTTP request.
* Otherwise, the returned value will contain the header pair to be used in
* HTTP requests.
*
* @param tp the current time, most callers should provide
* `std::chrono::system_clock::now()`. In tests, other value may be
* considered.
*
* @param endpoint the endpoint of the GCP service the RPC request will be
* sent to.
*/
virtual StatusOr<AccessToken> GetToken(
std::chrono::system_clock::time_point tp) = 0;
virtual StatusOr<std::vector<rest_internal::HttpHeader>>
AuthenticationHeaders(std::chrono::system_clock::time_point tp,
std::string_view endpoint);

/**
* Try to sign @p string_to_sign using @p service_account.
Expand Down Expand Up @@ -109,35 +123,44 @@ class Credentials {
virtual StatusOr<std::string> project_id(Options const&) const;

/**
* Returns a header pair used for authentication.
*
* In most cases, this is the "Authorization" HTTP header. For API key
* credentials, it is the "X-Goog-Api-Key" header.
* Returns only the "authorization" header if applicable for the credential
* type.
*
* If unable to obtain a value for the header, which could happen for
* `Credentials` that need to be periodically refreshed, the underlying
* `Status` will indicate failure details from the refresh HTTP request.
* Otherwise, the returned value will contain the header pair to be used in
* HTTP requests.
* @param tp the current time, most callers should provide
* `std::chrono::system_clock::now()`. In tests, other value may be
* considered.
*/
virtual StatusOr<std::pair<std::string, std::string>> AuthenticationHeader(
virtual StatusOr<rest_internal::HttpHeader> Authorization(
std::chrono::system_clock::time_point tp);
};

/**
* Returns a header pair as a single string to be used for authentication.
*
* In most cases, this is the "Authorization" HTTP header. For API key
* credentials, it is the "X-Goog-Api-Key" header.
*
* If unable to obtain a value for the header, which could happen for
* `Credentials` that need to be periodically refreshed, the underlying `Status`
* will indicate failure details from the refresh HTTP request. Otherwise, the
* returned value will contain the header pair to be used in HTTP requests.
*/
StatusOr<std::string> AuthenticationHeaderJoined(
Credentials& credentials, std::chrono::system_clock::time_point tp =
std::chrono::system_clock::now());
/**
* Returns only the "x-allowed-locations" header if applicable for the
* credential type.
*
* @param tp the current time, most callers should provide
* `std::chrono::system_clock::now()`. In tests, other value may be
* considered.
*
* @param endpoint the endpoint of the GCP service the RPC request will be
* sent to.
*/
virtual StatusOr<rest_internal::HttpHeader> AllowedLocations(
std::chrono::system_clock::time_point tp, std::string_view endpoint);

/**
* Obtains an access token.
*
* Most implementations will cache the access token and (if possible) refresh
* the token before it expires. Refreshing the token may fail, as it often
* requires making HTTP requests. In that case, the last error is returned.
*
* @param tp the current time, most callers should provide
* `std::chrono::system_clock::now()`. In tests, other value may be
* considered.
*/
virtual StatusOr<AccessToken> GetToken(
std::chrono::system_clock::time_point tp) = 0;
};

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
Expand Down
76 changes: 45 additions & 31 deletions google/cloud/internal/oauth2_credentials_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ namespace {
using ::google::cloud::internal::UnavailableError;
using ::google::cloud::testing_util::IsOk;
using ::google::cloud::testing_util::IsOkAndHolds;
using ::testing::IsEmpty;
using ::testing::Contains;
using ::testing::Not;
using ::testing::Pair;
using ::testing::Return;

class MockCredentials : public Credentials {
public:
MOCK_METHOD(StatusOr<AccessToken>, GetToken,
(std::chrono::system_clock::time_point), (override));
MOCK_METHOD(StatusOr<rest_internal::HttpHeader>, AllowedLocations,
(std::chrono::system_clock::time_point, std::string_view),
(override));
};

TEST(Credentials, AuthorizationHeaderSuccess) {
Expand All @@ -43,48 +45,60 @@ TEST(Credentials, AuthorizationHeaderSuccess) {
auto const expiration = now + std::chrono::seconds(3600);
EXPECT_CALL(mock, GetToken(now))
.WillOnce(Return(AccessToken{"test-token", expiration}));
auto actual = mock.AuthenticationHeader(now);
EXPECT_THAT(actual, IsOkAndHolds(Pair("Authorization", "Bearer test-token")));
EXPECT_CALL(mock, AllowedLocations)
.WillOnce(Return(rest_internal::HttpHeader{}));
auto actual = mock.AuthenticationHeaders(now, "my-endpoint");
EXPECT_THAT(actual, IsOkAndHolds(Contains(rest_internal::HttpHeader(
"authorization", "Bearer test-token"))));
}

TEST(Credentials, AuthenticationHeaderJoinedSuccess) {
TEST(Credentials, AuthenticationHeaderError) {
MockCredentials mock;
auto const now = std::chrono::system_clock::now();
auto const expiration = now + std::chrono::seconds(3600);
EXPECT_CALL(mock, GetToken(now))
.WillOnce(Return(AccessToken{"test-token", expiration}));
auto actual = AuthenticationHeaderJoined(mock, now);
EXPECT_THAT(actual, IsOkAndHolds("Authorization: Bearer test-token"));
EXPECT_CALL(mock, GetToken).WillOnce(Return(UnavailableError("try-again")));
auto actual = mock.AuthenticationHeaders(std::chrono::system_clock::now(),
"my-endpoint");
EXPECT_EQ(actual.status(), UnavailableError("try-again"));
}

TEST(Credentials, AuthenticationHeaderJoinedEmpty) {
TEST(Credentials, ProjectId) {
MockCredentials mock;
auto const now = std::chrono::system_clock::now();
auto const expiration = now + std::chrono::seconds(3600);
EXPECT_CALL(mock, GetToken(now))
.WillOnce(Return(AccessToken{"", expiration}));
auto actual = AuthenticationHeaderJoined(mock, now);
EXPECT_THAT(actual, IsOkAndHolds(IsEmpty()));
EXPECT_THAT(mock.project_id(), Not(IsOk()));
EXPECT_THAT(mock.project_id({}), Not(IsOk()));
}

TEST(Credentials, AuthenticationHeaderError) {
TEST(Credentials, AllowedLocationsSuccess) {
MockCredentials mock;
EXPECT_CALL(mock, GetToken).WillOnce(Return(UnavailableError("try-again")));
auto actual = mock.AuthenticationHeader(std::chrono::system_clock::now());
EXPECT_EQ(actual.status(), UnavailableError("try-again"));
}
auto const now = std::chrono::system_clock::now();
auto const expiration = now + std::chrono::seconds(3600);
EXPECT_CALL(mock, GetToken)
.WillOnce(Return(AccessToken{"test-token", expiration}));
EXPECT_CALL(mock, AllowedLocations)
.WillOnce(Return(
rest_internal::HttpHeader("x-allowed-locations", "my-location")));

TEST(Credentials, AuthenticationHeaderJoinedError) {
MockCredentials mock;
EXPECT_CALL(mock, GetToken).WillOnce(Return(UnavailableError("try-again")));
auto actual = AuthenticationHeaderJoined(mock);
EXPECT_EQ(actual.status(), UnavailableError("try-again"));
auto auth_headers = mock.AuthenticationHeaders(
std::chrono::system_clock::now(), "my-endpoint");
EXPECT_THAT(
auth_headers,
IsOkAndHolds(::testing::ElementsAre(
rest_internal::HttpHeader("authorization", "Bearer test-token"),
rest_internal::HttpHeader("x-allowed-locations", "my-location"))));
}

TEST(Credentials, ProjectId) {
TEST(Credentials, AllowedLocationsFailure) {
MockCredentials mock;
EXPECT_THAT(mock.project_id(), Not(IsOk()));
EXPECT_THAT(mock.project_id({}), Not(IsOk()));
auto const now = std::chrono::system_clock::now();
auto const expiration = now + std::chrono::seconds(3600);
EXPECT_CALL(mock, GetToken)
.WillOnce(Return(AccessToken{"test-token", expiration}));
EXPECT_CALL(mock, AllowedLocations)
.WillOnce(Return(internal::DeadlineExceededError("RPC took too long")));

auto auth_headers = mock.AuthenticationHeaders(
std::chrono::system_clock::now(), "my-endpoint");
EXPECT_THAT(auth_headers,
IsOkAndHolds(::testing::ElementsAre(rest_internal::HttpHeader(
"authorization", "Bearer test-token"))));
}

} // namespace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,11 @@ MinimalIamCredentialsRestStub::MinimalIamCredentialsRestStub(
StatusOr<google::cloud::AccessToken>
MinimalIamCredentialsRestStub::GenerateAccessToken(
GenerateAccessTokenRequest const& request) {
auto auth_header =
credentials_->AuthenticationHeader(std::chrono::system_clock::now());
if (!auth_header) return std::move(auth_header).status();

auto authorization_header =
credentials_->Authorization(std::chrono::system_clock::now());
if (!authorization_header) return std::move(authorization_header).status();
rest_internal::RestRequest rest_request;
rest_request.AddHeader(rest_internal::HttpHeader(auth_header.value()));
rest_request.AddHeader(*std::move(authorization_header));
rest_request.AddHeader("Content-Type", "application/json");
rest_request.SetPath(MakeRequestPath(request));
nlohmann::json payload{
Expand Down
5 changes: 3 additions & 2 deletions google/cloud/internal/unified_rest_credentials_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,9 @@ TEST(UnifiedRestCredentialsTest, ApiKey) {
ASSERT_THAT(oauth2_creds, NotNull());

auto header =
oauth2_creds->AuthenticationHeader(std::chrono::system_clock::now());
EXPECT_THAT(header, IsOkAndHolds(Pair("x-goog-api-key", "api-key")));
oauth2_creds->AuthenticationHeaders(std::chrono::system_clock::now(), "");
EXPECT_THAT(header,
IsOkAndHolds(Contains(HttpHeader("x-goog-api-key", "api-key"))));
}

TEST(UnifiedRestCredentialsTest, LoadError) {
Expand Down
Loading
Loading