Skip to content

Commit a552c93

Browse files
authored
feat(oauth2): add support for external account workforce identity (#14800)
* feat(oauth2): add support for external account workforce identity * move * avoid cmake dep * format * address the comments
1 parent 852cff0 commit a552c93

3 files changed

+125
-18
lines changed

google/cloud/internal/oauth2_external_account_credentials.cc

+24-5
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,21 @@ StatusOr<ExternalAccountInfo> ParseExternalAccountConfiguration(
9696
MakeExternalAccountTokenSource(*credential_source, *audience, ec);
9797
if (!source) return std::move(source).status();
9898

99-
auto info =
100-
ExternalAccountInfo{*std::move(audience), *std::move(subject_token_type),
101-
*std::move(token_url), *std::move(source),
102-
absl::nullopt, *std::move(universe_domain)};
99+
absl::optional<std::string> workforce_pool_user_project;
100+
auto it = json.find("workforce_pool_user_project");
101+
if (it != json.end()) {
102+
workforce_pool_user_project = it->get<std::string>();
103+
}
104+
105+
auto info = ExternalAccountInfo{*std::move(audience),
106+
*std::move(subject_token_type),
107+
*std::move(token_url),
108+
*std::move(source),
109+
absl::nullopt,
110+
*std::move(universe_domain),
111+
std::move(workforce_pool_user_project)};
103112

104-
auto it = json.find("service_account_impersonation_url");
113+
it = json.find("service_account_impersonation_url");
105114
if (it == json.end()) return info;
106115

107116
auto constexpr kDefaultImpersonationTokenLifetime =
@@ -148,6 +157,16 @@ StatusOr<AccessToken> ExternalAccountCredentials::GetToken(
148157
{"subject_token_type", info_.subject_token_type},
149158
{"subject_token", subject_token->token},
150159
};
160+
161+
// Workforce Identity is handled at the org level and requires the userProject
162+
// header. Workload Identity is handled at the project level and doesn't
163+
// require the header.
164+
if (info_.workforce_pool_user_project) {
165+
form_data.emplace_back(
166+
"options", absl::StrCat(R"({"userProject": ")",
167+
*info_.workforce_pool_user_project, R"("})"));
168+
}
169+
151170
auto request =
152171
rest_internal::RestRequest(info_.token_url)
153172
.AddHeader("content-type", "application/x-www-form-urlencoded");

google/cloud/internal/oauth2_external_account_credentials.h

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ struct ExternalAccountInfo {
6868
ExternalAccountTokenSource token_source;
6969
absl::optional<ExternalAccountImpersonationConfig> impersonation_config;
7070
std::string universe_domain;
71+
absl::optional<std::string> workforce_pool_user_project;
7172
};
7273

7374
/// Parse a JSON string with an external account configuration.

google/cloud/internal/oauth2_external_account_credentials_test.cc

+100-13
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ using ::testing::Contains;
4848
using ::testing::ElementsAre;
4949
using ::testing::HasSubstr;
5050
using ::testing::MatcherCast;
51+
using ::testing::Optional;
5152
using ::testing::Pair;
5253
using ::testing::Property;
5354
using ::testing::ResultOf;
@@ -304,6 +305,27 @@ TEST(ExternalAccount, ParseWithImpersonationDefaultLifetimeSuccess) {
304305
std::chrono::seconds(3600));
305306
}
306307

