From e3b27515278d3a4b1d9854498ee7b2bc5fabbc2f Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Wed, 5 Aug 2020 16:10:32 -0700
Subject: [PATCH 01/23] feat: add STS token exchange utility (#454)
* feat: adds an internal token exchange utility based on https://tools.ietf.org/html/rfc8693
* fix: add copyright and address other comments
* fix: fix formatting
* fix: fixes copyright again
* fix: revert pom changes
* fix: revert auto-value changes
* fix: remove gson and address other comments
* fix: fixes to StsRequestHandlerTest
---
.../google/auth/oauth2/OAuthException.java | 79 +++++
.../google/auth/oauth2/StsRequestHandler.java | 238 ++++++++++++++
.../auth/oauth2/StsTokenExchangeRequest.java | 201 ++++++++++++
.../auth/oauth2/StsTokenExchangeResponse.java | 134 ++++++++
.../auth/oauth2/MockStsServiceTransport.java | 136 ++++++++
.../auth/oauth2/OAuthExceptionTest.java | 88 +++++
.../auth/oauth2/StsRequestHandlerTest.java | 304 ++++++++++++++++++
oauth2_http/pom.xml | 6 +
8 files changed, 1186 insertions(+)
create mode 100644 oauth2_http/java/com/google/auth/oauth2/OAuthException.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/MockStsServiceTransport.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java
new file mode 100644
index 000000000..c4304f2b0
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
+import javax.annotation.Nullable;
+
+class OAuthException extends IOException {
+ private static final String FULL_MESSAGE_FORMAT = "Error code %s: %s - %s";
+ private static final String ERROR_DESCRIPTION_FORMAT = "Error code %s: %s";
+ private static final String BASE_MESSAGE_FORMAT = "Error code %s";
+
+ private String errorCode;
+ @Nullable private String errorDescription;
+ @Nullable private String errorUri;
+
+ public OAuthException(
+ String errorCode, @Nullable String errorDescription, @Nullable String errorUri) {
+ this.errorCode = checkNotNull(errorCode);
+ this.errorDescription = errorDescription;
+ this.errorUri = errorUri;
+ }
+
+ @Override
+ public String getMessage() {
+ if (errorDescription != null && errorUri != null) {
+ return String.format(FULL_MESSAGE_FORMAT, errorCode, errorDescription, errorUri);
+ }
+ if (errorDescription != null) {
+ return String.format(ERROR_DESCRIPTION_FORMAT, errorCode, errorDescription);
+ }
+ return String.format(BASE_MESSAGE_FORMAT, errorCode);
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ @Nullable
+ public String getErrorDescription() {
+ return errorDescription;
+ }
+
+ @Nullable
+ public String getErrorUri() {
+ return errorUri;
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
new file mode 100644
index 000000000..9e2559e0a
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.http.UrlEncodedContent;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.api.client.json.JsonParser;
+import com.google.api.client.util.GenericData;
+import com.google.api.client.util.Joiner;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693.
+ *
+ *
TODO(lsirac): Add client auth support. TODO(lsirac): Ensure request content is formatted
+ * correctly.
+ */
+public class StsRequestHandler {
+ private static final String TOKEN_EXCHANGE_GRANT_TYPE =
+ "urn:ietf:params:oauth:grant-type:token-exchange";
+ private static final String REQUESTED_TOKEN_TYPE =
+ "urn:ietf:params:oauth:token-type:access_token";
+ private static final String CLOUD_PLATFORM_SCOPE =
+ "https://www.googleapis.com/auth/cloud-platform";
+ private static final String PARSE_ERROR_PREFIX = "Error parsing token response.";
+
+ private String tokenExchangeEndpoint;
+ private StsTokenExchangeRequest request;
+ private HttpRequestFactory httpRequestFactory;
+
+ @Nullable private HttpHeaders headers;
+ @Nullable private String internalOptions;
+
+ /**
+ * Internal constructor.
+ *
+ * @param tokenExchangeEndpoint The token exchange endpoint.
+ * @param request The token exchange request.
+ * @param headers Optional additional headers to pass along the request.
+ * @param internalOptions Optional GCP specific STS options.
+ * @return An StsTokenExchangeResponse instance if the request was successful.
+ */
+ private StsRequestHandler(
+ String tokenExchangeEndpoint,
+ StsTokenExchangeRequest request,
+ HttpRequestFactory httpRequestFactory,
+ @Nullable HttpHeaders headers,
+ @Nullable String internalOptions) {
+ this.tokenExchangeEndpoint = tokenExchangeEndpoint;
+ this.request = request;
+ this.httpRequestFactory = httpRequestFactory;
+ this.headers = headers;
+ this.internalOptions = internalOptions;
+ }
+
+ public static Builder newBuilder(
+ String tokenExchangeEndpoint,
+ StsTokenExchangeRequest stsTokenExchangeRequest,
+ HttpRequestFactory httpRequestFactory) {
+ return new Builder(tokenExchangeEndpoint, stsTokenExchangeRequest, httpRequestFactory);
+ }
+
+ /** Exchanges the provided token for another type of token based on the rfc8693 spec. */
+ public StsTokenExchangeResponse exchangeToken() throws IOException {
+ UrlEncodedContent content = new UrlEncodedContent(buildTokenRequest());
+
+ HttpRequest httpRequest =
+ httpRequestFactory.buildPostRequest(new GenericUrl(tokenExchangeEndpoint), content);
+ httpRequest.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
+ if (headers != null) {
+ httpRequest.setHeaders(headers);
+ }
+
+ try {
+ HttpResponse response = httpRequest.execute();
+ GenericData responseData = response.parseAs(GenericData.class);
+ return buildResponse(responseData);
+ } catch (IOException e) {
+ if (!(e instanceof HttpResponseException)) {
+ throw e;
+ }
+ GenericJson errorResponse = parseJson(((HttpResponseException) e).getContent());
+ String errorCode = (String) errorResponse.get("error");
+ String errorDescription = null;
+ String errorUri = null;
+ if (errorResponse.containsKey("error_description")) {
+ errorDescription = (String) errorResponse.get("error_description");
+ }
+ if (errorResponse.containsKey("error_uri")) {
+ errorUri = (String) errorResponse.get("error_uri");
+ }
+ throw new OAuthException(errorCode, errorDescription, errorUri);
+ }
+ }
+
+ private GenericData buildTokenRequest() {
+ GenericData tokenRequest =
+ new GenericData()
+ .set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE)
+ .set("scope", CLOUD_PLATFORM_SCOPE)
+ .set("subject_token_type", request.getSubjectTokenType())
+ .set("subject_token", request.getSubjectToken());
+
+ // Add scopes as a space-delimited string.
+ List scopes = new ArrayList<>();
+ scopes.add(CLOUD_PLATFORM_SCOPE);
+ if (request.hasScopes()) {
+ scopes.addAll(request.getScopes());
+ }
+ tokenRequest.set("scope", Joiner.on(' ').join(scopes));
+
+ // Set the requested token type, which defaults to
+ // urn:ietf:params:oauth:token-type:access_token.
+ String requestTokenType =
+ request.hasRequestedTokenType() ? request.getRequestedTokenType() : REQUESTED_TOKEN_TYPE;
+ tokenRequest.set("requested_token_type", requestTokenType);
+
+ // Add other optional params, if possible.
+ if (request.hasResource()) {
+ tokenRequest.set("resource", request.getResource());
+ }
+ if (request.hasAudience()) {
+ tokenRequest.set("audience", request.getAudience());
+ }
+
+ if (request.hasActingParty()) {
+ tokenRequest.set("actor_token", request.getActingParty().getActorToken());
+ tokenRequest.set("actor_token_type", request.getActingParty().getActorTokenType());
+ }
+
+ if (internalOptions != null && !internalOptions.isEmpty()) {
+ tokenRequest.set("options", internalOptions);
+ }
+ return tokenRequest;
+ }
+
+ private StsTokenExchangeResponse buildResponse(GenericData responseData) throws IOException {
+ String accessToken =
+ OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX);
+ String issuedTokenType =
+ OAuth2Utils.validateString(responseData, "issued_token_type", PARSE_ERROR_PREFIX);
+ String tokenType = OAuth2Utils.validateString(responseData, "token_type", PARSE_ERROR_PREFIX);
+ Long expiresInSeconds =
+ OAuth2Utils.validateLong(responseData, "expires_in", PARSE_ERROR_PREFIX);
+
+ StsTokenExchangeResponse.Builder builder =
+ StsTokenExchangeResponse.newBuilder(
+ accessToken, issuedTokenType, tokenType, expiresInSeconds);
+
+ if (responseData.containsKey("refresh_token")) {
+ builder.setRefreshToken(
+ OAuth2Utils.validateString(responseData, "refresh_token", PARSE_ERROR_PREFIX));
+ }
+ if (responseData.containsKey("scope")) {
+ String scope = OAuth2Utils.validateString(responseData, "scope", PARSE_ERROR_PREFIX);
+ builder.setScopes(Arrays.asList(scope.trim().split("\\s+")));
+ }
+ return builder.build();
+ }
+
+ private GenericJson parseJson(String json) throws IOException {
+ JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(json);
+ return parser.parseAndClose(GenericJson.class);
+ }
+
+ public static class Builder {
+ private String tokenExchangeEndpoint;
+ private StsTokenExchangeRequest request;
+ private HttpRequestFactory httpRequestFactory;
+
+ @Nullable private HttpHeaders headers;
+ @Nullable private String internalOptions;
+
+ private Builder(
+ String tokenExchangeEndpoint,
+ StsTokenExchangeRequest stsTokenExchangeRequest,
+ HttpRequestFactory httpRequestFactory) {
+ this.tokenExchangeEndpoint = tokenExchangeEndpoint;
+ this.request = stsTokenExchangeRequest;
+ this.httpRequestFactory = httpRequestFactory;
+ }
+
+ public StsRequestHandler.Builder setHeaders(HttpHeaders headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ public StsRequestHandler.Builder setInternalOptions(String internalOptions) {
+ this.internalOptions = internalOptions;
+ return this;
+ }
+
+ public StsRequestHandler build() {
+ return new StsRequestHandler(
+ tokenExchangeEndpoint, request, httpRequestFactory, headers, internalOptions);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java
new file mode 100644
index 000000000..bcd3e95a9
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Defines an OAuth 2.0 token exchange request. Based on
+ * https://tools.ietf.org/html/rfc8693#section-2.1.
+ */
+public class StsTokenExchangeRequest {
+ private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
+
+ private String subjectToken;
+ private String subjectTokenType;
+
+ @Nullable private ActingParty actingParty;
+ @Nullable private List scopes;
+ @Nullable private String resource;
+ @Nullable private String audience;
+ @Nullable private String requestedTokenType;
+
+ private StsTokenExchangeRequest(
+ String subjectToken,
+ String subjectTokenType,
+ @Nullable ActingParty actingParty,
+ @Nullable List scopes,
+ @Nullable String resource,
+ @Nullable String audience,
+ @Nullable String requestedTokenType) {
+ this.subjectToken = checkNotNull(subjectToken);
+ this.subjectTokenType = checkNotNull(subjectTokenType);
+ this.actingParty = actingParty;
+ this.scopes = scopes;
+ this.resource = resource;
+ this.audience = audience;
+ this.requestedTokenType = requestedTokenType;
+ }
+
+ public static Builder newBuilder(String subjectToken, String subjectTokenType) {
+ return new Builder(subjectToken, subjectTokenType);
+ }
+
+ public String getGrantType() {
+ return GRANT_TYPE;
+ }
+
+ public String getSubjectToken() {
+ return subjectToken;
+ }
+
+ public String getSubjectTokenType() {
+ return subjectTokenType;
+ }
+
+ @Nullable
+ public String getResource() {
+ return resource;
+ }
+
+ @Nullable
+ public String getAudience() {
+ return audience;
+ }
+
+ @Nullable
+ public String getRequestedTokenType() {
+ return requestedTokenType;
+ }
+
+ @Nullable
+ public List getScopes() {
+ return scopes;
+ }
+
+ @Nullable
+ public ActingParty getActingParty() {
+ return actingParty;
+ }
+
+ public boolean hasResource() {
+ return resource != null && !resource.isEmpty();
+ }
+
+ public boolean hasAudience() {
+ return audience != null && !audience.isEmpty();
+ }
+
+ public boolean hasRequestedTokenType() {
+ return requestedTokenType != null && !requestedTokenType.isEmpty();
+ }
+
+ public boolean hasScopes() {
+ return scopes != null && !scopes.isEmpty();
+ }
+
+ public boolean hasActingParty() {
+ return actingParty != null;
+ }
+
+ public static class Builder {
+ String subjectToken;
+ String subjectTokenType;
+ String resource;
+ String audience;
+ String requestedTokenType;
+ List scopes;
+ ActingParty actingParty;
+
+ private Builder(String subjectToken, String subjectTokenType) {
+ this.subjectToken = subjectToken;
+ this.subjectTokenType = subjectTokenType;
+ }
+
+ public StsTokenExchangeRequest.Builder setResource(String resource) {
+ this.resource = resource;
+ return this;
+ }
+
+ public StsTokenExchangeRequest.Builder setAudience(String audience) {
+ this.audience = audience;
+ return this;
+ }
+
+ public StsTokenExchangeRequest.Builder setRequestTokenType(String requestedTokenType) {
+ this.requestedTokenType = requestedTokenType;
+ return this;
+ }
+
+ public StsTokenExchangeRequest.Builder setScopes(List scopes) {
+ this.scopes = scopes;
+ return this;
+ }
+
+ public StsTokenExchangeRequest.Builder setActingParty(ActingParty actingParty) {
+ this.actingParty = actingParty;
+ return this;
+ }
+
+ public StsTokenExchangeRequest build() {
+ return new StsTokenExchangeRequest(
+ subjectToken,
+ subjectTokenType,
+ actingParty,
+ scopes,
+ resource,
+ audience,
+ requestedTokenType);
+ }
+ }
+
+ static class ActingParty {
+ private String actorToken;
+ private String actorTokenType;
+
+ public ActingParty(String actorToken, String actorTokenType) {
+ this.actorToken = checkNotNull(actorToken);
+ this.actorTokenType = checkNotNull(actorTokenType);
+ }
+
+ public String getActorToken() {
+ return actorToken;
+ }
+
+ public String getActorTokenType() {
+ return actorTokenType;
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java
new file mode 100644
index 000000000..93655ccab
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.api.client.util.Preconditions.checkNotNull;
+
+import com.google.api.client.util.Clock;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Defines an OAuth 2.0 token exchange successful response. Based on
+ * https://tools.ietf.org/html/rfc8693#section-2.2.1.
+ */
+public class StsTokenExchangeResponse {
+ private AccessToken accessToken;
+ private String issuedTokenType;
+ private String tokenType;
+ private Long expiresIn;
+
+ @Nullable private String refreshToken;
+ @Nullable private List scopes;
+
+ private StsTokenExchangeResponse(
+ String accessToken,
+ String issuedTokenType,
+ String tokenType,
+ Long expiresIn,
+ @Nullable String refreshToken,
+ @Nullable List scopes) {
+ checkNotNull(accessToken);
+ this.expiresIn = checkNotNull(expiresIn);
+ long expiresAtMilliseconds = Clock.SYSTEM.currentTimeMillis() + expiresIn * 1000L;
+ this.accessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds));
+ this.issuedTokenType = checkNotNull(issuedTokenType);
+ this.tokenType = checkNotNull(tokenType);
+ this.refreshToken = refreshToken;
+ this.scopes = scopes;
+ }
+
+ public static Builder newBuilder(
+ String accessToken, String issuedTokenType, String tokenType, Long expiresIn) {
+ return new Builder(accessToken, issuedTokenType, tokenType, expiresIn);
+ }
+
+ public AccessToken getAccessToken() {
+ return accessToken;
+ }
+
+ public String getIssuedTokenType() {
+ return issuedTokenType;
+ }
+
+ public String getTokenType() {
+ return tokenType;
+ }
+
+ public Long getExpiresIn() {
+ return expiresIn;
+ }
+
+ @Nullable
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ @Nullable
+ public List getScopes() {
+ return new ArrayList<>(scopes);
+ }
+
+ public static class Builder {
+ private String accessToken;
+ private String issuedTokenType;
+ private String tokenType;
+ private Long expiresIn;
+
+ @Nullable private String refreshToken;
+ @Nullable private List scopes;
+
+ private Builder(String accessToken, String issuedTokenType, String tokenType, Long expiresIn) {
+ this.accessToken = accessToken;
+ this.issuedTokenType = issuedTokenType;
+ this.tokenType = tokenType;
+ this.expiresIn = expiresIn;
+ }
+
+ public StsTokenExchangeResponse.Builder setRefreshToken(String refreshToken) {
+ this.refreshToken = refreshToken;
+ return this;
+ }
+
+ public StsTokenExchangeResponse.Builder setScopes(List scopes) {
+ this.scopes = scopes;
+ return this;
+ }
+
+ public StsTokenExchangeResponse build() {
+ return new StsTokenExchangeResponse(
+ accessToken, issuedTokenType, tokenType, expiresIn, refreshToken, scopes);
+ }
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockStsServiceTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockStsServiceTransport.java
new file mode 100644
index 000000000..8c8e4fcfd
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockStsServiceTransport.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.Json;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.api.client.util.Joiner;
+import com.google.auth.TestUtils;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+
+/** Mock transport that simulates STS. */
+public class MockStsServiceTransport extends MockHttpTransport {
+ private static final String EXPECTED_GRANT_TYPE =
+ "urn:ietf:params:oauth:grant-type:token-exchange";
+ private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
+ private static final String TOKEN_TYPE = "Bearer";
+ private static final String ACCESS_TOKEN = "accessToken";
+ private static final int EXPIRES_IN = 3600;
+
+ private static final JsonFactory JSON_FACTORY = new JacksonFactory();
+
+ private Queue responseErrorSequence = new ArrayDeque<>();
+ private Queue refreshTokenSequence = new ArrayDeque<>();
+ private Queue> scopeSequence = new ArrayDeque<>();
+ private MockLowLevelHttpRequest request;
+
+ public void addResponseErrorSequence(IOException... errors) {
+ Collections.addAll(responseErrorSequence, errors);
+ }
+
+ public void addRefreshTokenSequence(String... refreshTokens) {
+ Collections.addAll(refreshTokenSequence, refreshTokens);
+ }
+
+ public void addScopeSequence(List... scopes) {
+ Collections.addAll(scopeSequence, scopes);
+ }
+
+ public MockLowLevelHttpRequest getRequest() {
+ return request;
+ }
+
+ public String getTokenType() {
+ return TOKEN_TYPE;
+ }
+
+ public String getAccessToken() {
+ return ACCESS_TOKEN;
+ }
+
+ public String getIssuedTokenType() {
+ return ISSUED_TOKEN_TYPE;
+ }
+
+ public int getExpiresIn() {
+ return EXPIRES_IN;
+ }
+
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) {
+ this.request =
+ new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ if (!responseErrorSequence.isEmpty()) {
+ throw responseErrorSequence.poll();
+ }
+ Map query = TestUtils.parseQuery(getContentAsString());
+ assertThat(query.get("grant_type")).isEqualTo(EXPECTED_GRANT_TYPE);
+ assertThat(query.get("subject_token_type")).isNotEmpty();
+ assertThat(query.get("subject_token")).isNotEmpty();
+
+ GenericJson response = new GenericJson();
+ response.setFactory(JSON_FACTORY);
+ response.put("token_type", TOKEN_TYPE);
+ response.put("expires_in", EXPIRES_IN);
+ response.put("access_token", ACCESS_TOKEN);
+ response.put("issued_token_type", ISSUED_TOKEN_TYPE);
+
+ if (!refreshTokenSequence.isEmpty()) {
+ response.put("refresh_token", refreshTokenSequence.poll());
+ }
+ if (!scopeSequence.isEmpty()) {
+ response.put("scope", Joiner.on(' ').join(scopeSequence.poll()));
+ }
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(response.toPrettyString());
+ }
+ };
+ return this.request;
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
new file mode 100644
index 000000000..10e2af193
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link OAuthException}. */
+@RunWith(JUnit4.class)
+public final class OAuthExceptionTest {
+ private static final String FULL_MESSAGE_FORMAT = "Error code %s: %s - %s";
+ private static final String ERROR_DESCRIPTION_FORMAT = "Error code %s: %s";
+ private static final String BASE_MESSAGE_FORMAT = "Error code %s";
+
+ private static final String ERROR_CODE = "errorCode";
+ private static final String ERROR_DESCRIPTION = "errorDescription";
+ private static final String ERROR_URI = "errorUri";
+
+ @Test
+ public void getMessage_fullFormat() {
+ OAuthException e = new OAuthException(ERROR_CODE, ERROR_DESCRIPTION, ERROR_URI);
+
+ assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
+ assertThat(e.getErrorDescription()).isEqualTo(ERROR_DESCRIPTION);
+ assertThat(e.getErrorUri()).isEqualTo(ERROR_URI);
+
+ String expectedMessage =
+ String.format(FULL_MESSAGE_FORMAT, ERROR_CODE, ERROR_DESCRIPTION, ERROR_URI);
+ assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ }
+
+ @Test
+ public void getMessage_descriptionFormat() {
+ OAuthException e = new OAuthException(ERROR_CODE, ERROR_DESCRIPTION, /* errorUri= */ null);
+
+ assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
+ assertThat(e.getErrorDescription()).isEqualTo(ERROR_DESCRIPTION);
+ assertThat(e.getErrorUri()).isNull();
+
+ String expectedMessage = String.format(ERROR_DESCRIPTION_FORMAT, ERROR_CODE, ERROR_DESCRIPTION);
+ assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ }
+
+ @Test
+ public void getMessage_baseFormat() {
+ OAuthException e =
+ new OAuthException(ERROR_CODE, /* errorDescription= */ null, /* errorUri= */ null);
+
+ assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
+ assertThat(e.getErrorDescription()).isNull();
+ assertThat(e.getErrorUri()).isNull();
+
+ String expectedMessage = String.format(BASE_MESSAGE_FORMAT, ERROR_CODE);
+ assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
new file mode 100644
index 000000000..7f1be7ab6
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.util.GenericData;
+import com.google.api.client.util.Joiner;
+import com.google.auth.TestUtils;
+import com.google.auth.oauth2.StsTokenExchangeRequest.ActingParty;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StsRequestHandler}. */
+@RunWith(JUnit4.class)
+public final class StsRequestHandlerTest {
+
+ private static final String TOKEN_EXCHANGE_GRANT_TYPE =
+ "urn:ietf:params:oauth:grant-type:token-exchange";
+ private static final String CLOUD_PLATFORM_SCOPE =
+ "https://www.googleapis.com/auth/cloud-platform";
+ private static final String DEFAULT_REQUESTED_TOKEN_TYPE =
+ "urn:ietf:params:oauth:token-type:access_token";
+ private static final String TOKEN_URL = "https://www.sts.google.com";
+ private static final String CREDENTIAL = "credential";
+ private static final String SUBJECT_TOKEN_TYPE = "subjectTokenType";
+
+ // Optional params.
+ private static final String AUDIENCE = "audience";
+ private static final String RESOURCE = "resource";
+ private static final String ACTOR_TOKEN = "actorToken";
+ private static final String ACTOR_TOKEN_TYPE = "actorTokenType";
+ private static final String REQUESTED_TOKEN_TYPE = "requestedTokenType";
+ private static final String INTERNAL_OPTIONS = "internalOptions";
+ private static final String REFRESH_TOKEN = "refreshToken";
+ private static final List SCOPES = Arrays.asList("scope1", "scope2", "scope3");
+
+ // Headers.
+ private static final String CONTENT_TYPE_KEY = "content-type";
+ private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
+ private static final String ACCEPT_ENCODING_KEY = "accept-encoding";
+ private static final String ACCEPT_ENCODING = "gzip";
+ private static final String CUSTOM_HEADER_KEY = "custom_header_key";
+ private static final String CUSTOM_HEADER_VALUE = "custom_header_value";
+
+ private static final String INVALID_REQUEST = "invalid_request";
+ private static final String ERROR_DESCRIPTION = "errorDescription";
+ private static final String ERROR_URI = "errorUri";
+
+ private MockStsServiceTransport transport;
+
+ @Before
+ public void setup() {
+ transport = new MockStsServiceTransport();
+ }
+
+ @Test
+ public void exchangeToken() throws IOException {
+ StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+
+ StsRequestHandler requestHandler =
+ StsRequestHandler.newBuilder(
+ TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory())
+ .build();
+
+ StsTokenExchangeResponse response = requestHandler.exchangeToken();
+
+ // Validate response.
+ assertThat(response.getAccessToken().getTokenValue()).isEqualTo(transport.getAccessToken());
+ assertThat(response.getTokenType()).isEqualTo(transport.getTokenType());
+ assertThat(response.getIssuedTokenType()).isEqualTo(transport.getIssuedTokenType());
+ assertThat(response.getExpiresIn()).isEqualTo(transport.getExpiresIn());
+
+ // Validate request content.
+ GenericData expectedRequestContent =
+ new GenericData()
+ .set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE)
+ .set("scope", CLOUD_PLATFORM_SCOPE)
+ .set("requested_token_type", DEFAULT_REQUESTED_TOKEN_TYPE)
+ .set("subject_token_type", stsTokenExchangeRequest.getSubjectTokenType())
+ .set("subject_token", stsTokenExchangeRequest.getSubjectToken());
+
+ MockLowLevelHttpRequest request = transport.getRequest();
+ Map actualRequestContent = TestUtils.parseQuery(request.getContentAsString());
+ assertThat(actualRequestContent).isEqualTo(expectedRequestContent);
+ }
+
+ @Test
+ public void exchangeToken_withOptionalParams() throws IOException {
+ // Return optional params scope and the refresh_token.
+ List scopesToReturn = new ArrayList<>();
+ scopesToReturn.add(CLOUD_PLATFORM_SCOPE);
+ scopesToReturn.addAll(SCOPES);
+
+ transport.addScopeSequence(scopesToReturn);
+ transport.addRefreshTokenSequence(REFRESH_TOKEN);
+
+ // Build the token exchange request.
+ StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE)
+ .setAudience(AUDIENCE)
+ .setResource(RESOURCE)
+ .setActingParty(new ActingParty(ACTOR_TOKEN, ACTOR_TOKEN_TYPE))
+ .setRequestTokenType(REQUESTED_TOKEN_TYPE)
+ .setScopes(SCOPES)
+ .build();
+
+ HttpHeaders httpHeaders =
+ new HttpHeaders()
+ .setContentType(CONTENT_TYPE)
+ .setAcceptEncoding(ACCEPT_ENCODING)
+ .set(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE);
+
+ StsRequestHandler requestHandler =
+ StsRequestHandler.newBuilder(
+ TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory())
+ .setHeaders(httpHeaders)
+ .setInternalOptions(INTERNAL_OPTIONS)
+ .build();
+
+ StsTokenExchangeResponse response = requestHandler.exchangeToken();
+
+ // Validate response.
+ List expectedScopes = new ArrayList<>();
+ expectedScopes.add(CLOUD_PLATFORM_SCOPE);
+ expectedScopes.addAll(SCOPES);
+ String spaceDelimitedScopes = Joiner.on(' ').join(expectedScopes);
+
+ assertThat(response.getAccessToken().getTokenValue()).isEqualTo(transport.getAccessToken());
+ assertThat(response.getTokenType()).isEqualTo(transport.getTokenType());
+ assertThat(response.getIssuedTokenType()).isEqualTo(transport.getIssuedTokenType());
+ assertThat(response.getExpiresIn()).isEqualTo(transport.getExpiresIn());
+ assertThat(response.getScopes()).isEqualTo(scopesToReturn);
+ assertThat(response.getRefreshToken()).isEqualTo(REFRESH_TOKEN);
+
+ // Validate headers.
+ MockLowLevelHttpRequest request = transport.getRequest();
+ Map> requestHeaders = request.getHeaders();
+ assertThat(requestHeaders.get(CONTENT_TYPE_KEY).get(0)).isEqualTo(CONTENT_TYPE);
+ assertThat(requestHeaders.get(ACCEPT_ENCODING_KEY).get(0)).isEqualTo(ACCEPT_ENCODING);
+ assertThat(requestHeaders.get(CUSTOM_HEADER_KEY).get(0)).isEqualTo(CUSTOM_HEADER_VALUE);
+
+ // Validate request content.
+ GenericData expectedRequestContent =
+ new GenericData()
+ .set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE)
+ .set("scope", spaceDelimitedScopes)
+ .set("options", INTERNAL_OPTIONS)
+ .set("subject_token_type", stsTokenExchangeRequest.getSubjectTokenType())
+ .set("subject_token", stsTokenExchangeRequest.getSubjectToken())
+ .set("requested_token_type", stsTokenExchangeRequest.getRequestedTokenType())
+ .set("actor_token", stsTokenExchangeRequest.getActingParty().getActorToken())
+ .set("actor_token_type", stsTokenExchangeRequest.getActingParty().getActorTokenType())
+ .set("resource", stsTokenExchangeRequest.getResource())
+ .set("audience", stsTokenExchangeRequest.getAudience());
+
+ Map actualRequestContent = TestUtils.parseQuery(request.getContentAsString());
+ assertThat(actualRequestContent).isEqualTo(expectedRequestContent);
+ }
+
+ @Test
+ public void exchangeToken_throwsException() throws IOException {
+ StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+
+ final StsRequestHandler requestHandler =
+ StsRequestHandler.newBuilder(
+ TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory())
+ .build();
+
+ transport.addResponseErrorSequence(
+ buildHttpResponseException(
+ INVALID_REQUEST, /* errorDescription= */ null, /* errorUri= */ null));
+
+ OAuthException e =
+ assertThrows(
+ OAuthException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ requestHandler.exchangeToken();
+ }
+ });
+
+ assertThat(e.getErrorCode()).isEqualTo(INVALID_REQUEST);
+ assertThat(e.getErrorDescription()).isNull();
+ assertThat(e.getErrorUri()).isNull();
+ }
+
+ @Test
+ public void exchangeToken_withOptionalParams_throwsException() throws IOException {
+ StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+
+ final StsRequestHandler requestHandler =
+ StsRequestHandler.newBuilder(
+ TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory())
+ .build();
+
+ transport.addResponseErrorSequence(
+ buildHttpResponseException(INVALID_REQUEST, ERROR_DESCRIPTION, ERROR_URI));
+
+ OAuthException e =
+ assertThrows(
+ OAuthException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ requestHandler.exchangeToken();
+ }
+ });
+
+ assertThat(e.getErrorCode()).isEqualTo(INVALID_REQUEST);
+ assertThat(e.getErrorDescription()).isEqualTo(ERROR_DESCRIPTION);
+ assertThat(e.getErrorUri()).isEqualTo(ERROR_URI);
+ }
+
+ @Test
+ public void exchangeToken_ioException() {
+ StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+
+ final StsRequestHandler requestHandler =
+ StsRequestHandler.newBuilder(
+ TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory())
+ .build();
+
+ IOException e = new IOException();
+ transport.addResponseErrorSequence(e);
+
+ IOException thrownException =
+ assertThrows(
+ IOException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ requestHandler.exchangeToken();
+ }
+ });
+ assertThat(thrownException).isEqualTo(e);
+ }
+
+ public HttpResponseException buildHttpResponseException(
+ String error, @Nullable String errorDescription, @Nullable String errorUri)
+ throws IOException {
+ GenericJson json = new GenericJson();
+ json.setFactory(OAuth2Utils.JSON_FACTORY);
+ json.set("error", error);
+ if (errorDescription != null) {
+ json.set("error_description", errorDescription);
+ }
+ if (errorUri != null) {
+ json.set("error_uri", errorUri);
+ }
+ return new HttpResponseException.Builder(
+ /* statusCode= */ 400, /* statusMessage= */ "statusMessage", new HttpHeaders())
+ .setContent(json.toPrettyString())
+ .build();
+ }
+}
diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml
index 026769290..0ceba932f 100644
--- a/oauth2_http/pom.xml
+++ b/oauth2_http/pom.xml
@@ -91,6 +91,12 @@
junit
test
+
+ com.google.truth
+ truth
+ 1.0.1
+ test
+
From ebb44867b25d48711148adb0b1677836ddf43c6f Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Wed, 26 Aug 2020 12:00:25 -0700
Subject: [PATCH 02/23] feat: adds support for 3PI credentials (#464)
* feat: adds base external account credentials class and support for file/url based external credentials
* fix: javadoc changes
* fix: address review comments
* fix: nits
* fix: fix broken test
* fix: format
---
.../oauth2/ExternalAccountCredentials.java | 449 ++++++++++++++++++
.../auth/oauth2/IdentityPoolCredentials.java | 257 ++++++++++
.../google/auth/oauth2/StsRequestHandler.java | 4 +-
.../javatests/com/google/auth/TestUtils.java | 39 +-
.../ExternalAccountCredentialsTest.java | 273 +++++++++++
.../oauth2/IdentityPoolCredentialsTest.java | 282 +++++++++++
...kExternalAccountCredentialsTransport.java} | 137 ++++--
.../auth/oauth2/StsRequestHandlerTest.java | 49 +-
8 files changed, 1411 insertions(+), 79 deletions(-)
create mode 100644 oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
rename oauth2_http/javatests/com/google/auth/oauth2/{MockStsServiceTransport.java => MockExternalAccountCredentialsTransport.java} (50%)
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
new file mode 100644
index 000000000..d04fff0cb
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.api.client.util.Preconditions.checkNotNull;
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.UrlEncodedContent;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.api.client.util.GenericData;
+import com.google.auth.http.AuthHttpConstants;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Base external account credentials class.
+ *
+ * Handles initializing 3PI credentials, calls to STS and service account impersonation.
+ */
+public abstract class ExternalAccountCredentials extends GoogleCredentials
+ implements QuotaProjectIdProvider {
+
+ /** Base credential source class. Dictates the retrieval method of the 3PI credential. */
+ abstract static class CredentialSource {
+
+ protected Map credentialSourceMap;
+
+ protected CredentialSource(Map credentialSourceMap) {
+ this.credentialSourceMap = checkNotNull(credentialSourceMap);
+ }
+ }
+
+ private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+ private static final String CLOUD_PLATFORM_SCOPE =
+ "https://www.googleapis.com/auth/cloud-platform";
+
+ static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account";
+
+ protected final String transportFactoryClassName;
+ protected final String audience;
+ protected final String subjectTokenType;
+ protected final String tokenUrl;
+ protected final String tokenInfoUrl;
+ protected final String serviceAccountImpersonationUrl;
+ protected final CredentialSource credentialSource;
+
+ @Nullable protected final Collection scopes;
+ @Nullable protected final String quotaProjectId;
+ @Nullable protected final String clientId;
+ @Nullable protected final String clientSecret;
+
+ protected transient HttpTransportFactory transportFactory;
+
+ /**
+ * Constructor with minimum identifying information and custom HTTP transport.
+ *
+ * @param transportFactory HTTP transport factory, creates the transport used to get access
+ * tokens.
+ * @param audience The STS audience which is usually the fully specified resource name of the
+ * workload/workforce pool provider.
+ * @param subjectTokenType The STS subject token type based on the OAuth 2.0 token exchange spec.
+ * Indicates the type of the security token in the credential file.
+ * @param tokenUrl The STS token exchange endpoint.
+ * @param tokenInfoUrl The endpoint used to retrieve account related information. Required for
+ * gCloud session account identification.
+ * @param credentialSource The 3PI credential source.
+ * @param serviceAccountImpersonationUrl The URL for the service account impersonation request.
+ * This is only required for workload identity pools when APIs to be accessed have not
+ * integrated with UberMint. If this is not available, the STS returned GCP access token is
+ * directly used. May be null.
+ * @param quotaProjectId The project used for quota and billing purposes. May be null.
+ * @param clientId Client ID of the service account from the console. May be null.
+ * @param clientSecret Client secret of the service account from the console. May be null.
+ * @param scopes The scopes to request during the authorization grant. May be null.
+ */
+ protected ExternalAccountCredentials(
+ HttpTransportFactory transportFactory,
+ String audience,
+ String subjectTokenType,
+ String tokenUrl,
+ String tokenInfoUrl,
+ CredentialSource credentialSource,
+ @Nullable String serviceAccountImpersonationUrl,
+ @Nullable String quotaProjectId,
+ @Nullable String clientId,
+ @Nullable String clientSecret,
+ @Nullable Collection scopes) {
+ this.transportFactory =
+ firstNonNull(
+ transportFactory,
+ getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
+ this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName());
+ this.audience = checkNotNull(audience);
+ this.subjectTokenType = checkNotNull(subjectTokenType);
+ this.tokenUrl = checkNotNull(tokenUrl);
+ this.tokenInfoUrl = checkNotNull(tokenInfoUrl);
+ this.credentialSource = checkNotNull(credentialSource);
+ this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
+ this.quotaProjectId = quotaProjectId;
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ this.scopes =
+ (scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
+ }
+
+ /**
+ * Returns credentials defined by a JSON file stream.
+ *
+ * This will either return {@link IdentityPoolCredentials} or AwsCredentials.
+ *
+ * @param credentialsStream the stream with the credential definition.
+ * @return the credential defined by the credentialsStream.
+ * @throws IOException if the credential cannot be created from the stream.
+ */
+ public static ExternalAccountCredentials fromStream(InputStream credentialsStream)
+ throws IOException {
+ return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ }
+
+ /**
+ * Returns credentials defined by a JSON file stream.
+ *
+ *
This will either return a IdentityPoolCredentials or AwsCredentials.
+ *
+ * @param credentialsStream the stream with the credential definition.
+ * @param transportFactory the HTTP transport factory used to create the transport to get access
+ * tokens.
+ * @return the credential defined by the credentialsStream.
+ * @throws IOException if the credential cannot be created from the stream.
+ */
+ public static ExternalAccountCredentials fromStream(
+ InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException {
+ checkNotNull(credentialsStream);
+ checkNotNull(transportFactory);
+
+ JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
+ GenericJson fileContents =
+ parser.parseAndClose(credentialsStream, OAuth2Utils.UTF_8, GenericJson.class);
+ return fromJson(fileContents, transportFactory);
+ }
+
+ /**
+ * Returns external account credentials defined by JSON using the format generated by gCloud.
+ *
+ * @param json a map from the JSON representing the credentials.
+ * @param transportFactory HTTP transport factory, creates the transport used to get access
+ * tokens.
+ * @return the credentials defined by the JSON.
+ */
+ public static ExternalAccountCredentials fromJson(
+ Map json, HttpTransportFactory transportFactory) {
+ checkNotNull(json);
+ checkNotNull(transportFactory);
+
+ String audience = (String) json.get("audience");
+ String subjectTokenType = (String) json.get("subject_token_type");
+ String tokenUrl = (String) json.get("token_url");
+ String tokenInfoUrl = (String) json.get("token_info_url");
+ String serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
+
+ Map credentialSourceMap = (Map) json.get("credential_source");
+
+ // Optional params.
+ String clientId = json.containsKey("client_id") ? (String) json.get("client_id") : null;
+ String clientSecret =
+ json.containsKey("client_secret") ? (String) json.get("client_secret") : null;
+ String quotaProjectId =
+ json.containsKey("quota_project_id") ? (String) json.get("quota_project_id") : null;
+
+ if (isAwsCredential(credentialSourceMap)) {
+ // TODO(lsirac) return AWS credential here.
+ }
+ return new IdentityPoolCredentials(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ new IdentityPoolCredentialSource(credentialSourceMap),
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ /* scopes= */ null);
+ }
+
+ private static boolean isAwsCredential(Map credentialSource) {
+ return credentialSource.containsKey("environment_id")
+ && ((String) credentialSource.get("environment_id")).startsWith("aws");
+ }
+
+ /**
+ * Exchanges the 3PI credential for a GCP access token.
+ *
+ * @param stsTokenExchangeRequest the STS token exchange request.
+ * @return the access token returned by STS.
+ * @throws OAuthException if the call to STS fails.
+ */
+ protected AccessToken exchange3PICredentialForAccessToken(
+ StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException {
+
+ StsRequestHandler requestHandler =
+ StsRequestHandler.newBuilder(
+ tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory())
+ .setHeaders(new HttpHeaders().setContentType("application/x-www-form-urlencoded"))
+ .build();
+
+ StsTokenExchangeResponse response = requestHandler.exchangeToken();
+ return response.getAccessToken();
+ }
+
+ /**
+ * Attempts service account impersonation.
+ *
+ * @param accessToken the access token to be included in the request.
+ * @return the access token returned by the generateAccessToken call.
+ * @throws IOException if the service account impersonation call fails.
+ */
+ protected AccessToken attemptServiceAccountImpersonation(AccessToken accessToken)
+ throws IOException {
+ if (serviceAccountImpersonationUrl == null) {
+ return accessToken;
+ }
+
+ HttpRequest request =
+ transportFactory
+ .create()
+ .createRequestFactory()
+ .buildPostRequest(
+ new GenericUrl(serviceAccountImpersonationUrl),
+ new UrlEncodedContent(new GenericData().set("scope", scopes.toArray())));
+ request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
+ request.setHeaders(
+ new HttpHeaders()
+ .setAuthorization(
+ String.format("%s %s", AuthHttpConstants.BEARER, accessToken.getTokenValue())));
+
+ HttpResponse response;
+ try {
+ response = request.execute();
+ } catch (IOException e) {
+ throw new IOException(
+ String.format("Error getting access token for service account: %s", e.getMessage()), e);
+ }
+
+ GenericData responseData = response.parseAs(GenericData.class);
+ String token =
+ OAuth2Utils.validateString(responseData, "accessToken", "Expected to find an accessToken");
+
+ DateFormat format = new SimpleDateFormat(RFC3339);
+ String expireTime =
+ OAuth2Utils.validateString(responseData, "expireTime", "Expected to find an expireTime");
+ try {
+ Date date = format.parse(expireTime);
+ return new AccessToken(token, date);
+ } catch (ParseException e) {
+ throw new IOException("Error parsing expireTime: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Retrieves the 3PI subject token to be exchanged for a GCP access token.
+ *
+ * Must be implemented by subclasses as the retrieval method is dependent on the credential
+ * source.
+ *
+ * @return the 3PI subject token
+ */
+ public abstract String retrieveSubjectToken() throws IOException;
+
+ public String getAudience() {
+ return audience;
+ }
+
+ public String getSubjectTokenType() {
+ return subjectTokenType;
+ }
+
+ public String getTokenUrl() {
+ return tokenUrl;
+ }
+
+ public String getTokenInfoUrl() {
+ return tokenInfoUrl;
+ }
+
+ public CredentialSource getCredentialSource() {
+ return credentialSource;
+ }
+
+ @Nullable
+ public String getServiceAccountImpersonationUrl() {
+ return serviceAccountImpersonationUrl;
+ }
+
+ @Override
+ @Nullable
+ public String getQuotaProjectId() {
+ return quotaProjectId;
+ }
+
+ @Nullable
+ public String getClientId() {
+ return clientId;
+ }
+
+ @Nullable
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ @Nullable
+ public Collection getScopes() {
+ return scopes;
+ }
+
+ /** Base builder for external account credentials. */
+ public abstract static class Builder extends GoogleCredentials.Builder {
+
+ protected String audience;
+ protected String subjectTokenType;
+ protected String tokenUrl;
+ protected String tokenInfoUrl;
+ protected CredentialSource credentialSource;
+ protected HttpTransportFactory transportFactory;
+
+ @Nullable protected String serviceAccountImpersonationUrl;
+ @Nullable protected String quotaProjectId;
+ @Nullable protected String clientId;
+ @Nullable protected String clientSecret;
+ @Nullable protected Collection scopes;
+
+ protected Builder() {}
+
+ protected Builder(ExternalAccountCredentials credentials) {
+ this.audience = credentials.audience;
+ this.subjectTokenType = credentials.subjectTokenType;
+ this.tokenUrl = credentials.tokenUrl;
+ this.tokenInfoUrl = credentials.tokenInfoUrl;
+ this.serviceAccountImpersonationUrl = credentials.serviceAccountImpersonationUrl;
+ this.credentialSource = credentials.credentialSource;
+ this.quotaProjectId = credentials.quotaProjectId;
+ this.clientId = credentials.clientId;
+ this.clientSecret = credentials.clientSecret;
+ this.scopes = credentials.scopes;
+ }
+
+ public Builder setAudience(String audience) {
+ this.audience = audience;
+ return this;
+ }
+
+ public Builder setSubjectTokenType(String subjectTokenType) {
+ this.subjectTokenType = subjectTokenType;
+ return this;
+ }
+
+ public Builder setTokenUrl(String tokenUrl) {
+ this.tokenUrl = tokenUrl;
+ return this;
+ }
+
+ public Builder setTokenInfoUrl(String tokenInfoUrl) {
+ this.tokenInfoUrl = tokenInfoUrl;
+ return this;
+ }
+
+ public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) {
+ this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
+ return this;
+ }
+
+ public Builder setCredentialSource(CredentialSource credentialSource) {
+ this.credentialSource = credentialSource;
+ return this;
+ }
+
+ public Builder setScopes(Collection scopes) {
+ this.scopes = scopes;
+ return this;
+ }
+
+ public Builder setQuotaProjectId(String quotaProjectId) {
+ this.quotaProjectId = quotaProjectId;
+ return this;
+ }
+
+ public Builder setClientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ public Builder setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
+ this.transportFactory = transportFactory;
+ return this;
+ }
+
+ public abstract ExternalAccountCredentials build();
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
new file mode 100644
index 000000000..a99ca04cd
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Url-sourced and file-sourced external account credentials.
+ *
+ * By default, attempts to exchange the 3PI credential for a GCP access token.
+ */
+public class IdentityPoolCredentials extends ExternalAccountCredentials {
+
+ /**
+ * The IdentityPool credential source. Dictates the retrieval method of the 3PI credential, which
+ * can either be through a metadata server or a local file.
+ */
+ @VisibleForTesting
+ static class IdentityPoolCredentialSource extends CredentialSource {
+
+ enum IdentityPoolCredentialSourceType {
+ FILE,
+ URL
+ }
+
+ private String credentialLocation;
+ private IdentityPoolCredentialSourceType credentialSourceType;
+
+ @Nullable private Map headers;
+
+ /**
+ * The source of the 3P credential.
+ *
+ * If the this a file based 3P credential, the credentials file can be retrieved using the
+ * `file` key.
+ *
+ *
If this is URL-based 3p credential, the metadata server URL can be retrieved using the
+ * `url` key.
+ *
+ *
Optional headers can be present, and should be keyed by `headers`.
+ */
+ public IdentityPoolCredentialSource(Map credentialSourceMap) {
+ super(credentialSourceMap);
+
+ if (credentialSourceMap.containsKey("file")) {
+ credentialLocation = (String) credentialSourceMap.get("file");
+ credentialSourceType = IdentityPoolCredentialSourceType.FILE;
+ } else {
+ credentialLocation = (String) credentialSourceMap.get("url");
+ credentialSourceType = IdentityPoolCredentialSourceType.URL;
+ }
+
+ Map headersMap = (Map) credentialSourceMap.get("headers");
+ if (headersMap != null && !headersMap.isEmpty()) {
+ headers = new HashMap<>();
+ headers.putAll(headersMap);
+ }
+ }
+
+ private boolean hasHeaders() {
+ return headers != null && !headers.isEmpty();
+ }
+ }
+
+ /**
+ * Internal constructor. See {@link
+ * ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
+ * String, String, CredentialSource, String, String, String, String, Collection)}
+ */
+ IdentityPoolCredentials(
+ HttpTransportFactory transportFactory,
+ String audience,
+ String subjectTokenType,
+ String tokenUrl,
+ String tokenInfoUrl,
+ IdentityPoolCredentialSource credentialSource,
+ @Nullable String serviceAccountImpersonationUrl,
+ @Nullable String quotaProjectId,
+ @Nullable String clientId,
+ @Nullable String clientSecret,
+ @Nullable Collection scopes) {
+ super(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ scopes);
+ }
+
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ String credential = retrieveSubjectToken();
+ StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(credential, subjectTokenType).setAudience(audience);
+
+ if (scopes != null && !scopes.isEmpty()) {
+ stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
+ }
+
+ AccessToken accessToken = exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build());
+ return attemptServiceAccountImpersonation(accessToken);
+ }
+
+ @Override
+ public String retrieveSubjectToken() throws IOException {
+ IdentityPoolCredentialSource identityPoolCredentialSource =
+ (IdentityPoolCredentialSource) credentialSource;
+ if (identityPoolCredentialSource.credentialSourceType
+ == IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) {
+ return retrieveSubjectTokenFromCredentialFile();
+ }
+ return getSubjectTokenFromMetadataServer();
+ }
+
+ private String retrieveSubjectTokenFromCredentialFile() throws IOException {
+ IdentityPoolCredentialSource identityPoolCredentialSource =
+ (IdentityPoolCredentialSource) credentialSource;
+ String credentialFilePath = identityPoolCredentialSource.credentialLocation;
+ if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) {
+ throw new IOException(
+ String.format(
+ "Invalid credential location. The file at %s does not exist.", credentialFilePath));
+ }
+ try {
+ return new String(Files.readAllBytes(Paths.get(credentialFilePath)));
+ } catch (IOException e) {
+ throw new IOException(
+ "Error when attempting to read the subject token from the credential file.", e);
+ }
+ }
+
+ private String getSubjectTokenFromMetadataServer() throws IOException {
+ IdentityPoolCredentialSource identityPoolCredentialSource =
+ (IdentityPoolCredentialSource) credentialSource;
+
+ HttpRequest request =
+ transportFactory
+ .create()
+ .createRequestFactory()
+ .buildGetRequest(new GenericUrl(identityPoolCredentialSource.credentialLocation));
+ request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
+
+ if (identityPoolCredentialSource.hasHeaders()) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.putAll(identityPoolCredentialSource.headers);
+ request.setHeaders(headers);
+ }
+
+ try {
+ HttpResponse response = request.execute();
+ return response.parseAsString();
+ } catch (IOException e) {
+ throw new IOException(
+ String.format("Error getting subject token from metadata server: %s", e.getMessage()), e);
+ }
+ }
+
+ /** Clones the IdentityPoolCredentials with the specified scopes. */
+ @Override
+ public GoogleCredentials createScoped(Collection newScopes) {
+ return new IdentityPoolCredentials(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ (IdentityPoolCredentialSource) credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ newScopes);
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static Builder newBuilder(IdentityPoolCredentials identityPoolCredentials) {
+ return new Builder(identityPoolCredentials);
+ }
+
+ public static class Builder extends ExternalAccountCredentials.Builder {
+
+ protected Builder() {}
+
+ protected Builder(ExternalAccountCredentials credentials) {
+ super(credentials);
+ }
+
+ @Override
+ public IdentityPoolCredentials build() {
+ return new IdentityPoolCredentials(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ (IdentityPoolCredentialSource) credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ scopes);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
index 9e2559e0a..bbc8ec0f7 100644
--- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
+++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
@@ -137,17 +137,15 @@ private GenericData buildTokenRequest() {
GenericData tokenRequest =
new GenericData()
.set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE)
- .set("scope", CLOUD_PLATFORM_SCOPE)
.set("subject_token_type", request.getSubjectTokenType())
.set("subject_token", request.getSubjectToken());
// Add scopes as a space-delimited string.
List scopes = new ArrayList<>();
- scopes.add(CLOUD_PLATFORM_SCOPE);
if (request.hasScopes()) {
scopes.addAll(request.getScopes());
+ tokenRequest.set("scope", Joiner.on(' ').join(scopes));
}
- tokenRequest.set("scope", Joiner.on(' ').join(scopes));
// Set the requested token type, which defaults to
// urn:ietf:params:oauth:token-type:access_token.
diff --git a/oauth2_http/javatests/com/google/auth/TestUtils.java b/oauth2_http/javatests/com/google/auth/TestUtils.java
index 0726fb1f6..12a488f3e 100644
--- a/oauth2_http/javatests/com/google/auth/TestUtils.java
+++ b/oauth2_http/javatests/com/google/auth/TestUtils.java
@@ -34,6 +34,8 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpResponseException;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
@@ -45,17 +47,24 @@
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
/** Utilities for test code under com.google.auth. */
public class TestUtils {
- public static final String UTF_8 = "UTF-8";
-
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
+ private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+ private static final int VALID_LIFETIME = 300;
+
+ public static final String UTF_8 = "UTF-8";
+
public static void assertContainsBearerToken(Map> metadata, String token) {
assertNotNull(metadata);
assertNotNull(token);
@@ -119,5 +128,31 @@ public static String errorJson(String message) throws IOException {
return errorResponse.toPrettyString();
}
+ public static HttpResponseException buildHttpResponseException(
+ String error, @Nullable String errorDescription, @Nullable String errorUri)
+ throws IOException {
+ GenericJson json = new GenericJson();
+ json.setFactory(JacksonFactory.getDefaultInstance());
+ json.set("error", error);
+ if (errorDescription != null) {
+ json.set("error_description", errorDescription);
+ }
+ if (errorUri != null) {
+ json.set("error_uri", errorUri);
+ }
+ return new HttpResponseException.Builder(
+ /* statusCode= */ 400, /* statusMessage= */ "statusMessage", new HttpHeaders())
+ .setContent(json.toPrettyString())
+ .build();
+ }
+
+ public static String getDefaultExpireTime() {
+ Date currentDate = new Date();
+ Calendar c = Calendar.getInstance();
+ c.setTime(currentDate);
+ c.add(Calendar.SECOND, VALID_LIFETIME);
+ return new SimpleDateFormat(RFC3339).format(c.getTime());
+ }
+
private TestUtils() {}
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
new file mode 100644
index 000000000..fb3bc9454
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2020, Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.auth.TestUtils.getDefaultExpireTime;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.util.GenericData;
+import com.google.auth.TestUtils;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ExternalAccountCredentials}. */
+@RunWith(JUnit4.class)
+public class ExternalAccountCredentialsTest {
+
+ private static final String AUDIENCE = "audience";
+ private static final String SUBJECT_TOKEN_TYPE = "subjectTokenType";
+ private static final String TOKEN_INFO_URL = "tokenInfoUrl";
+ private static final String STS_URL = "https://www.sts.google.com";
+ private static final String CREDENTIAL = "credential";
+ private static final String ACCESS_TOKEN = "eya23tfgdfga2123as";
+ private static final String CLOUD_PLATFORM_SCOPE =
+ "https://www.googleapis.com/auth/cloud-platform";
+
+ static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
+
+ MockExternalAccountCredentialsTransport transport =
+ new MockExternalAccountCredentialsTransport();
+
+ private static final Map FILE_CREDENTIAL_SOURCE_MAP =
+ new HashMap() {
+ {
+ put("file", "file");
+ }
+ };
+
+ private static final IdentityPoolCredentialSource FILE_CREDENTIAL_SOURCE =
+ new IdentityPoolCredentialSource(FILE_CREDENTIAL_SOURCE_MAP);
+
+ @Override
+ public HttpTransport create() {
+ return transport;
+ }
+ }
+
+ private MockExternalAccountCredentialsTransportFactory transportFactory;
+
+ @Before
+ public void setup() {
+ transportFactory = new MockExternalAccountCredentialsTransportFactory();
+ }
+
+ @Test
+ public void fromStream_identityPoolCredentials() throws IOException {
+ GenericJson json = buildDefaultIdentityPoolCredential();
+ TestUtils.jsonToInputStream(json);
+
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
+
+ assertThat(credential).isInstanceOf(IdentityPoolCredentials.class);
+ }
+
+ @Test
+ public void fromStream_nullTransport_throws() {
+ assertThrows(
+ NullPointerException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ ExternalAccountCredentials.fromStream(
+ new ByteArrayInputStream("foo".getBytes()), /* transportFactory= */ null);
+ }
+ });
+ }
+
+ @Test
+ public void fromStream_nullStream_throws() {
+ assertThrows(
+ NullPointerException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ ExternalAccountCredentials.fromStream(
+ /* credentialsStream= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ }
+ });
+ }
+
+ @Test
+ public void fromJson_identityPoolCredentials() {
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(
+ buildDefaultIdentityPoolCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ assertThat(credential).isInstanceOf(IdentityPoolCredentials.class);
+
+ assertThat(credential.getAudience()).isEqualTo(AUDIENCE);
+ assertThat(credential.getSubjectTokenType()).isEqualTo(SUBJECT_TOKEN_TYPE);
+ assertThat(credential.getTokenUrl()).isEqualTo(STS_URL);
+ assertThat(credential.getTokenInfoUrl()).isEqualTo(TOKEN_INFO_URL);
+ assertThat(credential.getCredentialSource()).isNotNull();
+ }
+
+ @Test
+ public void fromJson_nullJson_throws() {
+ assertThrows(
+ NullPointerException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() {
+ ExternalAccountCredentials.fromJson(
+ /* json= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ }
+ });
+ }
+
+ @Test
+ public void fromJson_nullTransport_throws() {
+ assertThrows(
+ NullPointerException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() {
+ ExternalAccountCredentials.fromJson(
+ new HashMap(), /* transportFactory= */ null);
+ }
+ });
+ }
+
+ @Test
+ public void exchange3PICredentialForAccessToken() throws IOException {
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(buildDefaultIdentityPoolCredential(), transportFactory);
+
+ StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+
+ AccessToken accessToken =
+ credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest);
+
+ assertThat(accessToken.getTokenValue()).isEqualTo(transportFactory.transport.getAccessToken());
+
+ Map> headers = transportFactory.transport.getRequest().getHeaders();
+
+ assertThat(headers.containsKey("content-type")).isTrue();
+ assertThat(headers.get("content-type").get(0)).isEqualTo("application/x-www-form-urlencoded");
+ }
+
+ @Test
+ public void exchange3PICredentialForAccessToken_throws() throws IOException {
+ final ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(buildDefaultIdentityPoolCredential(), transportFactory);
+
+ String errorCode = "invalidRequest";
+ String errorDescription = "errorDescription";
+ String errorUri = "errorUri";
+ transportFactory.transport.addResponseErrorSequence(
+ TestUtils.buildHttpResponseException(errorCode, errorDescription, errorUri));
+
+ final StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+
+ OAuthException e =
+ assertThrows(
+ OAuthException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest);
+ }
+ });
+
+ assertThat(e.getErrorCode()).isEqualTo(errorCode);
+ assertThat(e.getErrorDescription()).isEqualTo(errorDescription);
+ assertThat(e.getErrorUri()).isEqualTo(errorUri);
+ }
+
+ @Test
+ public void attemptServiceAccountImpersonation() throws IOException {
+ GenericJson defaultCredential = buildDefaultIdentityPoolCredential();
+ defaultCredential.put(
+ "service_account_impersonation_url",
+ transportFactory.transport.getServiceAccountImpersonationUrl());
+
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(defaultCredential, transportFactory);
+
+ transportFactory.transport.setExpireTime(getDefaultExpireTime());
+ AccessToken accessToken = new AccessToken(ACCESS_TOKEN, new Date());
+
+ AccessToken returnedToken = credential.attemptServiceAccountImpersonation(accessToken);
+
+ assertThat(returnedToken.getTokenValue())
+ .isEqualTo(transportFactory.transport.getAccessToken());
+ assertThat(returnedToken.getTokenValue()).isNotEqualTo(accessToken.getTokenValue());
+
+ // Validate request content.
+ MockLowLevelHttpRequest request = transportFactory.transport.getRequest();
+ Map actualRequestContent = TestUtils.parseQuery(request.getContentAsString());
+
+ GenericData expectedRequestContent = new GenericData().set("scope", CLOUD_PLATFORM_SCOPE);
+ assertThat(actualRequestContent).isEqualTo(expectedRequestContent);
+ }
+
+ @Test
+ public void attemptServiceAccountImpersonation_noUrl() throws IOException {
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(buildDefaultIdentityPoolCredential(), transportFactory);
+
+ AccessToken accessToken = new AccessToken(ACCESS_TOKEN, new Date());
+ AccessToken returnedToken = credential.attemptServiceAccountImpersonation(accessToken);
+
+ assertThat(returnedToken).isEqualTo(accessToken);
+ }
+
+ private GenericJson buildDefaultIdentityPoolCredential() {
+ GenericJson json = new GenericJson();
+ json.put("audience", AUDIENCE);
+ json.put("subject_token_type", SUBJECT_TOKEN_TYPE);
+ json.put("token_url", STS_URL);
+ json.put("token_info_url", TOKEN_INFO_URL);
+
+ Map map = new HashMap<>();
+ map.put("file", "file");
+ json.put("credential_source", map);
+ return json;
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
new file mode 100644
index 000000000..86996368c
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2020, Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.auth.TestUtils.getDefaultExpireTime;
+import static com.google.auth.oauth2.OAuth2Utils.UTF_8;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link IdentityPoolCredentials}. */
+@RunWith(JUnit4.class)
+public class IdentityPoolCredentialsTest {
+
+ private static final String AUDIENCE = "audience";
+ private static final String SUBJECT_TOKEN_TYPE = "subjectTokenType";
+ private static final String TOKEN_URL = "tokenUrl";
+ private static final String TOKEN_INFO_URL = "tokenInfoUrl";
+ private static final String SERVICE_ACCOUNT_IMPERSONATION_URL = "tokenInfoUrl";
+ private static final String QUOTA_PROJECT_ID = "quotaProjectId";
+ private static final String CLIENT_ID = "clientId";
+ private static final String CLIENT_SECRET = "clientSecret";
+
+ private static final String FILE = "file";
+ private static final String URL = "url";
+ private static final String HEADERS = "headers";
+
+ private static final Map FILE_CREDENTIAL_SOURCE_MAP =
+ new HashMap() {
+ {
+ put(FILE, FILE);
+ }
+ };
+
+ private static final IdentityPoolCredentialSource FILE_CREDENTIAL_SOURCE =
+ new IdentityPoolCredentialSource(FILE_CREDENTIAL_SOURCE_MAP);
+
+ private static final IdentityPoolCredentials FILE_SOURCED_CREDENTIAL =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience(AUDIENCE)
+ .setSubjectTokenType(SUBJECT_TOKEN_TYPE)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .build();
+
+ static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
+
+ MockExternalAccountCredentialsTransport transport =
+ new MockExternalAccountCredentialsTransport();
+
+ @Override
+ public HttpTransport create() {
+ return transport;
+ }
+ }
+
+ @Test
+ public void createdScoped_clonedCredentialWithAddedScopes() {
+ GoogleCredentials credentials =
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
+ .setQuotaProjectId(QUOTA_PROJECT_ID)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .build();
+
+ List newScopes = Arrays.asList("scope1", "scope2");
+
+ IdentityPoolCredentials newCredentials =
+ (IdentityPoolCredentials) credentials.createScoped(newScopes);
+
+ assertThat(newCredentials.getAudience()).isEqualTo(AUDIENCE);
+ assertThat(newCredentials.getSubjectTokenType()).isEqualTo(SUBJECT_TOKEN_TYPE);
+ assertThat(newCredentials.getTokenUrl()).isEqualTo(TOKEN_URL);
+ assertThat(newCredentials.getTokenInfoUrl()).isEqualTo(TOKEN_INFO_URL);
+ assertThat(newCredentials.getServiceAccountImpersonationUrl())
+ .isEqualTo(SERVICE_ACCOUNT_IMPERSONATION_URL);
+ assertThat(newCredentials.getCredentialSource()).isEqualTo(FILE_CREDENTIAL_SOURCE);
+ assertThat(newCredentials.getScopes()).isEqualTo(newScopes);
+ assertThat(newCredentials.getQuotaProjectId()).isEqualTo(QUOTA_PROJECT_ID);
+ assertThat(newCredentials.getClientId()).isEqualTo(CLIENT_ID);
+ assertThat(newCredentials.getClientSecret()).isEqualTo(CLIENT_SECRET);
+ }
+
+ @Test
+ public void retrieveSubjectToken_fileSourced() throws IOException {
+ File file =
+ File.createTempFile("RETRIEVE_SUBJECT_TOKEN", /* suffix= */ null, /* directory= */ null);
+ file.deleteOnExit();
+
+ String credential = "credential";
+ OAuth2Utils.writeInputStreamToFile(
+ new ByteArrayInputStream(credential.getBytes(UTF_8)), file.getAbsolutePath());
+
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put(FILE, file.getAbsolutePath());
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+
+ IdentityPoolCredentials credentials =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setCredentialSource(credentialSource)
+ .build();
+
+ String subjectToken = credentials.retrieveSubjectToken();
+ assertThat(subjectToken).isEqualTo(credential);
+ }
+
+ @Test
+ public void retrieveSubjectToken_noFile_throws() {
+ Map credentialSourceMap = new HashMap<>();
+ String path = "badPath";
+ credentialSourceMap.put(FILE, path);
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+
+ final IdentityPoolCredentials credentials =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setCredentialSource(credentialSource)
+ .build();
+
+ IOException e =
+ assertThrows(
+ IOException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ credentials.retrieveSubjectToken();
+ }
+ });
+
+ assertThat(e.getMessage())
+ .isEqualTo(
+ String.format("Invalid credential location. The file at %s does not exist.", path));
+ }
+
+ @Test
+ public void retrieveSubjectToken_urlSourced() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ IdentityPoolCredentials credential =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(
+ buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
+ .build();
+
+ String subjectToken = credential.retrieveSubjectToken();
+ assertThat(subjectToken).isEqualTo(transportFactory.transport.getSubjectToken());
+ }
+
+ @Test
+ public void retrieveSubjectToken_urlSourcedCredential_throws() {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ IOException response = new IOException();
+ transportFactory.transport.addResponseErrorSequence(response);
+
+ final IdentityPoolCredentials credential =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(
+ buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
+ .build();
+
+ IOException e =
+ assertThrows(
+ IOException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ credential.retrieveSubjectToken();
+ }
+ });
+
+ assertThat(e.getMessage())
+ .isEqualTo(
+ String.format(
+ "Error getting subject token from metadata server: %s", response.getMessage()));
+ }
+
+ @Test
+ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ final IdentityPoolCredentials credential =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setTokenUrl(transportFactory.transport.getStsUrl())
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(
+ buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
+ .build();
+
+ AccessToken accessToken = credential.refreshAccessToken();
+ assertThat(accessToken.getTokenValue()).isEqualTo(transportFactory.transport.getAccessToken());
+ }
+
+ @Test
+ public void refreshAccessToken_withServiceAccountImpersonation() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ transportFactory.transport.setExpireTime(getDefaultExpireTime());
+ final IdentityPoolCredentials credential =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setTokenUrl(transportFactory.transport.getStsUrl())
+ .setServiceAccountImpersonationUrl(
+ transportFactory.transport.getServiceAccountImpersonationUrl())
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(
+ buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
+ .build();
+
+ AccessToken accessToken = credential.refreshAccessToken();
+ assertThat(accessToken.getTokenValue()).isEqualTo(transportFactory.transport.getAccessToken());
+ }
+
+ private IdentityPoolCredentialSource buildUrlBasedCredentialSource(String url) {
+ Map credentialSourceMap = new HashMap<>();
+ Map headers = new HashMap<>();
+ headers.put("Metadata-Flavor", "Google");
+ credentialSourceMap.put(URL, url);
+ credentialSourceMap.put(HEADERS, headers);
+
+ return new IdentityPoolCredentialSource(credentialSourceMap);
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockStsServiceTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
similarity index 50%
rename from oauth2_http/javatests/com/google/auth/oauth2/MockStsServiceTransport.java
rename to oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index 8c8e4fcfd..d977aeee8 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockStsServiceTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -51,10 +51,23 @@
import java.util.Map;
import java.util.Queue;
-/** Mock transport that simulates STS. */
-public class MockStsServiceTransport extends MockHttpTransport {
+/**
+ * Mock transport that handles the necessary steps to exchange a 3PI credential for a GCP
+ * access-token.
+ */
+public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
+
private static final String EXPECTED_GRANT_TYPE =
"urn:ietf:params:oauth:grant-type:token-exchange";
+ private static final String CLOUD_PLATFORM_SCOPE =
+ "https://www.googleapis.com/auth/cloud-platform";
+
+ private static final String METADATA_SERVER_URL = "https://www.metadata.google.com";
+ private static final String STS_URL = "https://www.sts.google.com";
+ private static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
+ "https://iamcredentials.googleapis.com";
+
+ private static final String SUBJECT_TOKEN = "subjectToken";
private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
private static final String TOKEN_TYPE = "Bearer";
private static final String ACCESS_TOKEN = "accessToken";
@@ -66,6 +79,7 @@ public class MockStsServiceTransport extends MockHttpTransport {
private Queue refreshTokenSequence = new ArrayDeque<>();
private Queue> scopeSequence = new ArrayDeque<>();
private MockLowLevelHttpRequest request;
+ private String expireTime;
public void addResponseErrorSequence(IOException... errors) {
Collections.addAll(responseErrorSequence, errors);
@@ -79,6 +93,74 @@ public void addScopeSequence(List... scopes) {
Collections.addAll(scopeSequence, scopes);
}
+ @Override
+ public LowLevelHttpRequest buildRequest(final String method, final String url) {
+ this.request =
+ new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ if (METADATA_SERVER_URL.equals(url)) {
+ if (!responseErrorSequence.isEmpty()) {
+ throw responseErrorSequence.poll();
+ }
+
+ String metadataRequestHeader = getFirstHeaderValue("Metadata-Flavor");
+ if (!"Google".equals(metadataRequestHeader)) {
+ throw new IOException("Metadata request header not found.");
+ }
+ return new MockLowLevelHttpResponse()
+ .setContentType("text/html")
+ .setContent(SUBJECT_TOKEN);
+ }
+ if (STS_URL.equals(url)) {
+ if (!responseErrorSequence.isEmpty()) {
+ throw responseErrorSequence.poll();
+ }
+ Map query = TestUtils.parseQuery(getContentAsString());
+ assertThat(query.get("grant_type")).isEqualTo(EXPECTED_GRANT_TYPE);
+ assertThat(query.get("subject_token_type")).isNotEmpty();
+ assertThat(query.get("subject_token")).isNotEmpty();
+
+ GenericJson response = new GenericJson();
+ response.setFactory(JSON_FACTORY);
+ response.put("token_type", TOKEN_TYPE);
+ response.put("expires_in", EXPIRES_IN);
+ response.put("access_token", ACCESS_TOKEN);
+ response.put("issued_token_type", ISSUED_TOKEN_TYPE);
+
+ if (!refreshTokenSequence.isEmpty()) {
+ response.put("refresh_token", refreshTokenSequence.poll());
+ }
+ if (!scopeSequence.isEmpty()) {
+ response.put("scope", Joiner.on(' ').join(scopeSequence.poll()));
+ }
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(response.toPrettyString());
+ }
+ if (SERVICE_ACCOUNT_IMPERSONATION_URL.equals(url)) {
+ Map query = TestUtils.parseQuery(getContentAsString());
+ assertThat(query.get("scope")).isEqualTo(CLOUD_PLATFORM_SCOPE);
+ assertThat(getHeaders().containsKey("authorization")).isTrue();
+ assertThat(getHeaders().get("authorization")).hasSize(1);
+ assertThat(getHeaders().get("authorization")).hasSize(1);
+ assertThat(getHeaders().get("authorization").get(0)).isNotEmpty();
+
+ GenericJson response = new GenericJson();
+ response.setFactory(JSON_FACTORY);
+ response.put("accessToken", ACCESS_TOKEN);
+ response.put("expireTime", expireTime);
+
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(response.toPrettyString());
+ }
+ return null;
+ }
+ };
+ return this.request;
+ }
+
public MockLowLevelHttpRequest getRequest() {
return request;
}
@@ -99,38 +181,23 @@ public int getExpiresIn() {
return EXPIRES_IN;
}
- @Override
- public LowLevelHttpRequest buildRequest(String method, String url) {
- this.request =
- new MockLowLevelHttpRequest(url) {
- @Override
- public LowLevelHttpResponse execute() throws IOException {
- if (!responseErrorSequence.isEmpty()) {
- throw responseErrorSequence.poll();
- }
- Map query = TestUtils.parseQuery(getContentAsString());
- assertThat(query.get("grant_type")).isEqualTo(EXPECTED_GRANT_TYPE);
- assertThat(query.get("subject_token_type")).isNotEmpty();
- assertThat(query.get("subject_token")).isNotEmpty();
-
- GenericJson response = new GenericJson();
- response.setFactory(JSON_FACTORY);
- response.put("token_type", TOKEN_TYPE);
- response.put("expires_in", EXPIRES_IN);
- response.put("access_token", ACCESS_TOKEN);
- response.put("issued_token_type", ISSUED_TOKEN_TYPE);
-
- if (!refreshTokenSequence.isEmpty()) {
- response.put("refresh_token", refreshTokenSequence.poll());
- }
- if (!scopeSequence.isEmpty()) {
- response.put("scope", Joiner.on(' ').join(scopeSequence.poll()));
- }
- return new MockLowLevelHttpResponse()
- .setContentType(Json.MEDIA_TYPE)
- .setContent(response.toPrettyString());
- }
- };
- return this.request;
+ public String getSubjectToken() {
+ return SUBJECT_TOKEN;
+ }
+
+ public String getMetadataUrl() {
+ return METADATA_SERVER_URL;
+ }
+
+ public String getStsUrl() {
+ return STS_URL;
+ }
+
+ public String getServiceAccountImpersonationUrl() {
+ return SERVICE_ACCOUNT_IMPERSONATION_URL;
+ }
+
+ public void setExpireTime(String expireTime) {
+ this.expireTime = expireTime;
}
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
index 7f1be7ab6..5de1d182c 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
@@ -35,19 +35,15 @@
import static org.junit.Assert.assertThrows;
import com.google.api.client.http.HttpHeaders;
-import com.google.api.client.http.HttpResponseException;
-import com.google.api.client.json.GenericJson;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Joiner;
import com.google.auth.TestUtils;
import com.google.auth.oauth2.StsTokenExchangeRequest.ActingParty;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
-import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
@@ -90,17 +86,19 @@ public final class StsRequestHandlerTest {
private static final String ERROR_DESCRIPTION = "errorDescription";
private static final String ERROR_URI = "errorUri";
- private MockStsServiceTransport transport;
+ private MockExternalAccountCredentialsTransport transport;
@Before
public void setup() {
- transport = new MockStsServiceTransport();
+ transport = new MockExternalAccountCredentialsTransport();
}
@Test
public void exchangeToken() throws IOException {
StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+ StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE)
+ .setScopes(Arrays.asList(CLOUD_PLATFORM_SCOPE))
+ .build();
StsRequestHandler requestHandler =
StsRequestHandler.newBuilder(
@@ -132,11 +130,7 @@ public void exchangeToken() throws IOException {
@Test
public void exchangeToken_withOptionalParams() throws IOException {
// Return optional params scope and the refresh_token.
- List scopesToReturn = new ArrayList<>();
- scopesToReturn.add(CLOUD_PLATFORM_SCOPE);
- scopesToReturn.addAll(SCOPES);
-
- transport.addScopeSequence(scopesToReturn);
+ transport.addScopeSequence(SCOPES);
transport.addRefreshTokenSequence(REFRESH_TOKEN);
// Build the token exchange request.
@@ -165,16 +159,11 @@ public void exchangeToken_withOptionalParams() throws IOException {
StsTokenExchangeResponse response = requestHandler.exchangeToken();
// Validate response.
- List expectedScopes = new ArrayList<>();
- expectedScopes.add(CLOUD_PLATFORM_SCOPE);
- expectedScopes.addAll(SCOPES);
- String spaceDelimitedScopes = Joiner.on(' ').join(expectedScopes);
-
assertThat(response.getAccessToken().getTokenValue()).isEqualTo(transport.getAccessToken());
assertThat(response.getTokenType()).isEqualTo(transport.getTokenType());
assertThat(response.getIssuedTokenType()).isEqualTo(transport.getIssuedTokenType());
assertThat(response.getExpiresIn()).isEqualTo(transport.getExpiresIn());
- assertThat(response.getScopes()).isEqualTo(scopesToReturn);
+ assertThat(response.getScopes()).isEqualTo(SCOPES);
assertThat(response.getRefreshToken()).isEqualTo(REFRESH_TOKEN);
// Validate headers.
@@ -188,7 +177,7 @@ public void exchangeToken_withOptionalParams() throws IOException {
GenericData expectedRequestContent =
new GenericData()
.set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE)
- .set("scope", spaceDelimitedScopes)
+ .set("scope", Joiner.on(' ').join(SCOPES))
.set("options", INTERNAL_OPTIONS)
.set("subject_token_type", stsTokenExchangeRequest.getSubjectTokenType())
.set("subject_token", stsTokenExchangeRequest.getSubjectToken())
@@ -213,7 +202,7 @@ public void exchangeToken_throwsException() throws IOException {
.build();
transport.addResponseErrorSequence(
- buildHttpResponseException(
+ TestUtils.buildHttpResponseException(
INVALID_REQUEST, /* errorDescription= */ null, /* errorUri= */ null));
OAuthException e =
@@ -242,7 +231,7 @@ public void exchangeToken_withOptionalParams_throwsException() throws IOExceptio
.build();
transport.addResponseErrorSequence(
- buildHttpResponseException(INVALID_REQUEST, ERROR_DESCRIPTION, ERROR_URI));
+ TestUtils.buildHttpResponseException(INVALID_REQUEST, ERROR_DESCRIPTION, ERROR_URI));
OAuthException e =
assertThrows(
@@ -283,22 +272,4 @@ public void run() throws Throwable {
});
assertThat(thrownException).isEqualTo(e);
}
-
- public HttpResponseException buildHttpResponseException(
- String error, @Nullable String errorDescription, @Nullable String errorUri)
- throws IOException {
- GenericJson json = new GenericJson();
- json.setFactory(OAuth2Utils.JSON_FACTORY);
- json.set("error", error);
- if (errorDescription != null) {
- json.set("error_description", errorDescription);
- }
- if (errorUri != null) {
- json.set("error_uri", errorUri);
- }
- return new HttpResponseException.Builder(
- /* statusCode= */ 400, /* statusMessage= */ "statusMessage", new HttpHeaders())
- .setContent(json.toPrettyString())
- .build();
- }
}
From 4ee1a14368b96f7d02491bd46b1cd3f848b8f927 Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Wed, 30 Sep 2020 13:48:42 -0700
Subject: [PATCH 03/23] feat: implements AWS signature version 4 for signing
requests (#476)
* feat: implements AWS signature version 4 for signing requests
* fix: fix javadoc
* fix: address review comments
* fix: changes to visibility and addresses other review comments
* fix: removes sortedHeaderNames from AwsRequestSignature, uses MessageDigest, and misc changes
* feat: generate authorization header in AwsRequestSigner
* fix: address more review comments
* fix: use RuntimeExceptions instead of invalid state/argument
* fix: javadoc
* fix: handle invalid input in Builder & misc fixes
* fix: get dates at construction and no longer catch ParseException in AwsDates
* fix: refactor AwsDates
---
.../auth/oauth2/AwsRequestSignature.java | 191 ++++++
.../google/auth/oauth2/AwsRequestSigner.java | 399 +++++++++++++
.../auth/oauth2/AwsSecurityCredentials.java | 65 +++
.../auth/oauth2/AwsRequestSignerTest.java | 543 ++++++++++++++++++
.../aws_security_credentials.json | 9 +
5 files changed, 1207 insertions(+)
create mode 100644 oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java
create mode 100644 oauth2_http/testresources/aws_security_credentials.json
diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java
new file mode 100644
index 000000000..af90a7603
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Stores the AWS API request signature based on the AWS Signature Version 4 signing process, and
+ * the parameters used in the signing process.
+ */
+class AwsRequestSignature {
+
+ private AwsSecurityCredentials awsSecurityCredentials;
+ private Map canonicalHeaders;
+
+ private String signature;
+ private String credentialScope;
+ private String url;
+ private String httpMethod;
+ private String date;
+ private String region;
+ private String authorizationHeader;
+
+ private AwsRequestSignature(
+ AwsSecurityCredentials awsSecurityCredentials,
+ Map canonicalHeaders,
+ String signature,
+ String credentialScope,
+ String url,
+ String httpMethod,
+ String date,
+ String region,
+ String authorizationHeader) {
+ this.awsSecurityCredentials = awsSecurityCredentials;
+ this.canonicalHeaders = canonicalHeaders;
+ this.signature = signature;
+ this.credentialScope = credentialScope;
+ this.url = url;
+ this.httpMethod = httpMethod;
+ this.date = date;
+ this.region = region;
+ this.authorizationHeader = authorizationHeader;
+ }
+
+ /** Returns the request signature based on the AWS Signature Version 4 signing process. */
+ String getSignature() {
+ return signature;
+ }
+
+ /** Returns the credential scope. e.g. 20150830/us-east-1/iam/aws4_request */
+ String getCredentialScope() {
+ return credentialScope;
+ }
+
+ /** Returns the AWS security credentials. */
+ AwsSecurityCredentials getSecurityCredentials() {
+ return awsSecurityCredentials;
+ }
+
+ /** Returns the request URL. */
+ String getUrl() {
+ return url;
+ }
+
+ /** Returns the HTTP request method. */
+ String getHttpMethod() {
+ return httpMethod;
+ }
+
+ /** Returns the HTTP request canonical headers. */
+ Map getCanonicalHeaders() {
+ return new HashMap<>(canonicalHeaders);
+ }
+
+ /** Returns the request date. */
+ String getDate() {
+ return date;
+ }
+
+ /** Returns the targeted region. */
+ String getRegion() {
+ return region;
+ }
+
+ /** Returns the authorization header. */
+ String getAuthorizationHeader() {
+ return authorizationHeader;
+ }
+
+ static class Builder {
+
+ private AwsSecurityCredentials awsSecurityCredentials;
+ private Map canonicalHeaders;
+
+ private String signature;
+ private String credentialScope;
+ private String url;
+ private String httpMethod;
+ private String date;
+ private String region;
+ private String authorizationHeader;
+
+ Builder setSignature(String signature) {
+ this.signature = signature;
+ return this;
+ }
+
+ Builder setCredentialScope(String credentialScope) {
+ this.credentialScope = credentialScope;
+ return this;
+ }
+
+ Builder setSecurityCredentials(AwsSecurityCredentials awsSecurityCredentials) {
+ this.awsSecurityCredentials = awsSecurityCredentials;
+ return this;
+ }
+
+ Builder setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ Builder setHttpMethod(String httpMethod) {
+ this.httpMethod = httpMethod;
+ return this;
+ }
+
+ Builder setCanonicalHeaders(Map canonicalHeaders) {
+ this.canonicalHeaders = new HashMap<>(canonicalHeaders);
+ return this;
+ }
+
+ Builder setDate(String date) {
+ this.date = date;
+ return this;
+ }
+
+ Builder setRegion(String region) {
+ this.region = region;
+ return this;
+ }
+
+ Builder setAuthorizationHeader(String authorizationHeader) {
+ this.authorizationHeader = authorizationHeader;
+ return this;
+ }
+
+ AwsRequestSignature build() {
+ return new AwsRequestSignature(
+ awsSecurityCredentials,
+ canonicalHeaders,
+ signature,
+ credentialScope,
+ url,
+ httpMethod,
+ date,
+ region,
+ authorizationHeader);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java
new file mode 100644
index 000000000..d49f7b908
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.api.client.util.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.api.client.util.Joiner;
+import com.google.auth.ServiceAccountSigner.SigningException;
+import com.google.common.base.Splitter;
+import com.google.common.io.BaseEncoding;
+import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import javax.annotation.Nullable;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Internal utility that signs AWS API requests based on the AWS Signature Version 4 signing
+ * process.
+ *
+ * https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+ */
+class AwsRequestSigner {
+
+ // AWS Signature Version 4 signing algorithm identifier.
+ private static final String HASHING_ALGORITHM = "AWS4-HMAC-SHA256";
+
+ // The termination string for the AWS credential scope value as defined in
+ // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+ private static final String AWS_REQUEST_TYPE = "aws4_request";
+
+ private final AwsSecurityCredentials awsSecurityCredentials;
+ private final Map additionalHeaders;
+ private final String httpMethod;
+ private final String region;
+ private final String requestPayload;
+ private final URI uri;
+ private final AwsDates dates;
+
+ /**
+ * Internal constructor.
+ *
+ * @param awsSecurityCredentials AWS security credentials
+ * @param httpMethod the HTTP request method
+ * @param url the request URL
+ * @param region the targeted region
+ * @param requestPayload the request payload
+ * @param additionalHeaders a map of additional HTTP headers to be included with the signed
+ * request.
+ */
+ private AwsRequestSigner(
+ AwsSecurityCredentials awsSecurityCredentials,
+ String httpMethod,
+ String url,
+ String region,
+ @Nullable String requestPayload,
+ @Nullable Map additionalHeaders,
+ @Nullable AwsDates awsDates) {
+ this.awsSecurityCredentials = checkNotNull(awsSecurityCredentials);
+ this.httpMethod = checkNotNull(httpMethod);
+ this.uri = URI.create(url).normalize();
+ this.region = checkNotNull(region);
+ this.requestPayload = requestPayload == null ? "" : requestPayload;
+ this.additionalHeaders =
+ (additionalHeaders != null)
+ ? new HashMap<>(additionalHeaders)
+ : new HashMap();
+ this.dates = awsDates == null ? AwsDates.generateXAmzDate() : awsDates;
+ }
+
+ /**
+ * Signs the specified AWS API request.
+ *
+ * @return the {@link AwsRequestSignature}
+ */
+ AwsRequestSignature sign() {
+ // Retrieve the service name. For example: iam.amazonaws.com host => iam service.
+ String serviceName = Splitter.on(".").split(uri.getHost()).iterator().next();
+
+ Map canonicalHeaders = getCanonicalHeaders(dates.getOriginalDate());
+ // Headers must be sorted.
+ List sortedHeaderNames = new ArrayList<>();
+ for (String headerName : canonicalHeaders.keySet()) {
+ sortedHeaderNames.add(headerName.toLowerCase(Locale.US));
+ }
+ Collections.sort(sortedHeaderNames);
+
+ String canonicalRequestHash = createCanonicalRequestHash(canonicalHeaders, sortedHeaderNames);
+ String credentialScope =
+ dates.getFormattedDate() + "/" + region + "/" + serviceName + "/" + AWS_REQUEST_TYPE;
+ String stringToSign =
+ createStringToSign(canonicalRequestHash, dates.getXAmzDate(), credentialScope);
+ String signature =
+ calculateAwsV4Signature(
+ serviceName,
+ awsSecurityCredentials.getSecretAccessKey(),
+ dates.getFormattedDate(),
+ region,
+ stringToSign);
+
+ String authorizationHeader =
+ generateAuthorizationHeader(
+ sortedHeaderNames, awsSecurityCredentials.getAccessKeyId(), credentialScope, signature);
+
+ return new AwsRequestSignature.Builder()
+ .setSignature(signature)
+ .setCanonicalHeaders(canonicalHeaders)
+ .setHttpMethod(httpMethod)
+ .setSecurityCredentials(awsSecurityCredentials)
+ .setCredentialScope(credentialScope)
+ .setUrl(uri.toString())
+ .setDate(dates.getOriginalDate())
+ .setRegion(region)
+ .setAuthorizationHeader(authorizationHeader)
+ .build();
+ }
+
+ /** Task 1: Create a canonical request for Signature Version 4. */
+ private String createCanonicalRequestHash(
+ Map headers, List sortedHeaderNames) {
+ // Append the HTTP request method.
+ StringBuilder canonicalRequest = new StringBuilder(httpMethod).append("\n");
+
+ // Append the path.
+ String urlPath = uri.getRawPath().isEmpty() ? "/" : uri.getRawPath();
+ canonicalRequest.append(urlPath).append("\n");
+
+ // Append the canonical query string.
+ String actionQueryString = uri.getRawQuery() != null ? uri.getRawQuery() : "";
+ canonicalRequest.append(actionQueryString).append("\n");
+
+ // Append the canonical headers.
+ StringBuilder canonicalHeaders = new StringBuilder();
+ for (String headerName : sortedHeaderNames) {
+ canonicalHeaders.append(headerName).append(":").append(headers.get(headerName)).append("\n");
+ }
+ canonicalRequest.append(canonicalHeaders).append("\n");
+
+ // Append the signed headers.
+ canonicalRequest.append(Joiner.on(';').join(sortedHeaderNames)).append("\n");
+
+ // Append the hashed request payload.
+ canonicalRequest.append(getHexEncodedSha256Hash(requestPayload.getBytes(UTF_8)));
+
+ // Return the hashed canonical request.
+ return getHexEncodedSha256Hash(canonicalRequest.toString().getBytes(UTF_8));
+ }
+
+ /** Task 2: Create a string to sign for Signature Version 4. */
+ private String createStringToSign(
+ String canonicalRequestHash, String xAmzDate, String credentialScope) {
+ return HASHING_ALGORITHM
+ + "\n"
+ + xAmzDate
+ + "\n"
+ + credentialScope
+ + "\n"
+ + canonicalRequestHash;
+ }
+
+ /**
+ * Task 3: Calculate the signature for AWS Signature Version 4.
+ *
+ * @param date the date used in the hashing process in YYYYMMDD format
+ */
+ private String calculateAwsV4Signature(
+ String serviceName, String secret, String date, String region, String stringToSign) {
+ byte[] kDate = sign(("AWS4" + secret).getBytes(UTF_8), date.getBytes(UTF_8));
+ byte[] kRegion = sign(kDate, region.getBytes(UTF_8));
+ byte[] kService = sign(kRegion, serviceName.getBytes(UTF_8));
+ byte[] kSigning = sign(kService, AWS_REQUEST_TYPE.getBytes(UTF_8));
+ return BaseEncoding.base16().lowerCase().encode(sign(kSigning, stringToSign.getBytes(UTF_8)));
+ }
+
+ /** Task 4: Format the signature to be added to the HTTP request. */
+ private String generateAuthorizationHeader(
+ List sortedHeaderNames,
+ String accessKeyId,
+ String credentialScope,
+ String signature) {
+ return String.format(
+ "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
+ HASHING_ALGORITHM,
+ accessKeyId,
+ credentialScope,
+ Joiner.on(';').join(sortedHeaderNames),
+ signature);
+ }
+
+ private Map getCanonicalHeaders(String defaultDate) {
+ Map headers = new HashMap<>();
+ headers.put("host", uri.getHost());
+
+ // Only add the date if it hasn't been specified through the "date" header.
+ if (!additionalHeaders.containsKey("date")) {
+ headers.put("x-amz-date", defaultDate);
+ }
+
+ if (awsSecurityCredentials.getToken() != null && !awsSecurityCredentials.getToken().isEmpty()) {
+ headers.put("x-amz-security-token", awsSecurityCredentials.getToken());
+ }
+
+ // Add all additional headers.
+ for (String key : additionalHeaders.keySet()) {
+ // Header keys need to be lowercase.
+ headers.put(key.toLowerCase(Locale.US), additionalHeaders.get(key));
+ }
+ return headers;
+ }
+
+ private static byte[] sign(byte[] key, byte[] value) {
+ try {
+ String algorithm = "HmacSHA256";
+ Mac mac = Mac.getInstance(algorithm);
+ mac.init(new SecretKeySpec(key, algorithm));
+ return mac.doFinal(value);
+ } catch (NoSuchAlgorithmException e) {
+ // Will not occur as HmacSHA256 is supported. We may allow other algorithms in the future.
+ throw new RuntimeException(
+ "Invalid algorithm used when calculating the AWS V4 Signature.", e);
+ } catch (InvalidKeyException e) {
+ throw new SigningException("Invalid key used when calculating the AWS V4 Signature", e);
+ }
+ }
+
+ private static String getHexEncodedSha256Hash(byte[] bytes) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return BaseEncoding.base16().lowerCase().encode(digest.digest(bytes));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Failed to compute SHA-256 hash.", e);
+ }
+ }
+
+ static Builder newBuilder(
+ AwsSecurityCredentials awsSecurityCredentials, String httpMethod, String url, String region) {
+ return new Builder(awsSecurityCredentials, httpMethod, url, region);
+ }
+
+ static class Builder {
+
+ private AwsSecurityCredentials awsSecurityCredentials;
+ private String httpMethod;
+ private String url;
+ private String region;
+
+ @Nullable private String requestPayload;
+ @Nullable private Map additionalHeaders;
+ @Nullable private AwsDates dates;
+
+ private Builder(
+ AwsSecurityCredentials awsSecurityCredentials,
+ String httpMethod,
+ String url,
+ String region) {
+ this.awsSecurityCredentials = awsSecurityCredentials;
+ this.httpMethod = httpMethod;
+ this.url = url;
+ this.region = region;
+ }
+
+ Builder setRequestPayload(String requestPayload) {
+ this.requestPayload = requestPayload;
+ return this;
+ }
+
+ Builder setAdditionalHeaders(Map additionalHeaders) {
+ if (additionalHeaders.containsKey("date") && additionalHeaders.containsKey("x-amz-date")) {
+ throw new IllegalArgumentException("One of {date, x-amz-date} can be specified, not both.");
+ }
+ try {
+ if (additionalHeaders.containsKey("date")) {
+ this.dates = AwsDates.fromDateHeader(additionalHeaders.get("date"));
+ }
+ if (additionalHeaders.containsKey("x-amz-date")) {
+ this.dates = AwsDates.fromXAmzDate(additionalHeaders.get("x-amz-date"));
+ }
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("The provided date header value is invalid.", e);
+ }
+
+ this.additionalHeaders = additionalHeaders;
+ return this;
+ }
+
+ AwsRequestSigner build() {
+ return new AwsRequestSigner(
+ awsSecurityCredentials,
+ httpMethod,
+ url,
+ region,
+ requestPayload,
+ additionalHeaders,
+ dates);
+ }
+ }
+
+ static final class AwsDates {
+
+ private static final String X_AMZ_DATE_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
+ private static final String CUSTOM_DATE_FORMAT = "E, dd MMM yyyy HH:mm:ss z";
+
+ private String originalDate;
+ private String xAmzDate;
+
+ private AwsDates(String amzDate) {
+ this.xAmzDate = checkNotNull(amzDate);
+ this.originalDate = amzDate;
+ }
+
+ private AwsDates(String xAmzDate, String originalDate) {
+ this.xAmzDate = checkNotNull(xAmzDate);
+ this.originalDate = checkNotNull(originalDate);
+ }
+
+ static AwsDates fromXAmzDate(String xAmzDate) throws ParseException {
+ // Validate format.
+ new SimpleDateFormat(AwsDates.X_AMZ_DATE_FORMAT).parse(xAmzDate);
+ return new AwsDates(xAmzDate);
+ }
+
+ static AwsDates fromDateHeader(String date) throws ParseException {
+ DateFormat dateFormat = new SimpleDateFormat(X_AMZ_DATE_FORMAT);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ Date inputDate = new SimpleDateFormat(CUSTOM_DATE_FORMAT).parse(date);
+ String xAmzDate = dateFormat.format(inputDate);
+ return new AwsDates(xAmzDate, date);
+ }
+
+ static AwsDates generateXAmzDate() {
+ DateFormat dateFormat = new SimpleDateFormat(X_AMZ_DATE_FORMAT);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String xAmzDate = dateFormat.format(new Date(System.currentTimeMillis()));
+ return new AwsDates(xAmzDate);
+ }
+
+ /**
+ * Returns the original date. This can either be the x-amz-date or a specified date in the
+ * format of E, dd MMM yyyy HH:mm:ss z.
+ */
+ String getOriginalDate() {
+ return originalDate;
+ }
+
+ /** Returns the x-amz-date in yyyyMMdd'T'HHmmss'Z' format. */
+ String getXAmzDate() {
+ return xAmzDate;
+ }
+
+ /** Returns the x-amz-date in YYYYMMDD format. */
+ String getFormattedDate() {
+ return xAmzDate.substring(0, 8);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java
new file mode 100644
index 000000000..04aaeacd7
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import javax.annotation.Nullable;
+
+/**
+ * Defines AWS security credentials. These are either retrieved from the AWS security_credentials
+ * endpoint or AWS environment variables.
+ */
+class AwsSecurityCredentials {
+
+ private String accessKeyId;
+ private String secretAccessKey;
+
+ @Nullable private String token;
+
+ AwsSecurityCredentials(String accessKeyId, String secretAccessKey, @Nullable String token) {
+ this.accessKeyId = accessKeyId;
+ this.secretAccessKey = secretAccessKey;
+ this.token = token;
+ }
+
+ String getAccessKeyId() {
+ return accessKeyId;
+ }
+
+ String getSecretAccessKey() {
+ return secretAccessKey;
+ }
+
+ @Nullable
+ String getToken() {
+ return token;
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java
new file mode 100644
index 000000000..5f22165e9
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.JsonObjectParser;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link AwsRequestSigner}.
+ *
+ * Examples of sigv4 signed requests:
+ * https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
+ */
+public class AwsRequestSignerTest {
+
+ private static final String DATE = "Mon, 09 Sep 2011 23:36:00 GMT";
+ private static final String X_AMZ_DATE = "20200811T065522Z";
+
+ private static final AwsSecurityCredentials BOTOCORE_CREDENTIALS =
+ new AwsSecurityCredentials(
+ "AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", /* token= */ null);
+
+ private AwsSecurityCredentials awsSecurityCredentials;
+
+ @Before
+ public void setUp() throws IOException {
+ awsSecurityCredentials = retrieveAwsSecurityCredentials();
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.sreq
+ @Test
+ public void sign_getHost() {
+ String url = "https://host.foo.com";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "GET", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("GET");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.sreq
+ @Test
+ public void sign_getHostRelativePath() {
+ String url = "https://host.foo.com/foo/bar/../..";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "GET", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("GET");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.sreq
+ @Test
+ public void sign_getHostInvalidPath() {
+ String url = "https://host.foo.com/./";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "GET", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("GET");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.sreq
+ @Test
+ public void sign_getHostDotPath() {
+ String url = "https://host.foo.com/./foo";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "GET", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "910e4d6c9abafaf87898e1eb4c929135782ea25bb0279703146455745391e63a";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("GET");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.sreq
+ @Test
+ public void sign_getHostUtf8Path() {
+ String url = "https://host.foo.com/%E1%88%B4";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "GET", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "8d6634c189aa8c75c2e51e106b6b5121bed103fdb351f7d7d4381c738823af74";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("GET");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.sreq
+ @Test
+ public void sign_getHostDuplicateQueryParam() {
+ String url = "https://host.foo.com/?foo=Zoo&foo=aha";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "GET", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "be7148d34ebccdc6423b19085378aa0bee970bdc61d144bd1a8c48c33079ab09";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("GET");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.sreq
+ @Test
+ public void sign_postWithUpperCaseHeaderKey() {
+ String url = "https://host.foo.com/";
+ String headerKey = "ZOO";
+ String headerValue = "zoobar";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+ headers.put(headerKey, headerValue);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "POST", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "b7a95a52518abbca0964a999a880429ab734f35ebbf1235bd79a5de87756dc4a";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host;zoo, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("POST");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertThat(signature.getCanonicalHeaders().get(headerKey.toLowerCase())).isEqualTo(headerValue);
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.sreq
+ @Test
+ public void sign_postWithUpperCaseHeaderValue() {
+ String url = "https://host.foo.com/";
+ String headerKey = "zoo";
+ String headerValue = "ZOOBAR";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+ headers.put("zoo", "ZOOBAR");
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "POST", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "273313af9d0c265c531e11db70bbd653f3ba074c1009239e8559d3987039cad7";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host;zoo, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("POST");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertThat(signature.getCanonicalHeaders().get(headerKey)).isEqualTo(headerValue);
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.sreq
+ @Test
+ public void sign_postWithHeader() {
+ String url = "https://host.foo.com/";
+ String headerKey = "p";
+ String headerValue = "phfft";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+ headers.put(headerKey, headerValue);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "POST", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host;p, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("POST");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertThat(signature.getCanonicalHeaders().get(headerKey)).isEqualTo(headerValue);
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.sreq
+ @Test
+ public void sign_postWithBodyNoCustomHeaders() {
+ String url = "https://host.foo.com/";
+ String headerKey = "Content-Type";
+ String headerValue = "application/x-www-form-urlencoded";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+ headers.put(headerKey, headerValue);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "POST", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .setRequestPayload("foo=bar")
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "5a15b22cf462f047318703b92e6f4f38884e4a7ab7b1d6426ca46a8bd1c26cbc";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=content-type;date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("POST");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertThat(signature.getCanonicalHeaders().get(headerKey.toLowerCase())).isEqualTo(headerValue);
+ }
+
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.req
+ // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.sreq
+ @Test
+ public void sign_postWithQueryString() {
+ String url = "https://host.foo.com/?foo=bar";
+
+ Map headers = new HashMap<>();
+ headers.put("date", DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(BOTOCORE_CREDENTIALS, "POST", url, "us-east-1")
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "b6e3b79003ce0743a491606ba1035a804593b0efb1e20a11cba83f8c25a57a92";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/"
+ + "aws4_request, SignedHeaders=date;host, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
+ assertThat(signature.getDate()).isEqualTo(DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("POST");
+ assertThat(signature.getRegion()).isEqualTo("us-east-1");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ @Test
+ public void sign_getDescribeRegions() {
+ String url = "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15";
+
+ Map additionalHeaders = new HashMap<>();
+ additionalHeaders.put("x-amz-date", X_AMZ_DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(awsSecurityCredentials, "GET", url, "us-east-2")
+ .setAdditionalHeaders(additionalHeaders)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "631ea80cddfaa545fdadb120dc92c9f18166e38a5c47b50fab9fce476e022855";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential="
+ + awsSecurityCredentials.getAccessKeyId()
+ + "/20200811/us-east-2/ec2/"
+ + "aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(awsSecurityCredentials);
+ assertThat(signature.getDate()).isEqualTo(X_AMZ_DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("GET");
+ assertThat(signature.getRegion()).isEqualTo("us-east-2");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ @Test
+ public void sign_postGetCallerIdentity() {
+ String url = "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15";
+
+ Map additionalHeaders = new HashMap<>();
+ additionalHeaders.put("x-amz-date", X_AMZ_DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(awsSecurityCredentials, "POST", url, "us-east-2")
+ .setAdditionalHeaders(additionalHeaders)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential="
+ + awsSecurityCredentials.getAccessKeyId()
+ + "/20200811/us-east-2/sts/"
+ + "aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(awsSecurityCredentials);
+ assertThat(signature.getDate()).isEqualTo(X_AMZ_DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("POST");
+ assertThat(signature.getRegion()).isEqualTo("us-east-2");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ @Test
+ public void sign_postGetCallerIdentityNoToken() {
+ String url = "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15";
+
+ AwsSecurityCredentials awsSecurityCredentialsWithoutToken =
+ new AwsSecurityCredentials(
+ awsSecurityCredentials.getAccessKeyId(),
+ awsSecurityCredentials.getSecretAccessKey(),
+ /* token= */ null);
+
+ Map additionalHeaders = new HashMap<>();
+ additionalHeaders.put("x-amz-date", X_AMZ_DATE);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(awsSecurityCredentialsWithoutToken, "POST", url, "us-east-2")
+ .setAdditionalHeaders(additionalHeaders)
+ .build();
+
+ AwsRequestSignature signature = signer.sign();
+
+ String expectedSignature = "d095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56";
+ String expectedAuthHeader =
+ "AWS4-HMAC-SHA256 Credential="
+ + awsSecurityCredentials.getAccessKeyId()
+ + "/20200811/us-east-2/sts/"
+ + "aws4_request, SignedHeaders=host;x-amz-date, Signature="
+ + expectedSignature;
+
+ assertThat(signature.getSignature()).isEqualTo(expectedSignature);
+ assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
+ assertThat(signature.getSecurityCredentials()).isEqualTo(awsSecurityCredentialsWithoutToken);
+ assertThat(signature.getDate()).isEqualTo(X_AMZ_DATE);
+ assertThat(signature.getHttpMethod()).isEqualTo("POST");
+ assertThat(signature.getRegion()).isEqualTo("us-east-2");
+ assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ }
+
+ public AwsSecurityCredentials retrieveAwsSecurityCredentials() throws IOException {
+ InputStream stream =
+ AwsRequestSignerTest.class
+ .getClassLoader()
+ .getResourceAsStream("aws_security_credentials.json");
+
+ JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
+ JsonObjectParser parser = new JsonObjectParser(jsonFactory);
+
+ GenericJson json = parser.parseAndClose(stream, OAuth2Utils.UTF_8, GenericJson.class);
+
+ String awsToken = (String) json.get("Token");
+ String secretAccessKey = (String) json.get("SecretAccessKey");
+ String accessKeyId = (String) json.get("AccessKeyId");
+
+ return new AwsSecurityCredentials(accessKeyId, secretAccessKey, awsToken);
+ }
+}
diff --git a/oauth2_http/testresources/aws_security_credentials.json b/oauth2_http/testresources/aws_security_credentials.json
new file mode 100644
index 000000000..76e7688a3
--- /dev/null
+++ b/oauth2_http/testresources/aws_security_credentials.json
@@ -0,0 +1,9 @@
+{
+ "Code" : "Success",
+ "LastUpdated" : "2020-08-11T19:33:07Z",
+ "Type" : "AWS-HMAC",
+ "AccessKeyId" : "ASIARD4OQDT6A77FR3CL",
+ "SecretAccessKey" : "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx",
+ "Token" : "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA==",
+ "Expiration" : "2020-08-11T07:35:49Z"
+}
\ No newline at end of file
From 466694913a990c007c1e62259cc7e49775646ff9 Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Thu, 15 Oct 2020 00:40:04 -0700
Subject: [PATCH 04/23] feat: support generic token formats in
IdentityPoolCredentials (#484)
* feat: adds text/json credential source support to IdentityPoolCredentials
* fix: format
* fix: format
* fix: add missing changes to MockExternalAccountCredentialsTransport
* fix: change parseToken to take an InputStream
* fix: charsets
* fix: broken build
* fix: type null check
---
.../auth/oauth2/IdentityPoolCredentials.java | 87 ++++--
.../oauth2/IdentityPoolCredentialsTest.java | 252 ++++++++++++++----
...ckExternalAccountCredentialsTransport.java | 14 +
3 files changed, 289 insertions(+), 64 deletions(-)
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
index a99ca04cd..7cd3ce046 100644
--- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
@@ -35,10 +35,19 @@
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.auth.http.HttpTransportFactory;
-import com.google.common.annotations.VisibleForTesting;
+import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType;
+import com.google.common.io.CharStreams;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
@@ -51,7 +60,7 @@
/**
* Url-sourced and file-sourced external account credentials.
*
- * By default, attempts to exchange the 3PI credential for a GCP access token.
+ *
By default, attempts to exchange the third-party credential for a GCP access token.
*/
public class IdentityPoolCredentials extends ExternalAccountCredentials {
@@ -59,17 +68,23 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials {
* The IdentityPool credential source. Dictates the retrieval method of the 3PI credential, which
* can either be through a metadata server or a local file.
*/
- @VisibleForTesting
- static class IdentityPoolCredentialSource extends CredentialSource {
+ static class IdentityPoolCredentialSource extends ExternalAccountCredentials.CredentialSource {
enum IdentityPoolCredentialSourceType {
FILE,
URL
}
- private String credentialLocation;
+ enum CredentialFormatType {
+ TEXT,
+ JSON
+ }
+
private IdentityPoolCredentialSourceType credentialSourceType;
+ private CredentialFormatType credentialFormatType;
+ private String credentialLocation;
+ @Nullable private String subjectTokenFieldName;
@Nullable private Map headers;
/**
@@ -81,6 +96,11 @@ enum IdentityPoolCredentialSourceType {
* If this is URL-based 3p credential, the metadata server URL can be retrieved using the
* `url` key.
*
+ *
The third party credential can be provided in different formats, such as text or JSON. The
+ * format can be specified using the `format` header, which will return a map with keys `type`
+ * and `subject_token_field_name`. If the `type` is json, the `subject_token_field_name` must be
+ * provided. If no format is provided, we expect the token to be in the raw text format.
+ *
*
Optional headers can be present, and should be keyed by `headers`.
*/
public IdentityPoolCredentialSource(Map credentialSourceMap) {
@@ -89,9 +109,12 @@ public IdentityPoolCredentialSource(Map credentialSourceMap) {
if (credentialSourceMap.containsKey("file")) {
credentialLocation = (String) credentialSourceMap.get("file");
credentialSourceType = IdentityPoolCredentialSourceType.FILE;
- } else {
+ } else if (credentialSourceMap.containsKey("url")) {
credentialLocation = (String) credentialSourceMap.get("url");
credentialSourceType = IdentityPoolCredentialSourceType.URL;
+ } else {
+ throw new IllegalArgumentException(
+ "Missing credential source file location or URL. At least one must be specified.");
}
Map headersMap = (Map) credentialSourceMap.get("headers");
@@ -99,6 +122,26 @@ public IdentityPoolCredentialSource(Map credentialSourceMap) {
headers = new HashMap<>();
headers.putAll(headersMap);
}
+
+ // If the format is not provided, we will expect the token to be in the raw text format.
+ credentialFormatType = CredentialFormatType.TEXT;
+
+ Map formatMap = (Map) credentialSourceMap.get("format");
+ if (formatMap != null && formatMap.containsKey("type")) {
+ String type = formatMap.get("type");
+ if (type == null || (!type.equals("text") && !type.equals("json"))) {
+ throw new IllegalArgumentException(
+ String.format("Invalid credential source format type: %s.", type));
+ }
+ credentialFormatType =
+ type.equals("text") ? CredentialFormatType.TEXT : CredentialFormatType.JSON;
+
+ if (!formatMap.containsKey("subject_token_field_name")) {
+ throw new IllegalArgumentException(
+ "When specifying a JSON credential type, the subject_token_field_name must be set.");
+ }
+ subjectTokenFieldName = formatMap.get("subject_token_field_name");
+ }
}
private boolean hasHeaders() {
@@ -106,6 +149,8 @@ private boolean hasHeaders() {
}
}
+ private final IdentityPoolCredentialSource identityPoolCredentialSource;
+
/**
* Internal constructor. See {@link
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
@@ -135,6 +180,7 @@ private boolean hasHeaders() {
clientId,
clientSecret,
scopes);
+ this.identityPoolCredentialSource = credentialSource;
}
@Override
@@ -153,8 +199,6 @@ public AccessToken refreshAccessToken() throws IOException {
@Override
public String retrieveSubjectToken() throws IOException {
- IdentityPoolCredentialSource identityPoolCredentialSource =
- (IdentityPoolCredentialSource) credentialSource;
if (identityPoolCredentialSource.credentialSourceType
== IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) {
return retrieveSubjectTokenFromCredentialFile();
@@ -163,8 +207,6 @@ public String retrieveSubjectToken() throws IOException {
}
private String retrieveSubjectTokenFromCredentialFile() throws IOException {
- IdentityPoolCredentialSource identityPoolCredentialSource =
- (IdentityPoolCredentialSource) credentialSource;
String credentialFilePath = identityPoolCredentialSource.credentialLocation;
if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) {
throw new IOException(
@@ -172,17 +214,32 @@ private String retrieveSubjectTokenFromCredentialFile() throws IOException {
"Invalid credential location. The file at %s does not exist.", credentialFilePath));
}
try {
- return new String(Files.readAllBytes(Paths.get(credentialFilePath)));
+ return parseToken(new FileInputStream(new File(credentialFilePath)));
} catch (IOException e) {
throw new IOException(
"Error when attempting to read the subject token from the credential file.", e);
}
}
- private String getSubjectTokenFromMetadataServer() throws IOException {
- IdentityPoolCredentialSource identityPoolCredentialSource =
- (IdentityPoolCredentialSource) credentialSource;
+ private String parseToken(InputStream inputStream) throws IOException {
+ if (identityPoolCredentialSource.credentialFormatType == CredentialFormatType.TEXT) {
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ return CharStreams.toString(reader);
+ }
+
+ JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
+ JsonObjectParser parser = new JsonObjectParser(jsonFactory);
+ GenericJson fileContents =
+ parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class);
+ if (!fileContents.containsKey(identityPoolCredentialSource.subjectTokenFieldName)) {
+ throw new IOException("Invalid subject token field name. No subject token was found.");
+ }
+ return (String) fileContents.get(identityPoolCredentialSource.subjectTokenFieldName);
+ }
+
+ private String getSubjectTokenFromMetadataServer() throws IOException {
HttpRequest request =
transportFactory
.create()
@@ -198,7 +255,7 @@ private String getSubjectTokenFromMetadataServer() throws IOException {
try {
HttpResponse response = request.execute();
- return response.parseAsString();
+ return parseToken(response.getContent());
} catch (IOException e) {
throw new IOException(
String.format("Error getting subject token from metadata server: %s", e.getMessage()), e);
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
index 86996368c..84bedcfaf 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -32,11 +32,13 @@
package com.google.auth.oauth2;
import static com.google.auth.TestUtils.getDefaultExpireTime;
+import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
import static com.google.auth.oauth2.OAuth2Utils.UTF_8;
-import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import com.google.api.client.http.HttpTransport;
+import com.google.api.client.json.GenericJson;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
import java.io.ByteArrayInputStream;
@@ -55,23 +57,10 @@
@RunWith(JUnit4.class)
public class IdentityPoolCredentialsTest {
- private static final String AUDIENCE = "audience";
- private static final String SUBJECT_TOKEN_TYPE = "subjectTokenType";
- private static final String TOKEN_URL = "tokenUrl";
- private static final String TOKEN_INFO_URL = "tokenInfoUrl";
- private static final String SERVICE_ACCOUNT_IMPERSONATION_URL = "tokenInfoUrl";
- private static final String QUOTA_PROJECT_ID = "quotaProjectId";
- private static final String CLIENT_ID = "clientId";
- private static final String CLIENT_SECRET = "clientSecret";
-
- private static final String FILE = "file";
- private static final String URL = "url";
- private static final String HEADERS = "headers";
-
private static final Map FILE_CREDENTIAL_SOURCE_MAP =
new HashMap() {
{
- put(FILE, FILE);
+ put("file", "file");
}
};
@@ -82,10 +71,10 @@ public class IdentityPoolCredentialsTest {
(IdentityPoolCredentials)
IdentityPoolCredentials.newBuilder()
.setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
- .setAudience(AUDIENCE)
- .setSubjectTokenType(SUBJECT_TOKEN_TYPE)
- .setTokenUrl(TOKEN_URL)
- .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setAudience("audience")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl("tokenUrl")
+ .setTokenInfoUrl("tokenInfoUrl")
.setCredentialSource(FILE_CREDENTIAL_SOURCE)
.build();
@@ -104,10 +93,10 @@ public HttpTransport create() {
public void createdScoped_clonedCredentialWithAddedScopes() {
GoogleCredentials credentials =
IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
- .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
- .setQuotaProjectId(QUOTA_PROJECT_ID)
- .setClientId(CLIENT_ID)
- .setClientSecret(CLIENT_SECRET)
+ .setServiceAccountImpersonationUrl("serviceAccountImpersonationUrl")
+ .setQuotaProjectId("quotaProjectId")
+ .setClientId("clientId")
+ .setClientSecret("clientSecret")
.build();
List newScopes = Arrays.asList("scope1", "scope2");
@@ -115,17 +104,17 @@ public void createdScoped_clonedCredentialWithAddedScopes() {
IdentityPoolCredentials newCredentials =
(IdentityPoolCredentials) credentials.createScoped(newScopes);
- assertThat(newCredentials.getAudience()).isEqualTo(AUDIENCE);
- assertThat(newCredentials.getSubjectTokenType()).isEqualTo(SUBJECT_TOKEN_TYPE);
- assertThat(newCredentials.getTokenUrl()).isEqualTo(TOKEN_URL);
- assertThat(newCredentials.getTokenInfoUrl()).isEqualTo(TOKEN_INFO_URL);
- assertThat(newCredentials.getServiceAccountImpersonationUrl())
- .isEqualTo(SERVICE_ACCOUNT_IMPERSONATION_URL);
- assertThat(newCredentials.getCredentialSource()).isEqualTo(FILE_CREDENTIAL_SOURCE);
- assertThat(newCredentials.getScopes()).isEqualTo(newScopes);
- assertThat(newCredentials.getQuotaProjectId()).isEqualTo(QUOTA_PROJECT_ID);
- assertThat(newCredentials.getClientId()).isEqualTo(CLIENT_ID);
- assertThat(newCredentials.getClientSecret()).isEqualTo(CLIENT_SECRET);
+ assertEquals("audience", newCredentials.getAudience());
+ assertEquals("subjectTokenType", newCredentials.getSubjectTokenType());
+ assertEquals("tokenUrl", newCredentials.getTokenUrl());
+ assertEquals("tokenInfoUrl", newCredentials.getTokenInfoUrl());
+ assertEquals(
+ "serviceAccountImpersonationUrl", newCredentials.getServiceAccountImpersonationUrl());
+ assertEquals(FILE_CREDENTIAL_SOURCE, newCredentials.getCredentialSource());
+ assertEquals(newScopes, newCredentials.getScopes());
+ assertEquals("quotaProjectId", newCredentials.getQuotaProjectId());
+ assertEquals("clientId", newCredentials.getClientId());
+ assertEquals("clientSecret", newCredentials.getClientSecret());
}
@Test
@@ -139,7 +128,7 @@ public void retrieveSubjectToken_fileSourced() throws IOException {
new ByteArrayInputStream(credential.getBytes(UTF_8)), file.getAbsolutePath());
Map credentialSourceMap = new HashMap<>();
- credentialSourceMap.put(FILE, file.getAbsolutePath());
+ credentialSourceMap.put("file", file.getAbsolutePath());
IdentityPoolCredentialSource credentialSource =
new IdentityPoolCredentialSource(credentialSourceMap);
@@ -150,14 +139,56 @@ public void retrieveSubjectToken_fileSourced() throws IOException {
.build();
String subjectToken = credentials.retrieveSubjectToken();
- assertThat(subjectToken).isEqualTo(credential);
+
+ assertEquals(credential, subjectToken);
+ }
+
+ @Test
+ public void retrieveSubjectToken_fileSourcedWithJsonFormat() throws IOException {
+ File file =
+ File.createTempFile("RETRIEVE_SUBJECT_TOKEN", /* suffix= */ null, /* directory= */ null);
+ file.deleteOnExit();
+
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ transportFactory.transport.setMetadataServerContentType("json");
+
+ Map credentialSourceMap = new HashMap<>();
+ Map formatMap = new HashMap<>();
+ formatMap.put("type", "json");
+ formatMap.put("subject_token_field_name", "subjectToken");
+
+ credentialSourceMap.put("file", file.getAbsolutePath());
+ credentialSourceMap.put("format", formatMap);
+
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+
+ GenericJson response = new GenericJson();
+ response.setFactory(JSON_FACTORY);
+ response.put("subjectToken", "subjectToken");
+
+ OAuth2Utils.writeInputStreamToFile(
+ new ByteArrayInputStream(response.toString().getBytes(UTF_8)), file.getAbsolutePath());
+
+ IdentityPoolCredentials credential =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(credentialSource)
+ .build();
+
+ String subjectToken = credential.retrieveSubjectToken();
+
+ assertEquals("subjectToken", subjectToken);
}
@Test
public void retrieveSubjectToken_noFile_throws() {
Map credentialSourceMap = new HashMap<>();
String path = "badPath";
- credentialSourceMap.put(FILE, path);
+ credentialSourceMap.put("file", path);
IdentityPoolCredentialSource credentialSource =
new IdentityPoolCredentialSource(credentialSourceMap);
@@ -177,9 +208,9 @@ public void run() throws Throwable {
}
});
- assertThat(e.getMessage())
- .isEqualTo(
- String.format("Invalid credential location. The file at %s does not exist.", path));
+ assertEquals(
+ String.format("Invalid credential location. The file at %s does not exist.", path),
+ e.getMessage());
}
@Test
@@ -196,7 +227,34 @@ public void retrieveSubjectToken_urlSourced() throws IOException {
.build();
String subjectToken = credential.retrieveSubjectToken();
- assertThat(subjectToken).isEqualTo(transportFactory.transport.getSubjectToken());
+
+ assertEquals(transportFactory.transport.getSubjectToken(), subjectToken);
+ }
+
+ @Test
+ public void retrieveSubjectToken_urlSourcedWithJsonFormat() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ transportFactory.transport.setMetadataServerContentType("json");
+
+ Map formatMap = new HashMap<>();
+ formatMap.put("type", "json");
+ formatMap.put("subject_token_field_name", "subjectToken");
+
+ IdentityPoolCredentialSource credentialSource =
+ buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl(), formatMap);
+
+ IdentityPoolCredentials credential =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(credentialSource)
+ .build();
+
+ String subjectToken = credential.retrieveSubjectToken();
+
+ assertEquals(transportFactory.transport.getSubjectToken(), subjectToken);
}
@Test
@@ -225,10 +283,10 @@ public void run() throws Throwable {
}
});
- assertThat(e.getMessage())
- .isEqualTo(
- String.format(
- "Error getting subject token from metadata server: %s", response.getMessage()));
+ assertEquals(
+ String.format(
+ "Error getting subject token from metadata server: %s", response.getMessage()),
+ e.getMessage());
}
@Test
@@ -246,7 +304,8 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc
.build();
AccessToken accessToken = credential.refreshAccessToken();
- assertThat(accessToken.getTokenValue()).isEqualTo(transportFactory.transport.getAccessToken());
+
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
}
@Test
@@ -267,15 +326,110 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept
.build();
AccessToken accessToken = credential.refreshAccessToken();
- assertThat(accessToken.getTokenValue()).isEqualTo(transportFactory.transport.getAccessToken());
+
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+ }
+
+ @Test
+ public void identityPoolCredentialSource_invalidSourceType() {
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() {
+ new IdentityPoolCredentialSource(new HashMap());
+ }
+ });
+
+ assertEquals(
+ "Missing credential source file location or URL. At least one must be specified.",
+ e.getMessage());
+ }
+
+ @Test
+ public void identityPoolCredentialSource_invalidFormatType() {
+ final Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("url", "url");
+
+ Map format = new HashMap<>();
+ format.put("type", "unsupportedType");
+ credentialSourceMap.put("format", format);
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() {
+ new IdentityPoolCredentialSource(credentialSourceMap);
+ }
+ });
+
+ assertEquals("Invalid credential source format type: unsupportedType.", e.getMessage());
+ }
+
+ @Test
+ public void identityPoolCredentialSource_nullFormatType() {
+ final Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("url", "url");
+
+ Map format = new HashMap<>();
+ format.put("type", null);
+ credentialSourceMap.put("format", format);
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() {
+ new IdentityPoolCredentialSource(credentialSourceMap);
+ }
+ });
+
+ assertEquals("Invalid credential source format type: null.", e.getMessage());
}
+ @Test
+ public void identityPoolCredentialSource_subjectTokenFieldNameUnset() {
+ final Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("url", "url");
+
+ Map format = new HashMap<>();
+ format.put("type", "json");
+ credentialSourceMap.put("format", format);
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() {
+ new IdentityPoolCredentialSource(credentialSourceMap);
+ }
+ });
+
+ assertEquals(
+ "When specifying a JSON credential type, the subject_token_field_name must be set.",
+ e.getMessage());
+ }
+
+ @Test
+ public void identityPoolCredentialSource_jsonFormatTypeWithoutSubjectTokenFieldName() {}
+
private IdentityPoolCredentialSource buildUrlBasedCredentialSource(String url) {
+ return buildUrlBasedCredentialSource(url, /* formatMap= */ null);
+ }
+
+ private IdentityPoolCredentialSource buildUrlBasedCredentialSource(
+ String url, Map formatMap) {
Map credentialSourceMap = new HashMap<>();
Map headers = new HashMap<>();
headers.put("Metadata-Flavor", "Google");
- credentialSourceMap.put(URL, url);
- credentialSourceMap.put(HEADERS, headers);
+ credentialSourceMap.put("url", url);
+ credentialSourceMap.put("headers", headers);
+ credentialSourceMap.put("format", formatMap);
return new IdentityPoolCredentialSource(credentialSourceMap);
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index d977aeee8..95ad8de06 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -80,6 +80,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
private Queue> scopeSequence = new ArrayDeque<>();
private MockLowLevelHttpRequest request;
private String expireTime;
+ private String metadataServerContentType;
public void addResponseErrorSequence(IOException... errors) {
Collections.addAll(responseErrorSequence, errors);
@@ -108,6 +109,15 @@ public LowLevelHttpResponse execute() throws IOException {
if (!"Google".equals(metadataRequestHeader)) {
throw new IOException("Metadata request header not found.");
}
+
+ if (metadataServerContentType != null && metadataServerContentType.equals("json")) {
+ GenericJson response = new GenericJson();
+ response.setFactory(JSON_FACTORY);
+ response.put("subjectToken", SUBJECT_TOKEN);
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(response.toString());
+ }
return new MockLowLevelHttpResponse()
.setContentType("text/html")
.setContent(SUBJECT_TOKEN);
@@ -200,4 +210,8 @@ public String getServiceAccountImpersonationUrl() {
public void setExpireTime(String expireTime) {
this.expireTime = expireTime;
}
+
+ public void setMetadataServerContentType(String contentType) {
+ this.metadataServerContentType = contentType;
+ }
}
From 9d4d721a2ef18bca1c4146e98654289ae00cbd60 Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Thu, 15 Oct 2020 00:40:44 -0700
Subject: [PATCH 05/23] feat: adds support for AWS credentials (#483)
* feat: adds support for AWS credentials
* fix: address nits
* fix: remove Truth lib use in AwsCredentialsTest
* fix: address more review comments
* fix: assertEquals param order
* feat: retrieve region from environment variable for AWS Lambda
---
.../google/auth/oauth2/AwsCredentials.java | 306 ++++++++++++++
.../oauth2/ExternalAccountCredentials.java | 17 +-
.../auth/oauth2/IdentityPoolCredentials.java | 2 +-
.../auth/oauth2/AwsCredentialsTest.java | 394 ++++++++++++++++++
...ckExternalAccountCredentialsTransport.java | 52 ++-
5 files changed, 760 insertions(+), 11 deletions(-)
create mode 100644 oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
new file mode 100644
index 000000000..e26246183
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonParser;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * AWS credentials representing a third-party identity for calling Google APIs.
+ *
+ * By default, attempts to exchange the 3PI credential for a GCP access token.
+ */
+public class AwsCredentials extends ExternalAccountCredentials {
+
+ /**
+ * The AWS credential source. Stores data required to retrieve the AWS credential from the AWS
+ * metadata server.
+ */
+ static class AwsCredentialSource extends CredentialSource {
+
+ private String regionUrl;
+ private String url;
+ private String regionalCredentialVerificationUrl;
+
+ /**
+ * The source of the AWS credential. The credential source map must contain the `region_url`,
+ * `url, and `regional_cred_verification_url` entries.
+ *
+ *
The `region_url` is used to retrieve to targeted region.
+ *
+ *
The `url` is the metadata server URL which is used to retrieve the AWS credentials.
+ *
+ *
The `regional_cred_verification_url` is the regional GetCallerIdentity action URL, used to
+ * determine the account ID and its roles.
+ */
+ AwsCredentialSource(Map credentialSourceMap) {
+ super(credentialSourceMap);
+ if (!credentialSourceMap.containsKey("region_url")) {
+ throw new IllegalArgumentException(
+ "A region_url representing the targeted region must be specified.");
+ }
+ if (!credentialSourceMap.containsKey("url")) {
+ throw new IllegalArgumentException(
+ "A url representing the metadata server endpoint must be specified.");
+ }
+ if (!credentialSourceMap.containsKey("regional_cred_verification_url")) {
+ throw new IllegalArgumentException(
+ "A regional_cred_verification_url representing the"
+ + " GetCallerIdentity action URL must be specified.");
+ }
+ this.regionUrl = (String) credentialSourceMap.get("region_url");
+ this.url = (String) credentialSourceMap.get("url");
+ this.regionalCredentialVerificationUrl =
+ (String) credentialSourceMap.get("regional_cred_verification_url");
+ }
+ }
+
+ /**
+ * Internal constructor. See {@link
+ * ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
+ * String, String, CredentialSource, String, String, String, String, Collection)}
+ */
+ AwsCredentials(
+ HttpTransportFactory transportFactory,
+ String audience,
+ String subjectTokenType,
+ String tokenUrl,
+ String tokenInfoUrl,
+ AwsCredentialSource credentialSource,
+ @Nullable String serviceAccountImpersonationUrl,
+ @Nullable String quotaProjectId,
+ @Nullable String clientId,
+ @Nullable String clientSecret,
+ @Nullable Collection scopes) {
+ super(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ scopes);
+ }
+
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), subjectTokenType)
+ .setAudience(audience);
+
+ // Add scopes, if possible.
+ if (scopes != null && !scopes.isEmpty()) {
+ stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
+ }
+
+ AccessToken accessToken = exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build());
+ return attemptServiceAccountImpersonation(accessToken);
+ }
+
+ @Override
+ public String retrieveSubjectToken() throws IOException {
+ // The targeted region is required to generate the signed request. The regional
+ // endpoint must also be used.
+ String region = getAwsRegion();
+
+ AwsSecurityCredentials credentials = getAwsSecurityCredentials();
+
+ // Generate the signed request to the AWS STS GetCallerIdentity API.
+ Map headers = new HashMap<>();
+ headers.put("x-goog-cloud-target-resource", audience);
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(
+ credentials,
+ "POST",
+ ((AwsCredentialSource) credentialSource)
+ .regionalCredentialVerificationUrl.replace("{region}", region),
+ region)
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature awsRequestSignature = signer.sign();
+ return buildSubjectToken(awsRequestSignature);
+ }
+
+ /** Clones the AwsCredentials with the specified scopes. */
+ @Override
+ public GoogleCredentials createScoped(Collection newScopes) {
+ return new AwsCredentials(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ (AwsCredentialSource) credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ newScopes);
+ }
+
+ private String retrieveResource(String url, String resourceName) throws IOException {
+ try {
+ HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
+ HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
+ HttpResponse response = request.execute();
+ return response.parseAsString();
+ } catch (IOException e) {
+ throw new IOException(String.format("Failed to retrieve AWS %s.", resourceName), e);
+ }
+ }
+
+ private String buildSubjectToken(AwsRequestSignature signature) {
+ GenericJson headers = new GenericJson();
+ headers.setFactory(OAuth2Utils.JSON_FACTORY);
+
+ Map canonicalHeaders = signature.getCanonicalHeaders();
+ for (String headerName : canonicalHeaders.keySet()) {
+ headers.put(headerName, canonicalHeaders.get(headerName));
+ }
+
+ headers.put("Authorization", signature.getAuthorizationHeader());
+ headers.put("x-goog-cloud-target-resource", audience);
+
+ GenericJson token = new GenericJson();
+ token.setFactory(OAuth2Utils.JSON_FACTORY);
+
+ token.put("headers", headers);
+ token.put("method", signature.getHttpMethod());
+ token.put(
+ "url",
+ ((AwsCredentialSource) credentialSource)
+ .regionalCredentialVerificationUrl.replace("{region}", signature.getRegion()));
+ return token.toString();
+ }
+
+ private String getAwsRegion() throws IOException {
+ // For AWS Lambda, the region is retrieved through the AWS_REGION environment variable.
+ String region = getEnv("AWS_REGION");
+ if (region != null) {
+ return region;
+ }
+ region = retrieveResource(((AwsCredentialSource) credentialSource).regionUrl, "region");
+
+ // There is an extra appended character that must be removed. If `us-east-1b` is returned,
+ // we want `us-east-1`.
+ return region.substring(0, region.length() - 1);
+ }
+
+ @VisibleForTesting
+ AwsSecurityCredentials getAwsSecurityCredentials() throws IOException {
+ // Check environment variables for credentials first.
+ String accessKeyId = getEnv("AWS_ACCESS_KEY_ID");
+ String secretAccessKey = getEnv("AWS_SECRET_ACCESS_KEY");
+ String token = getEnv("Token");
+ if (accessKeyId != null && secretAccessKey != null) {
+ return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token);
+ }
+
+ // Credentials not retrievable from environment variables - call metadata server.
+ AwsCredentialSource awsCredentialSource = (AwsCredentialSource) credentialSource;
+ // Retrieve the IAM role that is attached to the VM. This is required to retrieve the AWS
+ // security credentials.
+ String roleName = retrieveResource(awsCredentialSource.url, "IAM role");
+
+ // Retrieve the AWS security credentials by calling the endpoint specified by the credential
+ // source.
+ String awsCredentials =
+ retrieveResource(awsCredentialSource.url + "/" + roleName, "credentials");
+
+ JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(awsCredentials);
+ GenericJson genericJson = parser.parseAndClose(GenericJson.class);
+
+ accessKeyId = (String) genericJson.get("AccessKeyId");
+ secretAccessKey = (String) genericJson.get("SecretAccessKey");
+ token = (String) genericJson.get("Token");
+
+ // These credentials last for a few hours - we may consider caching these in the
+ // future.
+ return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token);
+ }
+
+ @VisibleForTesting
+ String getEnv(String name) {
+ return System.getenv(name);
+ }
+
+ public static AwsCredentials.Builder newBuilder() {
+ return new AwsCredentials.Builder();
+ }
+
+ public static AwsCredentials.Builder newBuilder(AwsCredentials awsCredentials) {
+ return new AwsCredentials.Builder(awsCredentials);
+ }
+
+ public static class Builder extends ExternalAccountCredentials.Builder {
+
+ protected Builder() {}
+
+ protected Builder(AwsCredentials credentials) {
+ super(credentials);
+ }
+
+ @Override
+ public AwsCredentials build() {
+ return new AwsCredentials(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ (AwsCredentialSource) credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ scopes);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index d04fff0cb..8f272ca5c 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -44,6 +44,7 @@
import com.google.api.client.util.GenericData;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
import java.io.IOException;
import java.io.InputStream;
@@ -87,8 +88,8 @@ protected CredentialSource(Map credentialSourceMap) {
protected final String tokenInfoUrl;
protected final String serviceAccountImpersonationUrl;
protected final CredentialSource credentialSource;
+ protected final Collection scopes;
- @Nullable protected final Collection scopes;
@Nullable protected final String quotaProjectId;
@Nullable protected final String clientId;
@Nullable protected final String clientSecret;
@@ -212,7 +213,18 @@ public static ExternalAccountCredentials fromJson(
json.containsKey("quota_project_id") ? (String) json.get("quota_project_id") : null;
if (isAwsCredential(credentialSourceMap)) {
- // TODO(lsirac) return AWS credential here.
+ return new AwsCredentials(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ new AwsCredentialSource(credentialSourceMap),
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ /* scopes= */ null);
}
return new IdentityPoolCredentials(
transportFactory,
@@ -377,6 +389,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
protected Builder() {}
protected Builder(ExternalAccountCredentials credentials) {
+ this.transportFactory = credentials.transportFactory;
this.audience = credentials.audience;
this.subjectTokenType = credentials.subjectTokenType;
this.tokenUrl = credentials.tokenUrl;
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
index 7cd3ce046..75b96353e 100644
--- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
@@ -291,7 +291,7 @@ public static class Builder extends ExternalAccountCredentials.Builder {
protected Builder() {}
- protected Builder(ExternalAccountCredentials credentials) {
+ protected Builder(IdentityPoolCredentials credentials) {
super(credentials);
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
new file mode 100644
index 000000000..7fd708bf6
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2020, Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import static com.google.auth.TestUtils.getDefaultExpireTime;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonParser;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource;
+import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link AwsCredentials}. */
+@RunWith(JUnit4.class)
+public class AwsCredentialsTest {
+
+ private static final String GET_CALLER_IDENTITY_URL =
+ "https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15";
+
+ private static final Map AWS_CREDENTIAL_SOURCE_MAP =
+ new HashMap() {
+ {
+ put("region_url", "regionUrl");
+ put("url", "url");
+ put("regional_cred_verification_url", "regionalCredVerificationUrl");
+ }
+ };
+
+ private static final AwsCredentialSource AWS_CREDENTIAL_SOURCE =
+ new AwsCredentialSource(AWS_CREDENTIAL_SOURCE_MAP);
+
+ private static final AwsCredentials AWS_CREDENTIAL =
+ (AwsCredentials)
+ AwsCredentials.newBuilder()
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience("audience")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl("tokenUrl")
+ .setTokenInfoUrl("tokenInfoUrl")
+ .setCredentialSource(AWS_CREDENTIAL_SOURCE)
+ .build();
+
+ @Test
+ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ AwsCredentials awsCredential =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setTokenUrl(transportFactory.transport.getStsUrl())
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(buildAwsCredentialSource(transportFactory))
+ .build();
+
+ AccessToken accessToken = awsCredential.refreshAccessToken();
+
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+ }
+
+ @Test
+ public void refreshAccessToken_withServiceAccountImpersonation() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ transportFactory.transport.setExpireTime(getDefaultExpireTime());
+
+ AwsCredentials awsCredential =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setTokenUrl(transportFactory.transport.getStsUrl())
+ .setServiceAccountImpersonationUrl(
+ transportFactory.transport.getServiceAccountImpersonationUrl())
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(buildAwsCredentialSource(transportFactory))
+ .build();
+
+ AccessToken accessToken = awsCredential.refreshAccessToken();
+
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+ }
+
+ @Test
+ public void retrieveSubjectToken() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ AwsCredentials awsCredential =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(buildAwsCredentialSource(transportFactory))
+ .build();
+
+ String subjectToken = awsCredential.retrieveSubjectToken();
+
+ JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(subjectToken);
+ GenericJson json = parser.parseAndClose(GenericJson.class);
+
+ Map headers = (Map) json.get("headers");
+
+ assertEquals("POST", json.get("method"));
+ assertEquals(GET_CALLER_IDENTITY_URL, json.get("url"));
+ assertEquals(URI.create(GET_CALLER_IDENTITY_URL).getHost(), headers.get("host"));
+ assertEquals("token", headers.get("x-amz-security-token"));
+ assertEquals(awsCredential.getAudience(), headers.get("x-goog-cloud-target-resource"));
+ assertTrue(headers.containsKey("x-amz-date"));
+ assertNotNull(headers.get("Authorization"));
+ }
+
+ @Test
+ public void retrieveSubjectToken_noRegion_expectThrows() {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ IOException response = new IOException();
+ transportFactory.transport.addResponseErrorSequence(response);
+
+ final AwsCredentials awsCredential =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(buildAwsCredentialSource(transportFactory))
+ .build();
+
+ IOException e =
+ assertThrows(
+ IOException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ awsCredential.retrieveSubjectToken();
+ }
+ });
+
+ assertEquals("Failed to retrieve AWS region.", e.getMessage());
+ }
+
+ @Test
+ public void retrieveSubjectToken_noRole_expectThrows() {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ IOException response = new IOException();
+ transportFactory.transport.addResponseErrorSequence(response);
+ transportFactory.transport.addResponseSequence(true, false);
+
+ final AwsCredentials awsCredential =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(buildAwsCredentialSource(transportFactory))
+ .build();
+
+ IOException e =
+ assertThrows(
+ IOException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ awsCredential.retrieveSubjectToken();
+ }
+ });
+
+ assertEquals("Failed to retrieve AWS IAM role.", e.getMessage());
+ }
+
+ @Test
+ public void retrieveSubjectToken_noCredentials_expectThrows() {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ IOException response = new IOException();
+ transportFactory.transport.addResponseErrorSequence(response);
+ transportFactory.transport.addResponseSequence(true, true, false);
+
+ final AwsCredentials awsCredential =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(buildAwsCredentialSource(transportFactory))
+ .build();
+
+ IOException e =
+ assertThrows(
+ IOException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ awsCredential.retrieveSubjectToken();
+ }
+ });
+
+ assertEquals("Failed to retrieve AWS credentials.", e.getMessage());
+ }
+
+ @Test
+ public void getAwsSecurityCredentials_fromEnvironmentVariablesNoToken() throws IOException {
+ TestAwsCredentials testAwsCredentials = TestAwsCredentials.newBuilder(AWS_CREDENTIAL).build();
+ testAwsCredentials.setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId");
+ testAwsCredentials.setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey");
+
+ AwsSecurityCredentials credentials = testAwsCredentials.getAwsSecurityCredentials();
+
+ assertEquals("awsAccessKeyId", credentials.getAccessKeyId());
+ assertEquals("awsSecretAccessKey", credentials.getSecretAccessKey());
+ assertNull(credentials.getToken());
+ }
+
+ @Test
+ public void getAwsSecurityCredentials_fromEnvironmentVariablesWithToken() throws IOException {
+ TestAwsCredentials testAwsCredentials = TestAwsCredentials.newBuilder(AWS_CREDENTIAL).build();
+ testAwsCredentials.setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId");
+ testAwsCredentials.setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey");
+ testAwsCredentials.setEnv("Token", "token");
+
+ AwsSecurityCredentials credentials = testAwsCredentials.getAwsSecurityCredentials();
+
+ assertEquals("awsAccessKeyId", credentials.getAccessKeyId());
+ assertEquals("awsSecretAccessKey", credentials.getSecretAccessKey());
+ assertEquals("token", credentials.getToken());
+ }
+
+ @Test
+ public void getAwsSecurityCredentials_fromMetadataServer() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ AwsCredentials awsCredential =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setHttpTransportFactory(transportFactory)
+ .setCredentialSource(buildAwsCredentialSource(transportFactory))
+ .build();
+
+ AwsSecurityCredentials credentials = awsCredential.getAwsSecurityCredentials();
+
+ assertEquals("accessKeyId", credentials.getAccessKeyId());
+ assertEquals("secretAccessKey", credentials.getSecretAccessKey());
+ assertEquals("token", credentials.getToken());
+ }
+
+ @Test
+ public void createdScoped_clonedCredentialWithAddedScopes() {
+ AwsCredentials credentials =
+ (AwsCredentials)
+ AwsCredentials.newBuilder(AWS_CREDENTIAL)
+ .setServiceAccountImpersonationUrl("tokenInfoUrl")
+ .setQuotaProjectId("quotaProjectId")
+ .setClientId("clientId")
+ .setClientSecret("clientSecret")
+ .build();
+
+ List newScopes = Arrays.asList("scope1", "scope2");
+
+ AwsCredentials newCredentials = (AwsCredentials) credentials.createScoped(newScopes);
+
+ assertEquals(credentials.getAudience(), newCredentials.getAudience());
+ assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType());
+ assertEquals(credentials.getTokenUrl(), newCredentials.getTokenUrl());
+ assertEquals(credentials.getTokenInfoUrl(), newCredentials.getTokenInfoUrl());
+ assertEquals(
+ credentials.getServiceAccountImpersonationUrl(),
+ newCredentials.getServiceAccountImpersonationUrl());
+ assertEquals(credentials.getCredentialSource(), newCredentials.getCredentialSource());
+ assertEquals(credentials.getQuotaProjectId(), newCredentials.getQuotaProjectId());
+ assertEquals(credentials.getClientId(), newCredentials.getClientId());
+ assertEquals(credentials.getClientSecret(), newCredentials.getClientSecret());
+ assertEquals(newScopes, newCredentials.getScopes());
+ }
+
+ private AwsCredentialSource buildAwsCredentialSource(
+ MockExternalAccountCredentialsTransportFactory transportFactory) {
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("region_url", transportFactory.transport.getAwsRegionEndpoint());
+ credentialSourceMap.put("url", transportFactory.transport.getAwsCredentialsEndpoint());
+ credentialSourceMap.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL);
+ return new AwsCredentialSource(credentialSourceMap);
+ }
+
+ /** Used to test the retrieval of AWS credentials from environment variables. */
+ private static class TestAwsCredentials extends AwsCredentials {
+
+ private final Map environmentVariables = new HashMap<>();
+
+ TestAwsCredentials(
+ HttpTransportFactory transportFactory,
+ String audience,
+ String subjectTokenType,
+ String tokenUrl,
+ String tokenInfoUrl,
+ AwsCredentialSource credentialSource,
+ @Nullable String serviceAccountImpersonationUrl,
+ @Nullable String quotaProjectId,
+ @Nullable String clientId,
+ @Nullable String clientSecret,
+ @Nullable Collection scopes) {
+ super(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ scopes);
+ }
+
+ public static TestAwsCredentials.Builder newBuilder(AwsCredentials awsCredentials) {
+ return new TestAwsCredentials.Builder(awsCredentials);
+ }
+
+ public static class Builder extends AwsCredentials.Builder {
+
+ protected Builder(AwsCredentials credentials) {
+ super(credentials);
+ }
+
+ @Override
+ public TestAwsCredentials build() {
+ return new TestAwsCredentials(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ (AwsCredentialSource) credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ scopes);
+ }
+ }
+
+ @Override
+ String getEnv(String name) {
+ return environmentVariables.get(name);
+ }
+
+ void setEnv(String name, String value) {
+ environmentVariables.put(name, value);
+ }
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index 95ad8de06..1c1d6fe41 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -61,20 +61,23 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
"urn:ietf:params:oauth:grant-type:token-exchange";
private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
+ private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
+ private static final String AWS_CREDENTIALS_URL = "https://www.aws-credentials.com";
+ private static final String AWS_REGION_URL = "https://www.aws-region.com";
private static final String METADATA_SERVER_URL = "https://www.metadata.google.com";
private static final String STS_URL = "https://www.sts.google.com";
private static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
"https://iamcredentials.googleapis.com";
private static final String SUBJECT_TOKEN = "subjectToken";
- private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
private static final String TOKEN_TYPE = "Bearer";
private static final String ACCESS_TOKEN = "accessToken";
private static final int EXPIRES_IN = 3600;
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
+ private Queue responseSequence = new ArrayDeque<>();
private Queue responseErrorSequence = new ArrayDeque<>();
private Queue refreshTokenSequence = new ArrayDeque<>();
private Queue> scopeSequence = new ArrayDeque<>();
@@ -86,6 +89,10 @@ public void addResponseErrorSequence(IOException... errors) {
Collections.addAll(responseErrorSequence, errors);
}
+ public void addResponseSequence(Boolean... responses) {
+ Collections.addAll(responseSequence, responses);
+ }
+
public void addRefreshTokenSequence(String... refreshTokens) {
Collections.addAll(refreshTokenSequence, refreshTokens);
}
@@ -100,11 +107,35 @@ public LowLevelHttpRequest buildRequest(final String method, final String url) {
new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
- if (METADATA_SERVER_URL.equals(url)) {
- if (!responseErrorSequence.isEmpty()) {
- throw responseErrorSequence.poll();
- }
+ boolean successfulResponse = !responseSequence.isEmpty() && responseSequence.poll();
+
+ if (!responseErrorSequence.isEmpty() && !successfulResponse) {
+ throw responseErrorSequence.poll();
+ }
+ if (AWS_REGION_URL.equals(url)) {
+ return new MockLowLevelHttpResponse()
+ .setContentType("text/html")
+ .setContent("us-east-1b");
+ }
+ if (AWS_CREDENTIALS_URL.equals(url)) {
+ return new MockLowLevelHttpResponse()
+ .setContentType("text/html")
+ .setContent("roleName");
+ }
+ if ((AWS_CREDENTIALS_URL + "/" + "roleName").equals(url)) {
+ GenericJson response = new GenericJson();
+ response.setFactory(JSON_FACTORY);
+ response.put("AccessKeyId", "accessKeyId");
+ response.put("SecretAccessKey", "secretAccessKey");
+ response.put("Token", "token");
+
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(response.toString());
+ }
+
+ if (METADATA_SERVER_URL.equals(url)) {
String metadataRequestHeader = getFirstHeaderValue("Metadata-Flavor");
if (!"Google".equals(metadataRequestHeader)) {
throw new IOException("Metadata request header not found.");
@@ -123,9 +154,6 @@ public LowLevelHttpResponse execute() throws IOException {
.setContent(SUBJECT_TOKEN);
}
if (STS_URL.equals(url)) {
- if (!responseErrorSequence.isEmpty()) {
- throw responseErrorSequence.poll();
- }
Map query = TestUtils.parseQuery(getContentAsString());
assertThat(query.get("grant_type")).isEqualTo(EXPECTED_GRANT_TYPE);
assertThat(query.get("subject_token_type")).isNotEmpty();
@@ -199,6 +227,14 @@ public String getMetadataUrl() {
return METADATA_SERVER_URL;
}
+ public String getAwsCredentialsEndpoint() {
+ return AWS_CREDENTIALS_URL;
+ }
+
+ public String getAwsRegionEndpoint() {
+ return AWS_REGION_URL;
+ }
+
public String getStsUrl() {
return STS_URL;
}
From dc16550aaf17e078c1a72cb2963dc3406818de7e Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Thu, 15 Oct 2020 15:27:59 -0700
Subject: [PATCH 06/23] feat: add quota project ID to requestMetadata if
present (#495)
---
.../oauth2/ExternalAccountCredentials.java | 10 +-
.../ExternalAccountCredentialsTest.java | 197 +++++++++++++-----
2 files changed, 157 insertions(+), 50 deletions(-)
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index 8f272ca5c..6367069d2 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -48,19 +48,21 @@
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
+import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Base external account credentials class.
*
- * Handles initializing 3PI credentials, calls to STS and service account impersonation.
+ *
Handles initializing third-party credentials, calls to STS and service account impersonation.
*/
public abstract class ExternalAccountCredentials extends GoogleCredentials
implements QuotaProjectIdProvider {
@@ -148,6 +150,12 @@ protected ExternalAccountCredentials(
(scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
}
+ @Override
+ public Map> getRequestMetadata(URI uri) throws IOException {
+ Map> requestMetadata = super.getRequestMetadata(uri);
+ return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
+ }
+
/**
* Returns credentials defined by a JSON file stream.
*
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
index fb3bc9454..a4ea7b72e 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
@@ -32,22 +32,27 @@
package com.google.auth.oauth2;
import static com.google.auth.TestUtils.getDefaultExpireTime;
-import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
-import com.google.api.client.util.GenericData;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
-import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
+import com.google.auth.oauth2.ExternalAccountCredentialsTest.TestExternalAccountCredentials.TestCredentialSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.net.URI;
+import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
@@ -58,11 +63,7 @@
@RunWith(JUnit4.class)
public class ExternalAccountCredentialsTest {
- private static final String AUDIENCE = "audience";
- private static final String SUBJECT_TOKEN_TYPE = "subjectTokenType";
- private static final String TOKEN_INFO_URL = "tokenInfoUrl";
private static final String STS_URL = "https://www.sts.google.com";
- private static final String CREDENTIAL = "credential";
private static final String ACCESS_TOKEN = "eya23tfgdfga2123as";
private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
@@ -72,16 +73,6 @@ static class MockExternalAccountCredentialsTransportFactory implements HttpTrans
MockExternalAccountCredentialsTransport transport =
new MockExternalAccountCredentialsTransport();
- private static final Map FILE_CREDENTIAL_SOURCE_MAP =
- new HashMap() {
- {
- put("file", "file");
- }
- };
-
- private static final IdentityPoolCredentialSource FILE_CREDENTIAL_SOURCE =
- new IdentityPoolCredentialSource(FILE_CREDENTIAL_SOURCE_MAP);
-
@Override
public HttpTransport create() {
return transport;
@@ -97,13 +88,24 @@ public void setup() {
@Test
public void fromStream_identityPoolCredentials() throws IOException {
- GenericJson json = buildDefaultIdentityPoolCredential();
+ GenericJson json = buildJsonIdentityPoolCredential();
+ TestUtils.jsonToInputStream(json);
+
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
+
+ assertTrue(credential instanceof IdentityPoolCredentials);
+ }
+
+ @Test
+ public void fromStream_awsCredentials() throws IOException {
+ GenericJson json = buildJsonAwsCredential();
TestUtils.jsonToInputStream(json);
ExternalAccountCredentials credential =
ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
- assertThat(credential).isInstanceOf(IdentityPoolCredentials.class);
+ assertTrue(credential instanceof AwsCredentials);
}
@Test
@@ -136,14 +138,28 @@ public void run() throws Throwable {
public void fromJson_identityPoolCredentials() {
ExternalAccountCredentials credential =
ExternalAccountCredentials.fromJson(
- buildDefaultIdentityPoolCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
- assertThat(credential).isInstanceOf(IdentityPoolCredentials.class);
-
- assertThat(credential.getAudience()).isEqualTo(AUDIENCE);
- assertThat(credential.getSubjectTokenType()).isEqualTo(SUBJECT_TOKEN_TYPE);
- assertThat(credential.getTokenUrl()).isEqualTo(STS_URL);
- assertThat(credential.getTokenInfoUrl()).isEqualTo(TOKEN_INFO_URL);
- assertThat(credential.getCredentialSource()).isNotNull();
+ buildJsonIdentityPoolCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+
+ assertTrue(credential instanceof IdentityPoolCredentials);
+ assertEquals("audience", credential.getAudience());
+ assertEquals("subjectTokenType", credential.getSubjectTokenType());
+ assertEquals(STS_URL, credential.getTokenUrl());
+ assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
+ assertNotNull(credential.getCredentialSource());
+ }
+
+ @Test
+ public void fromJson_awsCredentials() {
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(
+ buildJsonAwsCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+
+ assertTrue(credential instanceof AwsCredentials);
+ assertEquals("audience", credential.getAudience());
+ assertEquals("subjectTokenType", credential.getSubjectTokenType());
+ assertEquals(STS_URL, credential.getTokenUrl());
+ assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
+ assertNotNull(credential.getCredentialSource());
}
@Test
@@ -175,26 +191,26 @@ public void run() {
@Test
public void exchange3PICredentialForAccessToken() throws IOException {
ExternalAccountCredentials credential =
- ExternalAccountCredentials.fromJson(buildDefaultIdentityPoolCredential(), transportFactory);
+ ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory);
StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
AccessToken accessToken =
credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest);
- assertThat(accessToken.getTokenValue()).isEqualTo(transportFactory.transport.getAccessToken());
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
Map> headers = transportFactory.transport.getRequest().getHeaders();
- assertThat(headers.containsKey("content-type")).isTrue();
- assertThat(headers.get("content-type").get(0)).isEqualTo("application/x-www-form-urlencoded");
+ assertTrue(headers.containsKey("content-type"));
+ assertEquals("application/x-www-form-urlencoded", headers.get("content-type").get(0));
}
@Test
public void exchange3PICredentialForAccessToken_throws() throws IOException {
final ExternalAccountCredentials credential =
- ExternalAccountCredentials.fromJson(buildDefaultIdentityPoolCredential(), transportFactory);
+ ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory);
String errorCode = "invalidRequest";
String errorDescription = "errorDescription";
@@ -203,7 +219,7 @@ public void exchange3PICredentialForAccessToken_throws() throws IOException {
TestUtils.buildHttpResponseException(errorCode, errorDescription, errorUri));
final StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
OAuthException e =
assertThrows(
@@ -215,14 +231,14 @@ public void run() throws Throwable {
}
});
- assertThat(e.getErrorCode()).isEqualTo(errorCode);
- assertThat(e.getErrorDescription()).isEqualTo(errorDescription);
- assertThat(e.getErrorUri()).isEqualTo(errorUri);
+ assertEquals(errorCode, e.getErrorCode());
+ assertEquals(errorDescription, e.getErrorDescription());
+ assertEquals(errorUri, e.getErrorUri());
}
@Test
public void attemptServiceAccountImpersonation() throws IOException {
- GenericJson defaultCredential = buildDefaultIdentityPoolCredential();
+ GenericJson defaultCredential = buildJsonIdentityPoolCredential();
defaultCredential.put(
"service_account_impersonation_url",
transportFactory.transport.getServiceAccountImpersonationUrl());
@@ -235,39 +251,122 @@ public void attemptServiceAccountImpersonation() throws IOException {
AccessToken returnedToken = credential.attemptServiceAccountImpersonation(accessToken);
- assertThat(returnedToken.getTokenValue())
- .isEqualTo(transportFactory.transport.getAccessToken());
- assertThat(returnedToken.getTokenValue()).isNotEqualTo(accessToken.getTokenValue());
+ assertEquals(transportFactory.transport.getAccessToken(), returnedToken.getTokenValue());
+ assertNotEquals(accessToken.getTokenValue(), returnedToken.getTokenValue());
// Validate request content.
MockLowLevelHttpRequest request = transportFactory.transport.getRequest();
Map actualRequestContent = TestUtils.parseQuery(request.getContentAsString());
- GenericData expectedRequestContent = new GenericData().set("scope", CLOUD_PLATFORM_SCOPE);
- assertThat(actualRequestContent).isEqualTo(expectedRequestContent);
+ Map expectedRequestContent = new HashMap<>();
+ expectedRequestContent.put("scope", CLOUD_PLATFORM_SCOPE);
+ assertEquals(expectedRequestContent, actualRequestContent);
}
@Test
public void attemptServiceAccountImpersonation_noUrl() throws IOException {
ExternalAccountCredentials credential =
- ExternalAccountCredentials.fromJson(buildDefaultIdentityPoolCredential(), transportFactory);
+ ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory);
AccessToken accessToken = new AccessToken(ACCESS_TOKEN, new Date());
AccessToken returnedToken = credential.attemptServiceAccountImpersonation(accessToken);
- assertThat(returnedToken).isEqualTo(accessToken);
+ assertEquals(accessToken, returnedToken);
}
- private GenericJson buildDefaultIdentityPoolCredential() {
+ @Test
+ public void getRequestMetadata_withQuotaProjectId() throws IOException {
+ TestExternalAccountCredentials testCredentials =
+ new TestExternalAccountCredentials(
+ transportFactory,
+ "audience",
+ "subjectTokenType",
+ "tokenUrl",
+ "tokenInfoUrl",
+ new TestCredentialSource(new HashMap()),
+ /* serviceAccountImpersonationUrl= */ null,
+ "quotaProjectId",
+ /* clientId= */ null,
+ /* clientSecret= */ null,
+ /* scopes= */ null);
+
+ Map> requestMetadata =
+ testCredentials.getRequestMetadata(URI.create("http://googleapis.com/foo/bar"));
+
+ assertEquals("quotaProjectId", requestMetadata.get("x-goog-user-project").get(0));
+ }
+
+ private GenericJson buildJsonIdentityPoolCredential() {
GenericJson json = new GenericJson();
- json.put("audience", AUDIENCE);
- json.put("subject_token_type", SUBJECT_TOKEN_TYPE);
+ json.put("audience", "audience");
+ json.put("subject_token_type", "subjectTokenType");
json.put("token_url", STS_URL);
- json.put("token_info_url", TOKEN_INFO_URL);
+ json.put("token_info_url", "tokenInfoUrl");
Map map = new HashMap<>();
map.put("file", "file");
json.put("credential_source", map);
return json;
}
+
+ private GenericJson buildJsonAwsCredential() {
+ GenericJson json = new GenericJson();
+ json.put("audience", "audience");
+ json.put("subject_token_type", "subjectTokenType");
+ json.put("token_url", STS_URL);
+ json.put("token_info_url", "tokenInfoUrl");
+
+ Map map = new HashMap<>();
+ map.put("environment_id", "aws1");
+ map.put("region_url", "regionUrl");
+ map.put("url", "url");
+ map.put("regional_cred_verification_url", "regionalCredVerificationUrl");
+ json.put("credential_source", map);
+
+ return json;
+ }
+
+ static class TestExternalAccountCredentials extends ExternalAccountCredentials {
+ static class TestCredentialSource extends ExternalAccountCredentials.CredentialSource {
+ protected TestCredentialSource(Map credentialSourceMap) {
+ super(credentialSourceMap);
+ }
+ }
+
+ protected TestExternalAccountCredentials(
+ HttpTransportFactory transportFactory,
+ String audience,
+ String subjectTokenType,
+ String tokenUrl,
+ String tokenInfoUrl,
+ CredentialSource credentialSource,
+ @Nullable String serviceAccountImpersonationUrl,
+ @Nullable String quotaProjectId,
+ @Nullable String clientId,
+ @Nullable String clientSecret,
+ @Nullable Collection scopes) {
+ super(
+ transportFactory,
+ audience,
+ subjectTokenType,
+ tokenUrl,
+ tokenInfoUrl,
+ credentialSource,
+ serviceAccountImpersonationUrl,
+ quotaProjectId,
+ clientId,
+ clientSecret,
+ scopes);
+ }
+
+ @Override
+ public AccessToken refreshAccessToken() {
+ return new AccessToken("accessToken", new Date());
+ }
+
+ @Override
+ public String retrieveSubjectToken() {
+ return "subjectToken";
+ }
+ }
}
From 6d04b05f053d8e14219d2932c6b5e1f875d80d7a Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Fri, 16 Oct 2020 16:39:56 -0700
Subject: [PATCH 07/23] chore: remove use of Truth assertions (#498)
---
.../auth/oauth2/AwsRequestSignerTest.java | 206 +++++++++---------
...ckExternalAccountCredentialsTransport.java | 23 +-
.../auth/oauth2/OAuthExceptionTest.java | 45 ++--
.../auth/oauth2/StsRequestHandlerTest.java | 111 ++++------
oauth2_http/pom.xml | 6 -
5 files changed, 181 insertions(+), 210 deletions(-)
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java
index 5f22165e9..2e40109ea 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java
@@ -31,7 +31,7 @@
package com.google.auth.oauth2;
-import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
@@ -88,13 +88,13 @@ public void sign_getHost() {
+ "aws4_request, SignedHeaders=date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("GET");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("GET", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.req
@@ -119,13 +119,13 @@ public void sign_getHostRelativePath() {
+ "aws4_request, SignedHeaders=date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("GET");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("GET", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.req
@@ -150,13 +150,13 @@ public void sign_getHostInvalidPath() {
+ "aws4_request, SignedHeaders=date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("GET");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("GET", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.req
@@ -181,13 +181,13 @@ public void sign_getHostDotPath() {
+ "aws4_request, SignedHeaders=date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("GET");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("GET", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.req
@@ -212,13 +212,13 @@ public void sign_getHostUtf8Path() {
+ "aws4_request, SignedHeaders=date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("GET");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("GET", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.req
@@ -243,13 +243,13 @@ public void sign_getHostDuplicateQueryParam() {
+ "aws4_request, SignedHeaders=date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("GET");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("GET", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.req
@@ -277,14 +277,14 @@ public void sign_postWithUpperCaseHeaderKey() {
+ "aws4_request, SignedHeaders=date;host;zoo, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("POST");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
- assertThat(signature.getCanonicalHeaders().get(headerKey.toLowerCase())).isEqualTo(headerValue);
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("POST", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
+ assertEquals(headerValue, signature.getCanonicalHeaders().get(headerKey.toLowerCase()));
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.req
@@ -312,14 +312,14 @@ public void sign_postWithUpperCaseHeaderValue() {
+ "aws4_request, SignedHeaders=date;host;zoo, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("POST");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
- assertThat(signature.getCanonicalHeaders().get(headerKey)).isEqualTo(headerValue);
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("POST", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
+ assertEquals(headerValue, signature.getCanonicalHeaders().get(headerKey.toLowerCase()));
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
@@ -347,14 +347,14 @@ public void sign_postWithHeader() {
+ "aws4_request, SignedHeaders=date;host;p, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("POST");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
- assertThat(signature.getCanonicalHeaders().get(headerKey)).isEqualTo(headerValue);
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("POST", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
+ assertEquals(headerValue, signature.getCanonicalHeaders().get(headerKey.toLowerCase()));
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.req
@@ -383,14 +383,14 @@ public void sign_postWithBodyNoCustomHeaders() {
+ "aws4_request, SignedHeaders=content-type;date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("POST");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
- assertThat(signature.getCanonicalHeaders().get(headerKey.toLowerCase())).isEqualTo(headerValue);
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("POST", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
+ assertEquals(headerValue, signature.getCanonicalHeaders().get(headerKey.toLowerCase()));
}
// https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.req
@@ -415,13 +415,13 @@ public void sign_postWithQueryString() {
+ "aws4_request, SignedHeaders=date;host, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(BOTOCORE_CREDENTIALS);
- assertThat(signature.getDate()).isEqualTo(DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("POST");
- assertThat(signature.getRegion()).isEqualTo("us-east-1");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(BOTOCORE_CREDENTIALS, signature.getSecurityCredentials());
+ assertEquals(DATE, signature.getDate());
+ assertEquals("POST", signature.getHttpMethod());
+ assertEquals("us-east-1", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
@Test
@@ -446,13 +446,13 @@ public void sign_getDescribeRegions() {
+ "aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(awsSecurityCredentials);
- assertThat(signature.getDate()).isEqualTo(X_AMZ_DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("GET");
- assertThat(signature.getRegion()).isEqualTo("us-east-2");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(awsSecurityCredentials, signature.getSecurityCredentials());
+ assertEquals(X_AMZ_DATE, signature.getDate());
+ assertEquals("GET", signature.getHttpMethod());
+ assertEquals("us-east-2", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
@Test
@@ -477,13 +477,13 @@ public void sign_postGetCallerIdentity() {
+ "aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(awsSecurityCredentials);
- assertThat(signature.getDate()).isEqualTo(X_AMZ_DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("POST");
- assertThat(signature.getRegion()).isEqualTo("us-east-2");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(awsSecurityCredentials, signature.getSecurityCredentials());
+ assertEquals(X_AMZ_DATE, signature.getDate());
+ assertEquals("POST", signature.getHttpMethod());
+ assertEquals("us-east-2", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
@Test
@@ -514,13 +514,13 @@ public void sign_postGetCallerIdentityNoToken() {
+ "aws4_request, SignedHeaders=host;x-amz-date, Signature="
+ expectedSignature;
- assertThat(signature.getSignature()).isEqualTo(expectedSignature);
- assertThat(signature.getAuthorizationHeader()).isEqualTo(expectedAuthHeader);
- assertThat(signature.getSecurityCredentials()).isEqualTo(awsSecurityCredentialsWithoutToken);
- assertThat(signature.getDate()).isEqualTo(X_AMZ_DATE);
- assertThat(signature.getHttpMethod()).isEqualTo("POST");
- assertThat(signature.getRegion()).isEqualTo("us-east-2");
- assertThat(signature.getUrl()).isEqualTo(URI.create(url).normalize().toString());
+ assertEquals(expectedSignature, signature.getSignature());
+ assertEquals(expectedAuthHeader, signature.getAuthorizationHeader());
+ assertEquals(awsSecurityCredentialsWithoutToken, signature.getSecurityCredentials());
+ assertEquals(X_AMZ_DATE, signature.getDate());
+ assertEquals("POST", signature.getHttpMethod());
+ assertEquals("us-east-2", signature.getRegion());
+ assertEquals(URI.create(url).normalize().toString(), signature.getUrl());
}
public AwsSecurityCredentials retrieveAwsSecurityCredentials() throws IOException {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index 1c1d6fe41..df205814c 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -31,7 +31,9 @@
package com.google.auth.oauth2;
-import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import com.google.api.client.http.LowLevelHttpRequest;
import com.google.api.client.http.LowLevelHttpResponse;
@@ -73,7 +75,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
private static final String SUBJECT_TOKEN = "subjectToken";
private static final String TOKEN_TYPE = "Bearer";
private static final String ACCESS_TOKEN = "accessToken";
- private static final int EXPIRES_IN = 3600;
+ private static final Long EXPIRES_IN = 3600L;
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
@@ -155,9 +157,9 @@ public LowLevelHttpResponse execute() throws IOException {
}
if (STS_URL.equals(url)) {
Map query = TestUtils.parseQuery(getContentAsString());
- assertThat(query.get("grant_type")).isEqualTo(EXPECTED_GRANT_TYPE);
- assertThat(query.get("subject_token_type")).isNotEmpty();
- assertThat(query.get("subject_token")).isNotEmpty();
+ assertEquals(EXPECTED_GRANT_TYPE, query.get("grant_type"));
+ assertNotNull(query.get("subject_token_type"));
+ assertNotNull(query.get("subject_token"));
GenericJson response = new GenericJson();
response.setFactory(JSON_FACTORY);
@@ -178,11 +180,10 @@ public LowLevelHttpResponse execute() throws IOException {
}
if (SERVICE_ACCOUNT_IMPERSONATION_URL.equals(url)) {
Map query = TestUtils.parseQuery(getContentAsString());
- assertThat(query.get("scope")).isEqualTo(CLOUD_PLATFORM_SCOPE);
- assertThat(getHeaders().containsKey("authorization")).isTrue();
- assertThat(getHeaders().get("authorization")).hasSize(1);
- assertThat(getHeaders().get("authorization")).hasSize(1);
- assertThat(getHeaders().get("authorization").get(0)).isNotEmpty();
+ assertEquals(CLOUD_PLATFORM_SCOPE, query.get("scope"));
+ assertEquals(1, getHeaders().get("authorization").size());
+ assertTrue(getHeaders().containsKey("authorization"));
+ assertNotNull(getHeaders().get("authorization").get(0));
GenericJson response = new GenericJson();
response.setFactory(JSON_FACTORY);
@@ -215,7 +216,7 @@ public String getIssuedTokenType() {
return ISSUED_TOKEN_TYPE;
}
- public int getExpiresIn() {
+ public Long getExpiresIn() {
return EXPIRES_IN;
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
index 10e2af193..e9188e723 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
@@ -31,7 +31,8 @@
package com.google.auth.oauth2;
-import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -40,49 +41,47 @@
/** Tests for {@link OAuthException}. */
@RunWith(JUnit4.class)
public final class OAuthExceptionTest {
+
private static final String FULL_MESSAGE_FORMAT = "Error code %s: %s - %s";
private static final String ERROR_DESCRIPTION_FORMAT = "Error code %s: %s";
private static final String BASE_MESSAGE_FORMAT = "Error code %s";
- private static final String ERROR_CODE = "errorCode";
- private static final String ERROR_DESCRIPTION = "errorDescription";
- private static final String ERROR_URI = "errorUri";
-
@Test
public void getMessage_fullFormat() {
- OAuthException e = new OAuthException(ERROR_CODE, ERROR_DESCRIPTION, ERROR_URI);
+ OAuthException e = new OAuthException("errorCode", "errorDescription", "errorUri");
- assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
- assertThat(e.getErrorDescription()).isEqualTo(ERROR_DESCRIPTION);
- assertThat(e.getErrorUri()).isEqualTo(ERROR_URI);
+ assertEquals("errorCode", e.getErrorCode());
+ assertEquals("errorDescription", e.getErrorDescription());
+ assertEquals("errorUri", e.getErrorUri());
String expectedMessage =
- String.format(FULL_MESSAGE_FORMAT, ERROR_CODE, ERROR_DESCRIPTION, ERROR_URI);
- assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ String.format(FULL_MESSAGE_FORMAT, "errorCode", "errorDescription", "errorUri");
+ assertEquals(expectedMessage, e.getMessage());
}
@Test
public void getMessage_descriptionFormat() {
- OAuthException e = new OAuthException(ERROR_CODE, ERROR_DESCRIPTION, /* errorUri= */ null);
+ OAuthException e = new OAuthException("errorCode", "errorDescription", /* errorUri= */ null);
- assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
- assertThat(e.getErrorDescription()).isEqualTo(ERROR_DESCRIPTION);
- assertThat(e.getErrorUri()).isNull();
+ assertEquals("errorCode", e.getErrorCode());
+ assertEquals("errorDescription", e.getErrorDescription());
+ assertNull(e.getErrorUri());
- String expectedMessage = String.format(ERROR_DESCRIPTION_FORMAT, ERROR_CODE, ERROR_DESCRIPTION);
- assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ String expectedMessage =
+ String.format(ERROR_DESCRIPTION_FORMAT, "errorCode", "errorDescription");
+ assertEquals(expectedMessage, e.getMessage());
}
@Test
public void getMessage_baseFormat() {
OAuthException e =
- new OAuthException(ERROR_CODE, /* errorDescription= */ null, /* errorUri= */ null);
+ new OAuthException("errorCode", /* errorDescription= */ null, /* errorUri= */ null);
- assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
- assertThat(e.getErrorDescription()).isNull();
- assertThat(e.getErrorUri()).isNull();
+ assertEquals("errorCode", e.getErrorCode());
+ assertNull(e.getErrorDescription());
+ assertNull(e.getErrorUri());
- String expectedMessage = String.format(BASE_MESSAGE_FORMAT, ERROR_CODE);
- assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ String expectedMessage = String.format(BASE_MESSAGE_FORMAT, "errorCode");
+ assertEquals(expectedMessage, e.getMessage());
}
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
index 5de1d182c..175dff042 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java
@@ -31,7 +31,8 @@
package com.google.auth.oauth2;
-import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import com.google.api.client.http.HttpHeaders;
@@ -61,30 +62,6 @@ public final class StsRequestHandlerTest {
private static final String DEFAULT_REQUESTED_TOKEN_TYPE =
"urn:ietf:params:oauth:token-type:access_token";
private static final String TOKEN_URL = "https://www.sts.google.com";
- private static final String CREDENTIAL = "credential";
- private static final String SUBJECT_TOKEN_TYPE = "subjectTokenType";
-
- // Optional params.
- private static final String AUDIENCE = "audience";
- private static final String RESOURCE = "resource";
- private static final String ACTOR_TOKEN = "actorToken";
- private static final String ACTOR_TOKEN_TYPE = "actorTokenType";
- private static final String REQUESTED_TOKEN_TYPE = "requestedTokenType";
- private static final String INTERNAL_OPTIONS = "internalOptions";
- private static final String REFRESH_TOKEN = "refreshToken";
- private static final List SCOPES = Arrays.asList("scope1", "scope2", "scope3");
-
- // Headers.
- private static final String CONTENT_TYPE_KEY = "content-type";
- private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
- private static final String ACCEPT_ENCODING_KEY = "accept-encoding";
- private static final String ACCEPT_ENCODING = "gzip";
- private static final String CUSTOM_HEADER_KEY = "custom_header_key";
- private static final String CUSTOM_HEADER_VALUE = "custom_header_value";
-
- private static final String INVALID_REQUEST = "invalid_request";
- private static final String ERROR_DESCRIPTION = "errorDescription";
- private static final String ERROR_URI = "errorUri";
private MockExternalAccountCredentialsTransport transport;
@@ -96,7 +73,7 @@ public void setup() {
@Test
public void exchangeToken() throws IOException {
StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE)
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType")
.setScopes(Arrays.asList(CLOUD_PLATFORM_SCOPE))
.build();
@@ -108,10 +85,10 @@ public void exchangeToken() throws IOException {
StsTokenExchangeResponse response = requestHandler.exchangeToken();
// Validate response.
- assertThat(response.getAccessToken().getTokenValue()).isEqualTo(transport.getAccessToken());
- assertThat(response.getTokenType()).isEqualTo(transport.getTokenType());
- assertThat(response.getIssuedTokenType()).isEqualTo(transport.getIssuedTokenType());
- assertThat(response.getExpiresIn()).isEqualTo(transport.getExpiresIn());
+ assertEquals(transport.getAccessToken(), response.getAccessToken().getTokenValue());
+ assertEquals(transport.getTokenType(), response.getTokenType());
+ assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType());
+ assertEquals(transport.getExpiresIn(), response.getExpiresIn());
// Validate request content.
GenericData expectedRequestContent =
@@ -124,61 +101,61 @@ public void exchangeToken() throws IOException {
MockLowLevelHttpRequest request = transport.getRequest();
Map actualRequestContent = TestUtils.parseQuery(request.getContentAsString());
- assertThat(actualRequestContent).isEqualTo(expectedRequestContent);
+ assertEquals(expectedRequestContent.getUnknownKeys(), actualRequestContent);
}
@Test
public void exchangeToken_withOptionalParams() throws IOException {
// Return optional params scope and the refresh_token.
- transport.addScopeSequence(SCOPES);
- transport.addRefreshTokenSequence(REFRESH_TOKEN);
+ transport.addScopeSequence(Arrays.asList("scope1", "scope2", "scope3"));
+ transport.addRefreshTokenSequence("refreshToken");
// Build the token exchange request.
StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE)
- .setAudience(AUDIENCE)
- .setResource(RESOURCE)
- .setActingParty(new ActingParty(ACTOR_TOKEN, ACTOR_TOKEN_TYPE))
- .setRequestTokenType(REQUESTED_TOKEN_TYPE)
- .setScopes(SCOPES)
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType")
+ .setAudience("audience")
+ .setResource("resource")
+ .setActingParty(new ActingParty("actorToken", "actorTokenType"))
+ .setRequestTokenType("requestedTokenType")
+ .setScopes(Arrays.asList("scope1", "scope2", "scope3"))
.build();
HttpHeaders httpHeaders =
new HttpHeaders()
- .setContentType(CONTENT_TYPE)
- .setAcceptEncoding(ACCEPT_ENCODING)
- .set(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE);
+ .setContentType("application/x-www-form-urlencoded")
+ .setAcceptEncoding("gzip")
+ .set("custom_header_key", "custom_header_value");
StsRequestHandler requestHandler =
StsRequestHandler.newBuilder(
TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory())
.setHeaders(httpHeaders)
- .setInternalOptions(INTERNAL_OPTIONS)
+ .setInternalOptions("internalOptions")
.build();
StsTokenExchangeResponse response = requestHandler.exchangeToken();
// Validate response.
- assertThat(response.getAccessToken().getTokenValue()).isEqualTo(transport.getAccessToken());
- assertThat(response.getTokenType()).isEqualTo(transport.getTokenType());
- assertThat(response.getIssuedTokenType()).isEqualTo(transport.getIssuedTokenType());
- assertThat(response.getExpiresIn()).isEqualTo(transport.getExpiresIn());
- assertThat(response.getScopes()).isEqualTo(SCOPES);
- assertThat(response.getRefreshToken()).isEqualTo(REFRESH_TOKEN);
+ assertEquals(transport.getAccessToken(), response.getAccessToken().getTokenValue());
+ assertEquals(transport.getTokenType(), response.getTokenType());
+ assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType());
+ assertEquals(transport.getExpiresIn(), response.getExpiresIn());
+ assertEquals(Arrays.asList("scope1", "scope2", "scope3"), response.getScopes());
+ assertEquals("refreshToken", response.getRefreshToken());
// Validate headers.
MockLowLevelHttpRequest request = transport.getRequest();
Map> requestHeaders = request.getHeaders();
- assertThat(requestHeaders.get(CONTENT_TYPE_KEY).get(0)).isEqualTo(CONTENT_TYPE);
- assertThat(requestHeaders.get(ACCEPT_ENCODING_KEY).get(0)).isEqualTo(ACCEPT_ENCODING);
- assertThat(requestHeaders.get(CUSTOM_HEADER_KEY).get(0)).isEqualTo(CUSTOM_HEADER_VALUE);
+ assertEquals("application/x-www-form-urlencoded", requestHeaders.get("content-type").get(0));
+ assertEquals("gzip", requestHeaders.get("accept-encoding").get(0));
+ assertEquals("custom_header_value", requestHeaders.get("custom_header_key").get(0));
// Validate request content.
GenericData expectedRequestContent =
new GenericData()
.set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE)
- .set("scope", Joiner.on(' ').join(SCOPES))
- .set("options", INTERNAL_OPTIONS)
+ .set("scope", Joiner.on(' ').join(Arrays.asList("scope1", "scope2", "scope3")))
+ .set("options", "internalOptions")
.set("subject_token_type", stsTokenExchangeRequest.getSubjectTokenType())
.set("subject_token", stsTokenExchangeRequest.getSubjectToken())
.set("requested_token_type", stsTokenExchangeRequest.getRequestedTokenType())
@@ -188,13 +165,13 @@ public void exchangeToken_withOptionalParams() throws IOException {
.set("audience", stsTokenExchangeRequest.getAudience());
Map actualRequestContent = TestUtils.parseQuery(request.getContentAsString());
- assertThat(actualRequestContent).isEqualTo(expectedRequestContent);
+ assertEquals(expectedRequestContent.getUnknownKeys(), actualRequestContent);
}
@Test
public void exchangeToken_throwsException() throws IOException {
StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
final StsRequestHandler requestHandler =
StsRequestHandler.newBuilder(
@@ -203,7 +180,7 @@ public void exchangeToken_throwsException() throws IOException {
transport.addResponseErrorSequence(
TestUtils.buildHttpResponseException(
- INVALID_REQUEST, /* errorDescription= */ null, /* errorUri= */ null));
+ "invalidRequest", /* errorDescription= */ null, /* errorUri= */ null));
OAuthException e =
assertThrows(
@@ -215,15 +192,15 @@ public void run() throws Throwable {
}
});
- assertThat(e.getErrorCode()).isEqualTo(INVALID_REQUEST);
- assertThat(e.getErrorDescription()).isNull();
- assertThat(e.getErrorUri()).isNull();
+ assertEquals("invalidRequest", e.getErrorCode());
+ assertNull(e.getErrorDescription());
+ assertNull(e.getErrorUri());
}
@Test
public void exchangeToken_withOptionalParams_throwsException() throws IOException {
StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
final StsRequestHandler requestHandler =
StsRequestHandler.newBuilder(
@@ -231,7 +208,7 @@ public void exchangeToken_withOptionalParams_throwsException() throws IOExceptio
.build();
transport.addResponseErrorSequence(
- TestUtils.buildHttpResponseException(INVALID_REQUEST, ERROR_DESCRIPTION, ERROR_URI));
+ TestUtils.buildHttpResponseException("invalidRequest", "errorDescription", "errorUri"));
OAuthException e =
assertThrows(
@@ -243,15 +220,15 @@ public void run() throws Throwable {
}
});
- assertThat(e.getErrorCode()).isEqualTo(INVALID_REQUEST);
- assertThat(e.getErrorDescription()).isEqualTo(ERROR_DESCRIPTION);
- assertThat(e.getErrorUri()).isEqualTo(ERROR_URI);
+ assertEquals("invalidRequest", e.getErrorCode());
+ assertEquals("errorDescription", e.getErrorDescription());
+ assertEquals("errorUri", e.getErrorUri());
}
@Test
public void exchangeToken_ioException() {
StsTokenExchangeRequest stsTokenExchangeRequest =
- StsTokenExchangeRequest.newBuilder(CREDENTIAL, SUBJECT_TOKEN_TYPE).build();
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
final StsRequestHandler requestHandler =
StsRequestHandler.newBuilder(
@@ -270,6 +247,6 @@ public void run() throws Throwable {
requestHandler.exchangeToken();
}
});
- assertThat(thrownException).isEqualTo(e);
+ assertEquals(e, thrownException);
}
}
diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml
index 468e551aa..92c326638 100644
--- a/oauth2_http/pom.xml
+++ b/oauth2_http/pom.xml
@@ -91,12 +91,6 @@
junit
test
-
- com.google.truth
- truth
- 1.0.1
- test
-
From 124c77c0fc4ffd6bfa26654ce32ea27b5e05d82c Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Wed, 21 Oct 2020 09:35:01 -0700
Subject: [PATCH 08/23] feat: add external account credentials to ADC (#500)
---
.../google/auth/oauth2/GoogleCredentials.java | 7 +++-
.../auth/oauth2/AwsCredentialsTest.java | 27 +++++++++++--
.../auth/oauth2/GoogleCredentialsTest.java | 38 +++++++++++++++++++
.../oauth2/IdentityPoolCredentialsTest.java | 26 +++++++++++--
...ckExternalAccountCredentialsTransport.java | 4 +-
5 files changed, 92 insertions(+), 10 deletions(-)
diff --git a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
index c32e72f47..07b2d3d04 100644
--- a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
@@ -31,6 +31,8 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.IdentityPoolCredentials.EXTERNAL_ACCOUNT_FILE_TYPE;
+
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
@@ -49,8 +51,8 @@
public class GoogleCredentials extends OAuth2Credentials {
private static final long serialVersionUID = -1522852442442473691L;
- static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project";
+ static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project";
static final String USER_FILE_TYPE = "authorized_user";
static final String SERVICE_ACCOUNT_FILE_TYPE = "service_account";
@@ -165,6 +167,9 @@ public static GoogleCredentials fromStream(
if (SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) {
return ServiceAccountCredentials.fromJson(fileContents, transportFactory);
}
+ if (EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) {
+ return ExternalAccountCredentials.fromJson(fileContents, transportFactory);
+ }
throw new IOException(
String.format(
"Error reading credentials from stream, 'type' value '%s' not recognized."
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
index 7fd708bf6..8cee102a6 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
@@ -40,10 +40,12 @@
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonParser;
+import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource;
import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
@@ -315,15 +317,34 @@ public void createdScoped_clonedCredentialWithAddedScopes() {
assertEquals(newScopes, newCredentials.getScopes());
}
- private AwsCredentialSource buildAwsCredentialSource(
+ private static AwsCredentialSource buildAwsCredentialSource(
MockExternalAccountCredentialsTransportFactory transportFactory) {
Map credentialSourceMap = new HashMap<>();
- credentialSourceMap.put("region_url", transportFactory.transport.getAwsRegionEndpoint());
- credentialSourceMap.put("url", transportFactory.transport.getAwsCredentialsEndpoint());
+ credentialSourceMap.put("region_url", transportFactory.transport.getAwsRegionUrl());
+ credentialSourceMap.put("url", transportFactory.transport.getAwsCredentialsUrl());
credentialSourceMap.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL);
return new AwsCredentialSource(credentialSourceMap);
}
+ static InputStream writeAwsCredentialsStream(String stsUrl, String regionUrl, String metadataUrl)
+ throws IOException {
+ GenericJson json = new GenericJson();
+ json.put("audience", "audience");
+ json.put("subject_token_type", "subjectTokenType");
+ json.put("token_url", stsUrl);
+ json.put("token_info_url", "tokenInfoUrl");
+ json.put("type", ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE);
+
+ GenericJson credentialSource = new GenericJson();
+ credentialSource.put("environment_id", "aws1");
+ credentialSource.put("region_url", regionUrl);
+ credentialSource.put("url", metadataUrl);
+ credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL);
+ json.put("credential_source", credentialSource);
+
+ return TestUtils.jsonToInputStream(json);
+ }
+
/** Used to test the retrieval of AWS credentials from environment variables. */
private static class TestAwsCredentials extends AwsCredentials {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
index ebf60a8ce..d48ab94e4 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
@@ -40,6 +40,7 @@
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.IdentityPoolCredentialsTest.MockExternalAccountCredentialsTransportFactory;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -224,6 +225,43 @@ public void fromStream_userNoRefreshToken_throws() throws IOException {
testFromStreamException(userStream, "refresh_token");
}
+ @Test
+ public void fromStream_identityPoolCredentials_providesToken() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+ InputStream identityPoolCredentialStream =
+ IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream(
+ transportFactory.transport.getStsUrl(), transportFactory.transport.getMetadataUrl());
+
+ GoogleCredentials credentials =
+ GoogleCredentials.fromStream(identityPoolCredentialStream, transportFactory);
+
+ assertNotNull(credentials);
+ credentials = credentials.createScoped(SCOPES);
+ Map> metadata = credentials.getRequestMetadata(CALL_URI);
+ TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken());
+ }
+
+ @Test
+ public void fromStream_awsCredentials_providesToken() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ InputStream awsCredentialStream =
+ AwsCredentialsTest.writeAwsCredentialsStream(
+ transportFactory.transport.getStsUrl(),
+ transportFactory.transport.getAwsRegionUrl(),
+ transportFactory.transport.getAwsCredentialsUrl());
+
+ GoogleCredentials credentials =
+ GoogleCredentials.fromStream(awsCredentialStream, transportFactory);
+
+ assertNotNull(credentials);
+ credentials = credentials.createScoped(SCOPES);
+ Map> metadata = credentials.getRequestMetadata(CALL_URI);
+ TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken());
+ }
+
@Test
public void createScoped_overloadCallsImplementation() {
final AtomicReference> called = new AtomicReference<>();
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
index 84bedcfaf..1d9f56d14 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -39,11 +39,13 @@
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
+import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -415,14 +417,30 @@ public void run() {
e.getMessage());
}
- @Test
- public void identityPoolCredentialSource_jsonFormatTypeWithoutSubjectTokenFieldName() {}
+ static InputStream writeIdentityPoolCredentialsStream(String tokenUrl, String url)
+ throws IOException {
+ GenericJson json = new GenericJson();
+ json.put("audience", "audience");
+ json.put("subject_token_type", "subjectTokenType");
+ json.put("token_url", tokenUrl);
+ json.put("token_info_url", "tokenInfoUrl");
+ json.put("type", ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE);
+
+ GenericJson credentialSource = new GenericJson();
+ GenericJson headers = new GenericJson();
+ headers.put("Metadata-Flavor", "Google");
+ credentialSource.put("url", url);
+ credentialSource.put("headers", headers);
+
+ json.put("credential_source", credentialSource);
+ return TestUtils.jsonToInputStream(json);
+ }
- private IdentityPoolCredentialSource buildUrlBasedCredentialSource(String url) {
+ private static IdentityPoolCredentialSource buildUrlBasedCredentialSource(String url) {
return buildUrlBasedCredentialSource(url, /* formatMap= */ null);
}
- private IdentityPoolCredentialSource buildUrlBasedCredentialSource(
+ private static IdentityPoolCredentialSource buildUrlBasedCredentialSource(
String url, Map formatMap) {
Map credentialSourceMap = new HashMap<>();
Map headers = new HashMap<>();
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index df205814c..9860acf0b 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -228,11 +228,11 @@ public String getMetadataUrl() {
return METADATA_SERVER_URL;
}
- public String getAwsCredentialsEndpoint() {
+ public String getAwsCredentialsUrl() {
return AWS_CREDENTIALS_URL;
}
- public String getAwsRegionEndpoint() {
+ public String getAwsRegionUrl() {
return AWS_REGION_URL;
}
From 17e849e46cdcf62329a983489398e08af6d42f2d Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Thu, 29 Oct 2020 15:02:44 -0700
Subject: [PATCH 09/23] chore: use ImpersonatedCredentials for service account
impersonation for 3pi (#501)
* chore: use ImpersonatedCredentials for service account impersonation in ExternalAccountCredentials
* chore: add test for invalid service account impersonation url
---
.../google/auth/oauth2/AwsCredentials.java | 3 +-
.../oauth2/ExternalAccountCredentials.java | 103 ++++++++----------
.../auth/oauth2/IdentityPoolCredentials.java | 3 +-
.../auth/oauth2/AwsCredentialsTest.java | 8 +-
.../ExternalAccountCredentialsTest.java | 85 +++++++--------
.../auth/oauth2/GoogleCredentialsTest.java | 4 +-
.../oauth2/IdentityPoolCredentialsTest.java | 46 +++++---
...ckExternalAccountCredentialsTransport.java | 21 +++-
8 files changed, 142 insertions(+), 131 deletions(-)
diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
index e26246183..5c9e7b498 100644
--- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
@@ -138,8 +138,7 @@ public AccessToken refreshAccessToken() throws IOException {
stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
}
- AccessToken accessToken = exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build());
- return attemptServiceAccountImpersonation(accessToken);
+ return exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build());
}
@Override
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index 6367069d2..23293bfb9 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -34,27 +34,18 @@
import static com.google.api.client.util.Preconditions.checkNotNull;
import static com.google.common.base.MoreObjects.firstNonNull;
-import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
-import com.google.api.client.http.HttpRequest;
-import com.google.api.client.http.HttpResponse;
-import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonObjectParser;
-import com.google.api.client.util.GenericData;
-import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -77,7 +68,6 @@ protected CredentialSource(Map credentialSourceMap) {
}
}
- private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
@@ -88,16 +78,18 @@ protected CredentialSource(Map credentialSourceMap) {
protected final String subjectTokenType;
protected final String tokenUrl;
protected final String tokenInfoUrl;
- protected final String serviceAccountImpersonationUrl;
protected final CredentialSource credentialSource;
protected final Collection scopes;
+ @Nullable protected final String serviceAccountImpersonationUrl;
@Nullable protected final String quotaProjectId;
@Nullable protected final String clientId;
@Nullable protected final String clientSecret;
protected transient HttpTransportFactory transportFactory;
+ @Nullable protected final ImpersonatedCredentials impersonatedCredentials;
+
/**
* Constructor with minimum identifying information and custom HTTP transport.
*
@@ -148,6 +140,35 @@ protected ExternalAccountCredentials(
this.clientSecret = clientSecret;
this.scopes =
(scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
+ this.impersonatedCredentials = initializeImpersonatedCredentials();
+ }
+
+ private ImpersonatedCredentials initializeImpersonatedCredentials() {
+ if (serviceAccountImpersonationUrl == null) {
+ return null;
+ }
+ // Create a copy of this instance without service account impersonation.
+ ExternalAccountCredentials sourceCredentials;
+ if (this instanceof AwsCredentials) {
+ sourceCredentials =
+ AwsCredentials.newBuilder((AwsCredentials) this)
+ .setServiceAccountImpersonationUrl(null)
+ .build();
+ } else {
+ sourceCredentials =
+ IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) this)
+ .setServiceAccountImpersonationUrl(null)
+ .build();
+ }
+
+ String targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
+ return ImpersonatedCredentials.newBuilder()
+ .setSourceCredentials(sourceCredentials)
+ .setHttpTransportFactory(transportFactory)
+ .setTargetPrincipal(targetPrincipal)
+ .setScopes(new ArrayList<>(scopes))
+ .setLifetime(3600) // 1 hour in seconds
+ .build();
}
@Override
@@ -262,6 +283,10 @@ private static boolean isAwsCredential(Map credentialSource) {
*/
protected AccessToken exchange3PICredentialForAccessToken(
StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException {
+ // Handle service account impersonation if necessary.
+ if (impersonatedCredentials != null) {
+ return impersonatedCredentials.refreshAccessToken();
+ }
StsRequestHandler requestHandler =
StsRequestHandler.newBuilder(
@@ -273,52 +298,16 @@ protected AccessToken exchange3PICredentialForAccessToken(
return response.getAccessToken();
}
- /**
- * Attempts service account impersonation.
- *
- * @param accessToken the access token to be included in the request.
- * @return the access token returned by the generateAccessToken call.
- * @throws IOException if the service account impersonation call fails.
- */
- protected AccessToken attemptServiceAccountImpersonation(AccessToken accessToken)
- throws IOException {
- if (serviceAccountImpersonationUrl == null) {
- return accessToken;
- }
-
- HttpRequest request =
- transportFactory
- .create()
- .createRequestFactory()
- .buildPostRequest(
- new GenericUrl(serviceAccountImpersonationUrl),
- new UrlEncodedContent(new GenericData().set("scope", scopes.toArray())));
- request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
- request.setHeaders(
- new HttpHeaders()
- .setAuthorization(
- String.format("%s %s", AuthHttpConstants.BEARER, accessToken.getTokenValue())));
-
- HttpResponse response;
- try {
- response = request.execute();
- } catch (IOException e) {
- throw new IOException(
- String.format("Error getting access token for service account: %s", e.getMessage()), e);
- }
+ private static String extractTargetPrincipal(String serviceAccountImpersonationUrl) {
+ // Extract the target principle.
+ int startIndex = serviceAccountImpersonationUrl.lastIndexOf('/');
+ int endIndex = serviceAccountImpersonationUrl.indexOf(":generateAccessToken");
- GenericData responseData = response.parseAs(GenericData.class);
- String token =
- OAuth2Utils.validateString(responseData, "accessToken", "Expected to find an accessToken");
-
- DateFormat format = new SimpleDateFormat(RFC3339);
- String expireTime =
- OAuth2Utils.validateString(responseData, "expireTime", "Expected to find an expireTime");
- try {
- Date date = format.parse(expireTime);
- return new AccessToken(token, date);
- } catch (ParseException e) {
- throw new IOException("Error parsing expireTime: " + e.getMessage());
+ if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) {
+ return serviceAccountImpersonationUrl.substring(startIndex + 1, endIndex);
+ } else {
+ throw new IllegalArgumentException(
+ "Unable to determine target principal from service account impersonation URL.");
}
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
index 75b96353e..523989e33 100644
--- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
@@ -193,8 +193,7 @@ public AccessToken refreshAccessToken() throws IOException {
stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
}
- AccessToken accessToken = exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build());
- return attemptServiceAccountImpersonation(accessToken);
+ return exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build());
}
@Override
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
index 8cee102a6..9abd622b5 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
@@ -65,6 +65,9 @@ public class AwsCredentialsTest {
private static final String GET_CALLER_IDENTITY_URL =
"https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15";
+ private static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
+ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testn@test.iam.gserviceaccount.com:generateAccessToken";
+
private static final Map AWS_CREDENTIAL_SOURCE_MAP =
new HashMap() {
{
@@ -125,7 +128,8 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept
AccessToken accessToken = awsCredential.refreshAccessToken();
- assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+ assertEquals(
+ transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());
}
@Test
@@ -293,7 +297,7 @@ public void createdScoped_clonedCredentialWithAddedScopes() {
AwsCredentials credentials =
(AwsCredentials)
AwsCredentials.newBuilder(AWS_CREDENTIAL)
- .setServiceAccountImpersonationUrl("tokenInfoUrl")
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
.setClientSecret("clientSecret")
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
index a4ea7b72e..22fd568df 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
@@ -33,14 +33,12 @@
import static com.google.auth.TestUtils.getDefaultExpireTime;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
-import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.ExternalAccountCredentialsTest.TestExternalAccountCredentials.TestCredentialSource;
@@ -64,9 +62,6 @@
public class ExternalAccountCredentialsTest {
private static final String STS_URL = "https://www.sts.google.com";
- private static final String ACCESS_TOKEN = "eya23tfgdfga2123as";
- private static final String CLOUD_PLATFORM_SCOPE =
- "https://www.googleapis.com/auth/cloud-platform";
static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
@@ -175,6 +170,25 @@ public void run() {
});
}
+ @Test
+ public void fromJson_invalidServiceAccountImpersonationUrl_throws() {
+ final GenericJson json = buildJsonIdentityPoolCredential();
+ json.put("service_account_impersonation_url", "invalid_url");
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ new ThrowingRunnable() {
+ @Override
+ public void run() {
+ ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ }
+ });
+ assertEquals(
+ "Unable to determine target principal from service account impersonation URL.",
+ e.getMessage());
+ }
+
@Test
public void fromJson_nullTransport_throws() {
assertThrows(
@@ -207,6 +221,29 @@ public void exchange3PICredentialForAccessToken() throws IOException {
assertEquals("application/x-www-form-urlencoded", headers.get("content-type").get(0));
}
+ @Test
+ public void exchange3PICredentialForAccessToken_withServiceAccountImpersonation()
+ throws IOException {
+ transportFactory.transport.setExpireTime(getDefaultExpireTime());
+
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromStream(
+ IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream(
+ transportFactory.transport.getStsUrl(),
+ transportFactory.transport.getMetadataUrl(),
+ transportFactory.transport.getServiceAccountImpersonationUrl()),
+ transportFactory);
+
+ StsTokenExchangeRequest stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
+
+ AccessToken returnedToken =
+ credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest);
+
+ assertEquals(
+ transportFactory.transport.getServiceAccountAccessToken(), returnedToken.getTokenValue());
+ }
+
@Test
public void exchange3PICredentialForAccessToken_throws() throws IOException {
final ExternalAccountCredentials credential =
@@ -236,44 +273,6 @@ public void run() throws Throwable {
assertEquals(errorUri, e.getErrorUri());
}
- @Test
- public void attemptServiceAccountImpersonation() throws IOException {
- GenericJson defaultCredential = buildJsonIdentityPoolCredential();
- defaultCredential.put(
- "service_account_impersonation_url",
- transportFactory.transport.getServiceAccountImpersonationUrl());
-
- ExternalAccountCredentials credential =
- ExternalAccountCredentials.fromJson(defaultCredential, transportFactory);
-
- transportFactory.transport.setExpireTime(getDefaultExpireTime());
- AccessToken accessToken = new AccessToken(ACCESS_TOKEN, new Date());
-
- AccessToken returnedToken = credential.attemptServiceAccountImpersonation(accessToken);
-
- assertEquals(transportFactory.transport.getAccessToken(), returnedToken.getTokenValue());
- assertNotEquals(accessToken.getTokenValue(), returnedToken.getTokenValue());
-
- // Validate request content.
- MockLowLevelHttpRequest request = transportFactory.transport.getRequest();
- Map actualRequestContent = TestUtils.parseQuery(request.getContentAsString());
-
- Map expectedRequestContent = new HashMap<>();
- expectedRequestContent.put("scope", CLOUD_PLATFORM_SCOPE);
- assertEquals(expectedRequestContent, actualRequestContent);
- }
-
- @Test
- public void attemptServiceAccountImpersonation_noUrl() throws IOException {
- ExternalAccountCredentials credential =
- ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory);
-
- AccessToken accessToken = new AccessToken(ACCESS_TOKEN, new Date());
- AccessToken returnedToken = credential.attemptServiceAccountImpersonation(accessToken);
-
- assertEquals(accessToken, returnedToken);
- }
-
@Test
public void getRequestMetadata_withQuotaProjectId() throws IOException {
TestExternalAccountCredentials testCredentials =
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
index d48ab94e4..6af637284 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
@@ -231,7 +231,9 @@ public void fromStream_identityPoolCredentials_providesToken() throws IOExceptio
new MockExternalAccountCredentialsTransportFactory();
InputStream identityPoolCredentialStream =
IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream(
- transportFactory.transport.getStsUrl(), transportFactory.transport.getMetadataUrl());
+ transportFactory.transport.getStsUrl(),
+ transportFactory.transport.getMetadataUrl(),
+ /* serviceAccountImpersonationUrl= */ null);
GoogleCredentials credentials =
GoogleCredentials.fromStream(identityPoolCredentialStream, transportFactory);
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
index 1d9f56d14..3ce9fdc18 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -32,6 +32,7 @@
package com.google.auth.oauth2;
import static com.google.auth.TestUtils.getDefaultExpireTime;
+import static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL;
import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
import static com.google.auth.oauth2.OAuth2Utils.UTF_8;
import static org.junit.Assert.assertEquals;
@@ -50,6 +51,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.junit.runner.RunWith;
@@ -93,30 +95,32 @@ public HttpTransport create() {
@Test
public void createdScoped_clonedCredentialWithAddedScopes() {
- GoogleCredentials credentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
- .setServiceAccountImpersonationUrl("serviceAccountImpersonationUrl")
- .setQuotaProjectId("quotaProjectId")
- .setClientId("clientId")
- .setClientSecret("clientSecret")
- .build();
+ IdentityPoolCredentials credentials =
+ (IdentityPoolCredentials)
+ IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
+ .setQuotaProjectId("quotaProjectId")
+ .setClientId("clientId")
+ .setClientSecret("clientSecret")
+ .build();
List newScopes = Arrays.asList("scope1", "scope2");
IdentityPoolCredentials newCredentials =
(IdentityPoolCredentials) credentials.createScoped(newScopes);
- assertEquals("audience", newCredentials.getAudience());
- assertEquals("subjectTokenType", newCredentials.getSubjectTokenType());
- assertEquals("tokenUrl", newCredentials.getTokenUrl());
- assertEquals("tokenInfoUrl", newCredentials.getTokenInfoUrl());
+ assertEquals(credentials.getAudience(), newCredentials.getAudience());
+ assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType());
+ assertEquals(credentials.getTokenUrl(), newCredentials.getTokenUrl());
+ assertEquals(credentials.getTokenInfoUrl(), newCredentials.getTokenInfoUrl());
assertEquals(
- "serviceAccountImpersonationUrl", newCredentials.getServiceAccountImpersonationUrl());
- assertEquals(FILE_CREDENTIAL_SOURCE, newCredentials.getCredentialSource());
+ credentials.getServiceAccountImpersonationUrl(),
+ newCredentials.getServiceAccountImpersonationUrl());
+ assertEquals(credentials.getCredentialSource(), newCredentials.getCredentialSource());
assertEquals(newScopes, newCredentials.getScopes());
- assertEquals("quotaProjectId", newCredentials.getQuotaProjectId());
- assertEquals("clientId", newCredentials.getClientId());
- assertEquals("clientSecret", newCredentials.getClientSecret());
+ assertEquals(credentials.getQuotaProjectId(), newCredentials.getQuotaProjectId());
+ assertEquals(credentials.getClientId(), newCredentials.getClientId());
+ assertEquals(credentials.getClientSecret(), newCredentials.getClientSecret());
}
@Test
@@ -329,7 +333,8 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept
AccessToken accessToken = credential.refreshAccessToken();
- assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+ assertEquals(
+ transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());
}
@Test
@@ -417,7 +422,8 @@ public void run() {
e.getMessage());
}
- static InputStream writeIdentityPoolCredentialsStream(String tokenUrl, String url)
+ static InputStream writeIdentityPoolCredentialsStream(
+ String tokenUrl, String url, @Nullable String serviceAccountImpersonationUrl)
throws IOException {
GenericJson json = new GenericJson();
json.put("audience", "audience");
@@ -426,6 +432,10 @@ static InputStream writeIdentityPoolCredentialsStream(String tokenUrl, String ur
json.put("token_info_url", "tokenInfoUrl");
json.put("type", ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE);
+ if (serviceAccountImpersonationUrl != null) {
+ json.put("service_account_impersonation_url", serviceAccountImpersonationUrl);
+ }
+
GenericJson credentialSource = new GenericJson();
GenericJson headers = new GenericJson();
headers.put("Metadata-Flavor", "Google");
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index 9860acf0b..c9dff795f 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -48,6 +48,7 @@
import com.google.auth.TestUtils;
import java.io.IOException;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -64,21 +65,22 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
-
private static final String AWS_CREDENTIALS_URL = "https://www.aws-credentials.com";
private static final String AWS_REGION_URL = "https://www.aws-region.com";
private static final String METADATA_SERVER_URL = "https://www.metadata.google.com";
private static final String STS_URL = "https://www.sts.google.com";
- private static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
- "https://iamcredentials.googleapis.com";
private static final String SUBJECT_TOKEN = "subjectToken";
private static final String TOKEN_TYPE = "Bearer";
private static final String ACCESS_TOKEN = "accessToken";
+ private static final String SERVICE_ACCOUNT_ACCESS_TOKEN = "serviceAccountAccessToken";
private static final Long EXPIRES_IN = 3600L;
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
+ static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
+ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testn@test.iam.gserviceaccount.com:generateAccessToken";
+
private Queue responseSequence = new ArrayDeque<>();
private Queue responseErrorSequence = new ArrayDeque<>();
private Queue refreshTokenSequence = new ArrayDeque<>();
@@ -179,15 +181,18 @@ public LowLevelHttpResponse execute() throws IOException {
.setContent(response.toPrettyString());
}
if (SERVICE_ACCOUNT_IMPERSONATION_URL.equals(url)) {
- Map query = TestUtils.parseQuery(getContentAsString());
- assertEquals(CLOUD_PLATFORM_SCOPE, query.get("scope"));
+ GenericJson query =
+ OAuth2Utils.JSON_FACTORY
+ .createJsonParser(getContentAsString())
+ .parseAndClose(GenericJson.class);
+ assertEquals(CLOUD_PLATFORM_SCOPE, ((ArrayList) query.get("scope")).get(0));
assertEquals(1, getHeaders().get("authorization").size());
assertTrue(getHeaders().containsKey("authorization"));
assertNotNull(getHeaders().get("authorization").get(0));
GenericJson response = new GenericJson();
response.setFactory(JSON_FACTORY);
- response.put("accessToken", ACCESS_TOKEN);
+ response.put("accessToken", SERVICE_ACCOUNT_ACCESS_TOKEN);
response.put("expireTime", expireTime);
return new MockLowLevelHttpResponse()
@@ -212,6 +217,10 @@ public String getAccessToken() {
return ACCESS_TOKEN;
}
+ public String getServiceAccountAccessToken() {
+ return SERVICE_ACCOUNT_ACCESS_TOKEN;
+ }
+
public String getIssuedTokenType() {
return ISSUED_TOKEN_TYPE;
}
From 223c6ec6eab2e203d6f3bc517e094558af8969ff Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Tue, 17 Nov 2020 11:53:08 -0800
Subject: [PATCH 10/23] fix: fix issues found through manual testing (#506)
---
.../google/auth/oauth2/AwsCredentials.java | 52 +++++++++++++------
.../oauth2/ExternalAccountCredentials.java | 13 ++---
.../auth/oauth2/IdentityPoolCredentials.java | 10 ++--
.../auth/oauth2/AwsCredentialsTest.java | 11 ++--
.../ExternalAccountCredentialsTest.java | 2 +-
5 files changed, 56 insertions(+), 32 deletions(-)
diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
index 5c9e7b498..82af5265e 100644
--- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
@@ -40,9 +40,12 @@
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -59,9 +62,9 @@ public class AwsCredentials extends ExternalAccountCredentials {
*/
static class AwsCredentialSource extends CredentialSource {
- private String regionUrl;
- private String url;
- private String regionalCredentialVerificationUrl;
+ private final String regionUrl;
+ private final String url;
+ private final String regionalCredentialVerificationUrl;
/**
* The source of the AWS credential. The credential source map must contain the `region_url`,
@@ -99,15 +102,15 @@ static class AwsCredentialSource extends CredentialSource {
/**
* Internal constructor. See {@link
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
- * String, String, CredentialSource, String, String, String, String, Collection)}
+ * String, CredentialSource, String, String, String, String, String, Collection)}
*/
AwsCredentials(
HttpTransportFactory transportFactory,
String audience,
String subjectTokenType,
String tokenUrl,
- String tokenInfoUrl,
AwsCredentialSource credentialSource,
+ @Nullable String tokenInfoUrl,
@Nullable String serviceAccountImpersonationUrl,
@Nullable String quotaProjectId,
@Nullable String clientId,
@@ -118,8 +121,8 @@ static class AwsCredentialSource extends CredentialSource {
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
credentialSource,
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
@@ -175,8 +178,8 @@ public GoogleCredentials createScoped(Collection newScopes) {
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
(AwsCredentialSource) credentialSource,
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
@@ -195,28 +198,29 @@ private String retrieveResource(String url, String resourceName) throws IOExcept
}
}
- private String buildSubjectToken(AwsRequestSignature signature) {
- GenericJson headers = new GenericJson();
- headers.setFactory(OAuth2Utils.JSON_FACTORY);
-
+ private String buildSubjectToken(AwsRequestSignature signature)
+ throws UnsupportedEncodingException {
Map canonicalHeaders = signature.getCanonicalHeaders();
+ List headerList = new ArrayList<>();
for (String headerName : canonicalHeaders.keySet()) {
- headers.put(headerName, canonicalHeaders.get(headerName));
+ headerList.add(formatTokenHeaderForSts(headerName, canonicalHeaders.get(headerName)));
}
- headers.put("Authorization", signature.getAuthorizationHeader());
- headers.put("x-goog-cloud-target-resource", audience);
+ headerList.add(formatTokenHeaderForSts("Authorization", signature.getAuthorizationHeader()));
+
+ // The canonical resource name of the workload identity pool provider.
+ headerList.add(formatTokenHeaderForSts("x-goog-cloud-target-resource", audience));
GenericJson token = new GenericJson();
token.setFactory(OAuth2Utils.JSON_FACTORY);
- token.put("headers", headers);
+ token.put("headers", headerList);
token.put("method", signature.getHttpMethod());
token.put(
"url",
((AwsCredentialSource) credentialSource)
.regionalCredentialVerificationUrl.replace("{region}", signature.getRegion()));
- return token.toString();
+ return URLEncoder.encode(token.toString(), "UTF-8");
}
private String getAwsRegion() throws IOException {
@@ -270,6 +274,20 @@ String getEnv(String name) {
return System.getenv(name);
}
+ private static GenericJson formatTokenHeaderForSts(String key, String value) {
+ // The GCP STS endpoint expects the headers to be formatted as:
+ // [
+ // {key: 'x-amz-date', value: '...'},
+ // {key: 'Authorization', value: '...'},
+ // ...
+ // ]
+ GenericJson header = new GenericJson();
+ header.setFactory(OAuth2Utils.JSON_FACTORY);
+ header.put("key", key);
+ header.put("value", value);
+ return header;
+ }
+
public static AwsCredentials.Builder newBuilder() {
return new AwsCredentials.Builder();
}
@@ -293,8 +311,8 @@ public AwsCredentials build() {
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
(AwsCredentialSource) credentialSource,
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index 23293bfb9..1f946f6fb 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -77,10 +77,10 @@ protected CredentialSource(Map credentialSourceMap) {
protected final String audience;
protected final String subjectTokenType;
protected final String tokenUrl;
- protected final String tokenInfoUrl;
protected final CredentialSource credentialSource;
protected final Collection scopes;
+ @Nullable protected final String tokenInfoUrl;
@Nullable protected final String serviceAccountImpersonationUrl;
@Nullable protected final String quotaProjectId;
@Nullable protected final String clientId;
@@ -117,8 +117,8 @@ protected ExternalAccountCredentials(
String audience,
String subjectTokenType,
String tokenUrl,
- String tokenInfoUrl,
CredentialSource credentialSource,
+ @Nullable String tokenInfoUrl,
@Nullable String serviceAccountImpersonationUrl,
@Nullable String quotaProjectId,
@Nullable String clientId,
@@ -132,8 +132,8 @@ protected ExternalAccountCredentials(
this.audience = checkNotNull(audience);
this.subjectTokenType = checkNotNull(subjectTokenType);
this.tokenUrl = checkNotNull(tokenUrl);
- this.tokenInfoUrl = checkNotNull(tokenInfoUrl);
this.credentialSource = checkNotNull(credentialSource);
+ this.tokenInfoUrl = tokenInfoUrl;
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
this.quotaProjectId = quotaProjectId;
this.clientId = clientId;
@@ -229,12 +229,13 @@ public static ExternalAccountCredentials fromJson(
String audience = (String) json.get("audience");
String subjectTokenType = (String) json.get("subject_token_type");
String tokenUrl = (String) json.get("token_url");
- String tokenInfoUrl = (String) json.get("token_info_url");
String serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
Map credentialSourceMap = (Map) json.get("credential_source");
// Optional params.
+ String tokenInfoUrl =
+ json.containsKey("token_info_url") ? (String) json.get("token_info_url") : null;
String clientId = json.containsKey("client_id") ? (String) json.get("client_id") : null;
String clientSecret =
json.containsKey("client_secret") ? (String) json.get("client_secret") : null;
@@ -247,8 +248,8 @@ public static ExternalAccountCredentials fromJson(
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
new AwsCredentialSource(credentialSourceMap),
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
@@ -260,8 +261,8 @@ public static ExternalAccountCredentials fromJson(
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
new IdentityPoolCredentialSource(credentialSourceMap),
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
index 523989e33..365d021e2 100644
--- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
@@ -154,15 +154,15 @@ private boolean hasHeaders() {
/**
* Internal constructor. See {@link
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
- * String, String, CredentialSource, String, String, String, String, Collection)}
+ * String, CredentialSource, String, String, String, String, String, Collection)}
*/
IdentityPoolCredentials(
HttpTransportFactory transportFactory,
String audience,
String subjectTokenType,
String tokenUrl,
- String tokenInfoUrl,
IdentityPoolCredentialSource credentialSource,
+ @Nullable String tokenInfoUrl,
@Nullable String serviceAccountImpersonationUrl,
@Nullable String quotaProjectId,
@Nullable String clientId,
@@ -173,8 +173,8 @@ private boolean hasHeaders() {
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
credentialSource,
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
@@ -269,8 +269,8 @@ public GoogleCredentials createScoped(Collection newScopes) {
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
(IdentityPoolCredentialSource) credentialSource,
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
@@ -301,8 +301,8 @@ public IdentityPoolCredentials build() {
audience,
subjectTokenType,
tokenUrl,
- tokenInfoUrl,
(IdentityPoolCredentialSource) credentialSource,
+ tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
index 9abd622b5..6d967b791 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
@@ -47,6 +47,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
@@ -144,12 +145,16 @@ public void retrieveSubjectToken() throws IOException {
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
- String subjectToken = awsCredential.retrieveSubjectToken();
+ String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8");
JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(subjectToken);
GenericJson json = parser.parseAndClose(GenericJson.class);
- Map headers = (Map) json.get("headers");
+ List