From e64f424276f7eb06c0c91e997dd7ce2f7df8cb73 Mon Sep 17 00:00:00 2001 From: Scott Hart Date: Sat, 4 Apr 2026 13:45:58 -0400 Subject: [PATCH 1/2] impl(oauth2): add methods for returning multiple auth related http headers --- google/cloud/internal/curl_rest_client.cc | 10 ++- .../internal/oauth2_api_key_credentials.cc | 9 +- .../internal/oauth2_api_key_credentials.h | 5 +- .../oauth2_api_key_credentials_test.cc | 8 +- google/cloud/internal/oauth2_credentials.cc | 34 ++++--- google/cloud/internal/oauth2_credentials.h | 89 ++++++++++++------- .../cloud/internal/oauth2_credentials_test.cc | 76 +++++++++------- .../oauth2_minimal_iam_credentials_rest.cc | 9 +- .../internal/unified_rest_credentials_test.cc | 5 +- google/cloud/storage/client.cc | 4 - .../unified_credentials_integration_test.cc | 64 ++++++++----- 11 files changed, 194 insertions(+), 119 deletions(-) diff --git a/google/cloud/internal/curl_rest_client.cc b/google/cloud/internal/curl_rest_client.cc index 04f5ec98cea6b..0a993ff7c2a86 100644 --- a/google/cloud/internal/curl_rest_client.cc +++ b/google/cloud/internal/curl_rest_client.cc @@ -124,10 +124,12 @@ StatusOr> CurlRestClient::CreateCurlImpl( auto impl = std::make_unique(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()); diff --git a/google/cloud/internal/oauth2_api_key_credentials.cc b/google/cloud/internal/oauth2_api_key_credentials.cc index 245a9a33ed08d..aee0f3386f16c 100644 --- a/google/cloud/internal/oauth2_api_key_credentials.cc +++ b/google/cloud/internal/oauth2_api_key_credentials.cc @@ -27,9 +27,12 @@ StatusOr ApiKeyCredentials::GetToken( return AccessToken{std::string{}, tp}; } -StatusOr> -ApiKeyCredentials::AuthenticationHeader(std::chrono::system_clock::time_point) { - return std::make_pair(std::string{"x-goog-api-key"}, api_key_); +StatusOr> +ApiKeyCredentials::AuthenticationHeaders(std::chrono::system_clock::time_point, + std::string_view) { + std::vector headers; + headers.emplace_back("x-goog-api-key", api_key_); + return headers; } GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/internal/oauth2_api_key_credentials.h b/google/cloud/internal/oauth2_api_key_credentials.h index aec219a29cba0..1ab7141d46ece 100644 --- a/google/cloud/internal/oauth2_api_key_credentials.h +++ b/google/cloud/internal/oauth2_api_key_credentials.h @@ -37,8 +37,9 @@ class ApiKeyCredentials : public oauth2_internal::Credentials { StatusOr GetToken( std::chrono::system_clock::time_point tp) override; - StatusOr> AuthenticationHeader( - std::chrono::system_clock::time_point) override; + StatusOr> AuthenticationHeaders( + std::chrono::system_clock::time_point, + std::string_view endpoint) override; private: std::string api_key_; diff --git a/google/cloud/internal/oauth2_api_key_credentials_test.cc b/google/cloud/internal/oauth2_api_key_credentials_test.cc index 12f51a2257b50..3aaaab21199ae 100644 --- a/google/cloud/internal/oauth2_api_key_credentials_test.cc +++ b/google/cloud/internal/oauth2_api_key_credentials_test.cc @@ -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 #include @@ -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"); @@ -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 diff --git a/google/cloud/internal/oauth2_credentials.cc b/google/cloud/internal/oauth2_credentials.cc index b2f4d53c57d63..c3b166127c0d4 100644 --- a/google/cloud/internal/oauth2_credentials.cc +++ b/google/cloud/internal/oauth2_credentials.cc @@ -22,6 +22,23 @@ namespace cloud { namespace oauth2_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +StatusOr> +Credentials::AuthenticationHeaders(std::chrono::system_clock::time_point tp, + std::string_view endpoint) { + std::vector 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> Credentials::SignBlob( absl::optional const&, std::string const&) const { return internal::UnimplementedError( @@ -46,21 +63,18 @@ StatusOr Credentials::project_id( return project_id(); } -StatusOr> Credentials::AuthenticationHeader( +StatusOr 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 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 Credentials::AllowedLocations( + std::chrono::system_clock::time_point, std::string_view) { + return {}; } GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/internal/oauth2_credentials.h b/google/cloud/internal/oauth2_credentials.h index 7c050f229370c..b52f285579543 100644 --- a/google/cloud/internal/oauth2_credentials.h +++ b/google/cloud/internal/oauth2_credentials.h @@ -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 -#include #include #include @@ -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 GetToken( - std::chrono::system_clock::time_point tp) = 0; + virtual StatusOr> + AuthenticationHeaders(std::chrono::system_clock::time_point tp, + std::string_view endpoint); /** * Try to sign @p string_to_sign using @p service_account. @@ -109,35 +123,44 @@ class Credentials { virtual StatusOr 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> AuthenticationHeader( + virtual StatusOr 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 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 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 GetToken( + std::chrono::system_clock::time_point tp) = 0; +}; GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace oauth2_internal diff --git a/google/cloud/internal/oauth2_credentials_test.cc b/google/cloud/internal/oauth2_credentials_test.cc index 7bdadaac90fff..47679dcdac9d3 100644 --- a/google/cloud/internal/oauth2_credentials_test.cc +++ b/google/cloud/internal/oauth2_credentials_test.cc @@ -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, GetToken, (std::chrono::system_clock::time_point), (override)); + MOCK_METHOD(StatusOr, AllowedLocations, + (std::chrono::system_clock::time_point, std::string_view), + (override)); }; TEST(Credentials, AuthorizationHeaderSuccess) { @@ -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 diff --git a/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc b/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc index cdf940b58e6fd..c2d1b53fe751d 100644 --- a/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc +++ b/google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc @@ -46,12 +46,11 @@ MinimalIamCredentialsRestStub::MinimalIamCredentialsRestStub( StatusOr 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{ diff --git a/google/cloud/internal/unified_rest_credentials_test.cc b/google/cloud/internal/unified_rest_credentials_test.cc index 48760cfa8f0ca..b3d769bf462f0 100644 --- a/google/cloud/internal/unified_rest_credentials_test.cc +++ b/google/cloud/internal/unified_rest_credentials_test.cc @@ -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) { diff --git a/google/cloud/storage/client.cc b/google/cloud/storage/client.cc index 22db63e9346b6..559d2f87fca7c 100644 --- a/google/cloud/storage/client.cc +++ b/google/cloud/storage/client.cc @@ -58,10 +58,6 @@ class WrapRestCredentials { std::shared_ptr impl) : impl_(std::move(impl)) {} - StatusOr AuthorizationHeader() { - return oauth2_internal::AuthenticationHeaderJoined(*impl_); - } - StatusOr> SignBlob( SigningAccount const& signing_account, std::string const& blob) const { return impl_->SignBlob(signing_account.value_or(impl_->AccountEmail()), diff --git a/google/cloud/storage/tests/unified_credentials_integration_test.cc b/google/cloud/storage/tests/unified_credentials_integration_test.cc index 58ad784cc953f..2d5407f17f567 100644 --- a/google/cloud/storage/tests/unified_credentials_integration_test.cc +++ b/google/cloud/storage/tests/unified_credentials_integration_test.cc @@ -21,6 +21,7 @@ #include "google/cloud/internal/unified_rest_credentials.h" #include "google/cloud/testing_util/scoped_environment.h" #include "google/cloud/testing_util/status_matchers.h" +#include "absl/strings/match.h" #include #ifndef _WIN32 #include @@ -43,9 +44,9 @@ using ::google::cloud::UnifiedCredentialsOption; using ::google::cloud::internal::GetEnv; using ::google::cloud::storage::testing::TempFile; using ::google::cloud::testing_util::IsOk; +using ::testing::Contains; using ::testing::IsEmpty; using ::testing::Not; -using ::testing::StartsWith; // This is a properly formatted, but invalid, CA Certificate. We will use this // as the *only* root of trust and try to contact *.google.com. This will @@ -88,6 +89,10 @@ KlXA1yQW/ClmnHVg57SN1g1rvOJCcnHBnSbT7kGFqUol constexpr int kCurleAbortedByCallback = 42; constexpr int kCurleOk = 0; +MATCHER_P(HeaderStartsWith, prefix, "header start with") { + return absl::StartsWith(std::string{arg}, prefix); +} + class UnifiedCredentialsIntegrationTest : public ::google::cloud::storage::testing::StorageIntegrationTest { protected: @@ -375,13 +380,18 @@ TEST_F(UnifiedCredentialsIntegrationTest, AccessToken) { auto default_credentials = rest_internal::MapCredentials((*MakeGoogleDefaultCredentials())); auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1); - auto header = - oauth2_internal::AuthenticationHeaderJoined(*default_credentials); - ASSERT_THAT(header, IsOk()); - - auto constexpr kPrefix = "Authorization: Bearer "; - ASSERT_THAT(*header, StartsWith(kPrefix)); - auto token = header->substr(std::strlen(kPrefix)); + auto headers = default_credentials->AuthenticationHeaders( + std::chrono::system_clock::now(), ""); + ASSERT_THAT(headers, IsOk()); + + auto constexpr kPrefix = "authorization: Bearer "; + ASSERT_THAT(*headers, Contains(HeaderStartsWith(kPrefix))); + std::string authorization; + for (auto const& h : *headers) { + authorization = std::string{h}; + if (absl::StartsWith(authorization, kPrefix)) break; + } + auto token = authorization.substr(std::strlen(kPrefix)); auto client = MakeTestClient(Options{}.set( MakeAccessTokenCredentials(token, expiration))); @@ -401,13 +411,18 @@ TEST_F(UnifiedCredentialsIntegrationTest, AccessTokenCustomTrustStore) { auto default_credentials = rest_internal::MapCredentials((*MakeGoogleDefaultCredentials())); auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1); - auto header = - oauth2_internal::AuthenticationHeaderJoined(*default_credentials); - ASSERT_THAT(header, IsOk()); - - auto constexpr kPrefix = "Authorization: Bearer "; - ASSERT_THAT(*header, StartsWith(kPrefix)); - auto token = header->substr(std::strlen(kPrefix)); + auto headers = default_credentials->AuthenticationHeaders( + std::chrono::system_clock::now(), ""); + ASSERT_THAT(headers, IsOk()); + + auto constexpr kPrefix = "authorization: Bearer "; + ASSERT_THAT(*headers, Contains(HeaderStartsWith(kPrefix))); + std::string authorization; + for (auto const& h : *headers) { + authorization = std::string{h}; + if (absl::StartsWith(authorization, kPrefix)) break; + } + auto token = authorization.substr(std::strlen(kPrefix)); testing_util::ScopedEnvironment grpc_roots_pem( "GRPC_DEFAULT_SSL_ROOTS_FILE_PATH", absl::nullopt); @@ -430,13 +445,18 @@ TEST_F(UnifiedCredentialsIntegrationTest, AccessTokenEmptyTrustStore) { auto default_credentials = rest_internal::MapCredentials((*MakeGoogleDefaultCredentials())); auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1); - auto header = - oauth2_internal::AuthenticationHeaderJoined(*default_credentials); - ASSERT_THAT(header, IsOk()); - - auto constexpr kPrefix = "Authorization: Bearer "; - ASSERT_THAT(*header, StartsWith(kPrefix)); - auto token = header->substr(std::strlen(kPrefix)); + auto headers = default_credentials->AuthenticationHeaders( + std::chrono::system_clock::now(), ""); + ASSERT_THAT(headers, IsOk()); + + auto constexpr kPrefix = "authorization: Bearer "; + ASSERT_THAT(*headers, Contains(HeaderStartsWith(kPrefix))); + std::string authorization; + for (auto const& h : *headers) { + authorization = std::string{h}; + if (absl::StartsWith(authorization, kPrefix)) break; + } + auto token = authorization.substr(std::strlen(kPrefix)); auto client = MakeTestClient( EmptyTrustStoreOptions() From 3388994d7522f76058c220ce201606a2f8f27be1 Mon Sep 17 00:00:00 2001 From: Scott Hart Date: Mon, 6 Apr 2026 12:49:03 -0400 Subject: [PATCH 2/2] check for empty authorization header --- google/cloud/internal/oauth2_credentials.cc | 2 +- google/cloud/internal/oauth2_credentials_test.cc | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/google/cloud/internal/oauth2_credentials.cc b/google/cloud/internal/oauth2_credentials.cc index c3b166127c0d4..23301be9a1a44 100644 --- a/google/cloud/internal/oauth2_credentials.cc +++ b/google/cloud/internal/oauth2_credentials.cc @@ -28,7 +28,7 @@ Credentials::AuthenticationHeaders(std::chrono::system_clock::time_point tp, std::vector headers; auto authorization = Authorization(tp); if (!authorization) return std::move(authorization).status(); - headers.push_back(*std::move(authorization)); + if (!authorization->empty()) headers.push_back(*std::move(authorization)); auto allowed_locations = AllowedLocations(tp, endpoint); // Not all credential types support the x-allowed-locations header. For those diff --git a/google/cloud/internal/oauth2_credentials_test.cc b/google/cloud/internal/oauth2_credentials_test.cc index 47679dcdac9d3..08f52e18727d6 100644 --- a/google/cloud/internal/oauth2_credentials_test.cc +++ b/google/cloud/internal/oauth2_credentials_test.cc @@ -60,6 +60,17 @@ TEST(Credentials, AuthenticationHeaderError) { EXPECT_EQ(actual.status(), UnavailableError("try-again")); } +TEST(Credentials, AuthenticationHeaderEmpty) { + MockCredentials mock; + EXPECT_CALL(mock, GetToken) + .WillOnce(Return(AccessToken{"", std::chrono::system_clock::now()})); + EXPECT_CALL(mock, AllowedLocations) + .WillOnce(Return(rest_internal::HttpHeader())); + auto actual = mock.AuthenticationHeaders(std::chrono::system_clock::now(), + "my-endpoint"); + EXPECT_THAT(actual, IsOkAndHolds(std::vector{})); +} + TEST(Credentials, ProjectId) { MockCredentials mock; EXPECT_THAT(mock.project_id(), Not(IsOk()));