Skip to content

Commit e64f424

Browse files
committed
impl(oauth2): add methods for returning multiple auth related http headers
1 parent fdbec33 commit e64f424

11 files changed

+194
-119
lines changed

google/cloud/internal/curl_rest_client.cc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,12 @@ StatusOr<std::unique_ptr<CurlImpl>> CurlRestClient::CreateCurlImpl(
124124
auto impl =
125125
std::make_unique<CurlImpl>(std::move(handle), handle_factory_, options);
126126
if (credentials_) {
127-
auto auth_header =
128-
credentials_->AuthenticationHeader(std::chrono::system_clock::now());
129-
if (!auth_header.ok()) return std::move(auth_header).status();
130-
impl->SetHeader(HttpHeader(auth_header->first, auth_header->second));
127+
auto auth_headers = credentials_->AuthenticationHeaders(
128+
std::chrono::system_clock::now(), endpoint_address_);
129+
if (!auth_headers.ok()) return std::move(auth_headers).status();
130+
for (auto& header : *auth_headers) {
131+
impl->SetHeader(std::move(header));
132+
}
131133
}
132134
impl->SetHeader(HostHeader(options, endpoint_address_));
133135
impl->SetHeaders(context.headers());

google/cloud/internal/oauth2_api_key_credentials.cc

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ StatusOr<AccessToken> ApiKeyCredentials::GetToken(
2727
return AccessToken{std::string{}, tp};
2828
}
2929

30-
StatusOr<std::pair<std::string, std::string>>
31-
ApiKeyCredentials::AuthenticationHeader(std::chrono::system_clock::time_point) {
32-
return std::make_pair(std::string{"x-goog-api-key"}, api_key_);
30+
StatusOr<std::vector<rest_internal::HttpHeader>>
31+
ApiKeyCredentials::AuthenticationHeaders(std::chrono::system_clock::time_point,
32+
std::string_view) {
33+
std::vector<rest_internal::HttpHeader> headers;
34+
headers.emplace_back("x-goog-api-key", api_key_);
35+
return headers;
3336
}
3437

3538
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END

google/cloud/internal/oauth2_api_key_credentials.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ class ApiKeyCredentials : public oauth2_internal::Credentials {
3737
StatusOr<AccessToken> GetToken(
3838
std::chrono::system_clock::time_point tp) override;
3939

40-
StatusOr<std::pair<std::string, std::string>> AuthenticationHeader(
41-
std::chrono::system_clock::time_point) override;
40+
StatusOr<std::vector<rest_internal::HttpHeader>> AuthenticationHeaders(
41+
std::chrono::system_clock::time_point,
42+
std::string_view endpoint) override;
4243

4344
private:
4445
std::string api_key_;

google/cloud/internal/oauth2_api_key_credentials_test.cc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/internal/oauth2_api_key_credentials.h"
16+
#include "google/cloud/internal/http_header.h"
1617
#include "google/cloud/testing_util/status_matchers.h"
1718
#include <gmock/gmock.h>
1819
#include <chrono>
@@ -24,8 +25,8 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
2425
namespace {
2526

2627
using ::google::cloud::testing_util::IsOkAndHolds;
28+
using ::testing::Contains;
2729
using ::testing::IsEmpty;
28-
using ::testing::Pair;
2930

3031
TEST(ApiKeyCredentials, EmptyToken) {
3132
ApiKeyCredentials creds("api-key");
@@ -38,8 +39,9 @@ TEST(ApiKeyCredentials, EmptyToken) {
3839
TEST(ApiKeyCredentials, SetsXGoogApiKeyHeader) {
3940
ApiKeyCredentials creds("api-key");
4041
auto const now = std::chrono::system_clock::now();
41-
EXPECT_THAT(creds.AuthenticationHeader(now),
42-
IsOkAndHolds(Pair("x-goog-api-key", "api-key")));
42+
EXPECT_THAT(creds.AuthenticationHeaders(now, ""),
43+
IsOkAndHolds(Contains(
44+
rest_internal::HttpHeader("x-goog-api-key", "api-key"))));
4345
}
4446

4547
} // namespace

google/cloud/internal/oauth2_credentials.cc

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ namespace cloud {
2222
namespace oauth2_internal {
2323
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
2424

25+
StatusOr<std::vector<rest_internal::HttpHeader>>
26+
Credentials::AuthenticationHeaders(std::chrono::system_clock::time_point tp,
27+
std::string_view endpoint) {
28+
std::vector<rest_internal::HttpHeader> headers;
29+
auto authorization = Authorization(tp);
30+
if (!authorization) return std::move(authorization).status();
31+
headers.push_back(*std::move(authorization));
32+
33+
auto allowed_locations = AllowedLocations(tp, endpoint);
34+
// Not all credential types support the x-allowed-locations header. For those
35+
// that do, if there is a problem retrieving the header, omit the header.
36+
if (allowed_locations.ok() && !allowed_locations->empty()) {
37+
headers.push_back(*std::move(allowed_locations));
38+
}
39+
return headers;
40+
}
41+
2542
StatusOr<std::vector<std::uint8_t>> Credentials::SignBlob(
2643
absl::optional<std::string> const&, std::string const&) const {
2744
return internal::UnimplementedError(
@@ -46,21 +63,18 @@ StatusOr<std::string> Credentials::project_id(
4663
return project_id();
4764
}
4865

49-
StatusOr<std::pair<std::string, std::string>> Credentials::AuthenticationHeader(
66+
StatusOr<rest_internal::HttpHeader> Credentials::Authorization(
5067
std::chrono::system_clock::time_point tp) {
5168
auto token = GetToken(tp);
5269
if (!token) return std::move(token).status();
53-
if (token->token.empty()) return std::make_pair(std::string{}, std::string{});
54-
return std::make_pair(std::string{"Authorization"},
55-
absl::StrCat("Bearer ", token->token));
70+
if (token->token.empty()) return rest_internal::HttpHeader{};
71+
return rest_internal::HttpHeader{"authorization",
72+
absl::StrCat("Bearer ", token->token)};
5673
}
5774

58-
StatusOr<std::string> AuthenticationHeaderJoined(
59-
Credentials& credentials, std::chrono::system_clock::time_point tp) {
60-
auto header = credentials.AuthenticationHeader(tp);
61-
if (!header) return std::move(header).status();
62-
if (header->first.empty()) return std::string{};
63-
return absl::StrCat(header->first, ": ", header->second);
75+
StatusOr<rest_internal::HttpHeader> Credentials::AllowedLocations(
76+
std::chrono::system_clock::time_point, std::string_view) {
77+
return {};
6478
}
6579

6680
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END

google/cloud/internal/oauth2_credentials.h

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_OAUTH2_CREDENTIALS_H
1717

1818
#include "google/cloud/access_token.h"
19+
#include "google/cloud/internal/http_header.h"
1920
#include "google/cloud/options.h"
20-
#include "google/cloud/status.h"
2121
#include "google/cloud/status_or.h"
2222
#include "google/cloud/version.h"
2323
#include <chrono>
24-
#include <cstdint>
2524
#include <string>
2625
#include <vector>
2726

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

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

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

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

127-
/**
128-
* Returns a header pair as a single string to be used for authentication.
129-
*
130-
* In most cases, this is the "Authorization" HTTP header. For API key
131-
* credentials, it is the "X-Goog-Api-Key" header.
132-
*
133-
* If unable to obtain a value for the header, which could happen for
134-
* `Credentials` that need to be periodically refreshed, the underlying `Status`
135-
* will indicate failure details from the refresh HTTP request. Otherwise, the
136-
* returned value will contain the header pair to be used in HTTP requests.
137-
*/
138-
StatusOr<std::string> AuthenticationHeaderJoined(
139-
Credentials& credentials, std::chrono::system_clock::time_point tp =
140-
std::chrono::system_clock::now());
136+
/**
137+
* Returns only the "x-allowed-locations" header if applicable for the
138+
* credential type.
139+
*
140+
* @param tp the current time, most callers should provide
141+
* `std::chrono::system_clock::now()`. In tests, other value may be
142+
* considered.
143+
*
144+
* @param endpoint the endpoint of the GCP service the RPC request will be
145+
* sent to.
146+
*/
147+
virtual StatusOr<rest_internal::HttpHeader> AllowedLocations(
148+
std::chrono::system_clock::time_point tp, std::string_view endpoint);
149+
150+
/**
151+
* Obtains an access token.
152+
*
153+
* Most implementations will cache the access token and (if possible) refresh
154+
* the token before it expires. Refreshing the token may fail, as it often
155+
* requires making HTTP requests. In that case, the last error is returned.
156+
*
157+
* @param tp the current time, most callers should provide
158+
* `std::chrono::system_clock::now()`. In tests, other value may be
159+
* considered.
160+
*/
161+
virtual StatusOr<AccessToken> GetToken(
162+
std::chrono::system_clock::time_point tp) = 0;
163+
};
141164

142165
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
143166
} // namespace oauth2_internal

google/cloud/internal/oauth2_credentials_test.cc

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ namespace {
2626
using ::google::cloud::internal::UnavailableError;
2727
using ::google::cloud::testing_util::IsOk;
2828
using ::google::cloud::testing_util::IsOkAndHolds;
29-
using ::testing::IsEmpty;
29+
using ::testing::Contains;
3030
using ::testing::Not;
31-
using ::testing::Pair;
3231
using ::testing::Return;
3332

3433
class MockCredentials : public Credentials {
3534
public:
3635
MOCK_METHOD(StatusOr<AccessToken>, GetToken,
3736
(std::chrono::system_clock::time_point), (override));
37+
MOCK_METHOD(StatusOr<rest_internal::HttpHeader>, AllowedLocations,
38+
(std::chrono::system_clock::time_point, std::string_view),
39+
(override));
3840
};
3941

4042
TEST(Credentials, AuthorizationHeaderSuccess) {
@@ -43,48 +45,60 @@ TEST(Credentials, AuthorizationHeaderSuccess) {
4345
auto const expiration = now + std::chrono::seconds(3600);
4446
EXPECT_CALL(mock, GetToken(now))
4547
.WillOnce(Return(AccessToken{"test-token", expiration}));
46-
auto actual = mock.AuthenticationHeader(now);
47-
EXPECT_THAT(actual, IsOkAndHolds(Pair("Authorization", "Bearer test-token")));
48+
EXPECT_CALL(mock, AllowedLocations)
49+
.WillOnce(Return(rest_internal::HttpHeader{}));
50+
auto actual = mock.AuthenticationHeaders(now, "my-endpoint");
51+
EXPECT_THAT(actual, IsOkAndHolds(Contains(rest_internal::HttpHeader(
52+
"authorization", "Bearer test-token"))));
4853
}
4954

50-
TEST(Credentials, AuthenticationHeaderJoinedSuccess) {
55+
TEST(Credentials, AuthenticationHeaderError) {
5156
MockCredentials mock;
52-
auto const now = std::chrono::system_clock::now();
53-
auto const expiration = now + std::chrono::seconds(3600);
54-
EXPECT_CALL(mock, GetToken(now))
55-
.WillOnce(Return(AccessToken{"test-token", expiration}));
56-
auto actual = AuthenticationHeaderJoined(mock, now);
57-
EXPECT_THAT(actual, IsOkAndHolds("Authorization: Bearer test-token"));
57+
EXPECT_CALL(mock, GetToken).WillOnce(Return(UnavailableError("try-again")));
58+
auto actual = mock.AuthenticationHeaders(std::chrono::system_clock::now(),
59+
"my-endpoint");
60+
EXPECT_EQ(actual.status(), UnavailableError("try-again"));
5861
}
5962

60-
TEST(Credentials, AuthenticationHeaderJoinedEmpty) {
63+
TEST(Credentials, ProjectId) {
6164
MockCredentials mock;
62-
auto const now = std::chrono::system_clock::now();
63-
auto const expiration = now + std::chrono::seconds(3600);
64-
EXPECT_CALL(mock, GetToken(now))
65-
.WillOnce(Return(AccessToken{"", expiration}));
66-
auto actual = AuthenticationHeaderJoined(mock, now);
67-
EXPECT_THAT(actual, IsOkAndHolds(IsEmpty()));
65+
EXPECT_THAT(mock.project_id(), Not(IsOk()));
66+
EXPECT_THAT(mock.project_id({}), Not(IsOk()));
6867
}
6968

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

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

84-
TEST(Credentials, ProjectId) {
88+
TEST(Credentials, AllowedLocationsFailure) {
8589
MockCredentials mock;
86-
EXPECT_THAT(mock.project_id(), Not(IsOk()));
87-
EXPECT_THAT(mock.project_id({}), Not(IsOk()));
90+
auto const now = std::chrono::system_clock::now();
91+
auto const expiration = now + std::chrono::seconds(3600);
92+
EXPECT_CALL(mock, GetToken)
93+
.WillOnce(Return(AccessToken{"test-token", expiration}));
94+
EXPECT_CALL(mock, AllowedLocations)
95+
.WillOnce(Return(internal::DeadlineExceededError("RPC took too long")));
96+
97+
auto auth_headers = mock.AuthenticationHeaders(
98+
std::chrono::system_clock::now(), "my-endpoint");
99+
EXPECT_THAT(auth_headers,
100+
IsOkAndHolds(::testing::ElementsAre(rest_internal::HttpHeader(
101+
"authorization", "Bearer test-token"))));
88102
}
89103

90104
} // namespace

google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,11 @@ MinimalIamCredentialsRestStub::MinimalIamCredentialsRestStub(
4646
StatusOr<google::cloud::AccessToken>
4747
MinimalIamCredentialsRestStub::GenerateAccessToken(
4848
GenerateAccessTokenRequest const& request) {
49-
auto auth_header =
50-
credentials_->AuthenticationHeader(std::chrono::system_clock::now());
51-
if (!auth_header) return std::move(auth_header).status();
52-
49+
auto authorization_header =
50+
credentials_->Authorization(std::chrono::system_clock::now());
51+
if (!authorization_header) return std::move(authorization_header).status();
5352
rest_internal::RestRequest rest_request;
54-
rest_request.AddHeader(rest_internal::HttpHeader(auth_header.value()));
53+
rest_request.AddHeader(*std::move(authorization_header));
5554
rest_request.AddHeader("Content-Type", "application/json");
5655
rest_request.SetPath(MakeRequestPath(request));
5756
nlohmann::json payload{

google/cloud/internal/unified_rest_credentials_test.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,9 @@ TEST(UnifiedRestCredentialsTest, ApiKey) {
470470
ASSERT_THAT(oauth2_creds, NotNull());
471471

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

477478
TEST(UnifiedRestCredentialsTest, LoadError) {

0 commit comments

Comments
 (0)