308+
TEST(ExternalAccount, ParseUserProjectSuccess) {
309+
auto const configuration = nlohmann::json{
310+
{"type", "external_account"},
311+
{"audience", "test-audience"},
312+
{"subject_token_type", "test-subject-token-type"},
313+
{"token_url", "test-token-url"},
314+
{"credential_source", nlohmann::json{{"file", "/dev/null-test-only"}}},
315+
{"workforce_pool_user_project", "project-id-or-name"},
316+
};
317+
auto ec = internal::ErrorContext(
318+
{{"program", "test"}, {"full-configuration", configuration.dump()}});
319+
auto const actual =
320+
ParseExternalAccountConfiguration(configuration.dump(), ec);
321+
ASSERT_STATUS_OK(actual);
322+
EXPECT_EQ(actual->audience, "test-audience");
323+
EXPECT_EQ(actual->subject_token_type, "test-subject-token-type");
324+
EXPECT_EQ(actual->token_url, "test-token-url");
325+
EXPECT_THAT(actual->workforce_pool_user_project,
326+
Optional(std::string("project-id-or-name")));
327+
}
328+
307329
TEST(ExternalAccount, ParseNotJson) {
308330
auto const configuration = std::string{"not-json"};
309331
auto ec = internal::ErrorContext(
@@ -657,7 +679,8 @@ TEST(ExternalAccount, Working) {
657679
auto const info =
658680
ExternalAccountInfo{"test-audience", "test-subject-token-type",
659681
test_url, mock_source,
660-
absl::nullopt, {}};
682+
absl::nullopt, {},
683+
absl::nullopt};
661684

662685
MockClientFactory client_factory;
663686
EXPECT_CALL(client_factory, Call(make_expected_options())).WillOnce([&]() {
@@ -689,6 +712,58 @@ TEST(ExternalAccount, Working) {
689712
EXPECT_EQ(access_token->token, expected_access_token);
690713
}
691714

715+
TEST(ExternalAccount, WorkingWorkforceIdentity) {
716+
auto const test_url = std::string{"https://sts.example.com/"};
717+
auto const expected_access_token = std::string{"test-access-token"};
718+
auto const expected_expires_in = std::chrono::seconds(3456);
719+
auto const json_response = nlohmann::json{
720+
{"access_token", expected_access_token},
721+
{"expires_in", expected_expires_in.count()},
722+
{"issued_token_type", "urn:ietf:params:oauth:token-type:access_token"},
723+
{"token_type", "Bearer"},
724+
};
725+
auto mock_source = [](HttpClientFactory const&, Options const&) {
726+
return make_status_or(internal::SubjectToken{"test-subject-token"});
727+
};
728+
auto const info = ExternalAccountInfo{"test-audience",
729+
"test-subject-token-type",
730+
test_url,
731+
mock_source,
732+
absl::nullopt,
733+
{},
734+
"project-id-or-name"};
735+
736+
MockClientFactory client_factory;
737+
EXPECT_CALL(client_factory, Call(make_expected_options())).WillOnce([&]() {
738+
auto mock = std::make_unique<MockRestClient>();
739+
auto expected_request = make_expected_token_exchange_request(test_url);
740+
auto expected_payload =
741+
MatcherCast<FormDataType const&>(UnorderedElementsAre(
742+
Pair("grant_type",
743+
"urn:ietf:params:oauth:grant-type:token-exchange"),
744+
Pair("requested_token_type",
745+
"urn:ietf:params:oauth:token-type:access_token"),
746+
Pair("scope", "https://www.googleapis.com/auth/cloud-platform"),
747+
Pair("audience", "test-audience"),
748+
Pair("subject_token_type", "test-subject-token-type"),
749+
Pair("subject_token", "test-subject-token"),
750+
Pair("options", R"({"userProject": "project-id-or-name"})")));
751+
EXPECT_CALL(*mock, Post(_, expected_request, expected_payload))
752+
.WillOnce(
753+
Return(ByMove(MakeMockResponseSuccess(json_response.dump()))));
754+
return mock;
755+
});
756+
757+
auto credentials =
758+
ExternalAccountCredentials(info, client_factory.AsStdFunction(),
759+
Options{}.set<TestOnlyOption>("test-option"));
760+
auto const now = std::chrono::system_clock::now();
761+
auto access_token = credentials.GetToken(now);
762+
ASSERT_STATUS_OK(access_token);
763+
EXPECT_EQ(access_token->expiration, now + expected_expires_in);
764+
EXPECT_EQ(access_token->token, expected_access_token);
765+
}
766+
692767
TEST(ExternalAccount, WorkingWithImpersonation) {
693768
auto const sts_test_url = std::string{"https://sts.example.com/"};
694769
auto const sts_access_token = std::string{"test-sts-access-token"};
@@ -727,7 +802,8 @@ TEST(ExternalAccount, WorkingWithImpersonation) {
727802
mock_source,
728803
ExternalAccountImpersonationConfig{
729804
impersonate_test_url, impersonate_test_lifetime},
730-
{}};
805+
{},
806+
absl::nullopt};
731807

732808
auto sts_client = [&] {
733809
auto expected_sts_request = Property(&RestRequest::path, sts_test_url);
@@ -798,7 +874,8 @@ TEST(ExternalAccount, HandleHttpError) {
798874
auto const info =
799875
ExternalAccountInfo{"test-audience", "test-subject-token-type",
800876
test_url, mock_source,
801-
absl::nullopt, {}};
877+
absl::nullopt, {},
878+
absl::nullopt};
802879
MockClientFactory client_factory;
803880
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
804881
auto mock = std::make_unique<MockRestClient>();
@@ -836,7 +913,8 @@ TEST(ExternalAccount, HandleHttpPartialError) {
836913
auto const info =
837914
ExternalAccountInfo{"test-audience", "test-subject-token-type",
838915
test_url, mock_source,
839-
absl::nullopt, {}};
916+
absl::nullopt, {},
917+
absl::nullopt};
840918
MockClientFactory client_factory;
841919
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
842920
auto mock = std::make_unique<MockRestClient>();
@@ -875,7 +953,8 @@ TEST(ExternalAccount, HandleNotJson) {
875953
auto const info =
876954
ExternalAccountInfo{"test-audience", "test-subject-token-type",
877955
test_url, mock_source,
878-
absl::nullopt, {}};
956+
absl::nullopt, {},
957+
absl::nullopt};
879958
MockClientFactory client_factory;
880959
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
881960
auto mock = std::make_unique<MockRestClient>();
@@ -914,7 +993,8 @@ TEST(ExternalAccount, HandleNotJsonObject) {
914993
auto const info =
915994
ExternalAccountInfo{"test-audience", "test-subject-token-type",
916995
test_url, mock_source,
917-
absl::nullopt, {}};
996+
absl::nullopt, {},
997+
absl::nullopt};
918998
MockClientFactory client_factory;
919999
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
9201000
auto mock = std::make_unique<MockRestClient>();
@@ -959,7 +1039,8 @@ TEST(ExternalAccount, MissingToken) {
9591039
auto const info =
9601040
ExternalAccountInfo{"test-audience", "test-subject-token-type",
9611041
test_url, mock_source,
962-
absl::nullopt, {}};
1042+
absl::nullopt, {},
1043+
absl::nullopt};
9631044
MockClientFactory client_factory;
9641045
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
9651046
auto mock = std::make_unique<MockRestClient>();
@@ -993,7 +1074,8 @@ TEST(ExternalAccount, MissingIssuedTokenType) {
9931074
auto const info =
9941075
ExternalAccountInfo{"test-audience", "test-subject-token-type",
9951076
test_url, mock_source,
996-
absl::nullopt, {}};
1077+
absl::nullopt, {},
1078+
absl::nullopt};
9971079
MockClientFactory client_factory;
9981080
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
9991081
auto mock = std::make_unique<MockRestClient>();
@@ -1027,7 +1109,8 @@ TEST(ExternalAccount, MissingTokenType) {
10271109
auto const info =
10281110
ExternalAccountInfo{"test-audience", "test-subject-token-type",
10291111
test_url, mock_source,
1030-
absl::nullopt, {}};
1112+
absl::nullopt, {},
1113+
absl::nullopt};
10311114
MockClientFactory client_factory;
10321115
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
10331116
auto mock = std::make_unique<MockRestClient>();
@@ -1061,7 +1144,8 @@ TEST(ExternalAccount, InvalidIssuedTokenType) {
10611144
auto const info =
10621145
ExternalAccountInfo{"test-audience", "test-subject-token-type",
10631146
test_url, mock_source,
1064-
absl::nullopt, {}};
1147+
absl::nullopt, {},
1148+
absl::nullopt};
10651149
MockClientFactory client_factory;
10661150
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
10671151
auto mock = std::make_unique<MockRestClient>();
@@ -1097,7 +1181,8 @@ TEST(ExternalAccount, InvalidTokenType) {
10971181
auto const info =
10981182
ExternalAccountInfo{"test-audience", "test-subject-token-type",
10991183
test_url, mock_source,
1100-
absl::nullopt, {}};
1184+
absl::nullopt, {},
1185+
absl::nullopt};
11011186
MockClientFactory client_factory;
11021187
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
11031188
auto mock = std::make_unique<MockRestClient>();
@@ -1134,7 +1219,8 @@ TEST(ExternalAccount, MissingExpiresIn) {
11341219
auto const info =
11351220
ExternalAccountInfo{"test-audience", "test-subject-token-type",
11361221
test_url, mock_source,
1137-
absl::nullopt, {}};
1222+
absl::nullopt, {},
1223+
absl::nullopt};
11381224
MockClientFactory client_factory;
11391225
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
11401226
auto mock = std::make_unique<MockRestClient>();
@@ -1169,7 +1255,8 @@ TEST(ExternalAccount, InvalidExpiresIn) {
11691255
auto const info =
11701256
ExternalAccountInfo{"test-audience", "test-subject-token-type",
11711257
test_url, mock_source,
1172-
absl::nullopt, {}};
1258+
absl::nullopt, {},
1259+
absl::nullopt};
11731260
MockClientFactory client_factory;
11741261
EXPECT_CALL(client_factory, Call).WillOnce([&]() {
11751262
auto mock = std::make_unique<MockRestClient>();

0 commit comments

Comments
 (0)