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> headersList = (List>) json.get("headers"); + Map headers = new HashMap<>(); + for (Map header : headersList) { + headers.put(header.get("key"), header.get("value")); + } assertEquals("POST", json.get("method")); assertEquals(GET_CALLER_IDENTITY_URL, json.get("url")); @@ -371,8 +376,8 @@ private static class TestAwsCredentials extends AwsCredentials { audience, subjectTokenType, tokenUrl, - tokenInfoUrl, credentialSource, + tokenInfoUrl, serviceAccountImpersonationUrl, quotaProjectId, clientId, diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 22fd568df..730c4e8e9 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -349,8 +349,8 @@ protected TestExternalAccountCredentials( audience, subjectTokenType, tokenUrl, - tokenInfoUrl, credentialSource, + tokenInfoUrl, serviceAccountImpersonationUrl, quotaProjectId, clientId, From 3769b90151f320a5eaf6c7ae1953d9660b786b3f Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Tue, 15 Dec 2020 10:24:31 -0800 Subject: [PATCH 11/23] fix: updates AWS credential source (#520) * fix: update AwsCredentials credential source logic * fix: remove TODOs * fix: cleanup * fix: code review nits * fix: fix broken test * fix: lint --- .../google/auth/oauth2/AwsCredentials.java | 55 +++++--- .../oauth2/ExternalAccountCredentials.java | 2 - .../google/auth/oauth2/StsRequestHandler.java | 9 +- .../auth/oauth2/AwsCredentialsTest.java | 123 ++++++++++++++++++ .../ExternalAccountCredentialsTest.java | 5 - 5 files changed, 164 insertions(+), 30 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 82af5265e..84fc864fa 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -47,6 +47,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nullable; /** @@ -67,31 +69,42 @@ static class AwsCredentialSource extends CredentialSource { private final 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 source of the AWS credential. The credential source map must contain the + * `regional_cred_verification_url` field. * *

The `regional_cred_verification_url` is the regional GetCallerIdentity action URL, used to * determine the account ID and its roles. + * + *

The `environment_id` is the environment identifier, in the format “aws${version}”. This + * indicates whether breaking changes were introduced to the underlying AWS implementation. + * + *

The `region_url` identifies the targeted region. Optional. + * + *

The `url` locates the metadata server used to retrieve the AWS credentials. Optional. */ 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."); } + + String environmentId = (String) credentialSourceMap.get("environment_id"); + + // Environment version is prefixed by "aws". e.g. "aws1". + Matcher matcher = Pattern.compile("^(aws)([\\d]+)$").matcher(environmentId); + if (!matcher.find()) { + throw new IllegalArgumentException("Invalid AWS environment ID."); + } + + int environmentVersion = Integer.parseInt(matcher.group(2)); + if (environmentVersion != 1) { + throw new IllegalArgumentException( + String.format( + "AWS version %s is not supported in the current build.", environmentVersion)); + } + this.regionUrl = (String) credentialSourceMap.get("region_url"); this.url = (String) credentialSourceMap.get("url"); this.regionalCredentialVerificationUrl = @@ -229,7 +242,14 @@ private String getAwsRegion() throws IOException { if (region != null) { return region; } - region = retrieveResource(((AwsCredentialSource) credentialSource).regionUrl, "region"); + + AwsCredentialSource awsCredentialSource = (AwsCredentialSource) this.credentialSource; + if (awsCredentialSource.regionUrl == null || awsCredentialSource.regionUrl.isEmpty()) { + throw new IOException( + "Unable to determine the AWS region. The credential source does not contain the region URL."); + } + + region = retrieveResource(awsCredentialSource.regionUrl, "region"); // There is an extra appended character that must be removed. If `us-east-1b` is returned, // we want `us-east-1`. @@ -250,6 +270,11 @@ AwsSecurityCredentials getAwsSecurityCredentials() throws IOException { AwsCredentialSource awsCredentialSource = (AwsCredentialSource) credentialSource; // Retrieve the IAM role that is attached to the VM. This is required to retrieve the AWS // security credentials. + if (awsCredentialSource.url == null || awsCredentialSource.url.isEmpty()) { + throw new IOException( + "Unable to determine the AWS IAM role name. The credential source does not contain the" + + " url field."); + } String roleName = retrieveResource(awsCredentialSource.url, "IAM role"); // Retrieve the AWS security credentials by calling the endpoint specified by the credential diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 1f946f6fb..a909d7b7e 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -34,7 +34,6 @@ import static com.google.api.client.util.Preconditions.checkNotNull; import static com.google.common.base.MoreObjects.firstNonNull; -import com.google.api.client.http.HttpHeaders; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonObjectParser; import com.google.auth.http.HttpTransportFactory; @@ -292,7 +291,6 @@ protected AccessToken exchange3PICredentialForAccessToken( StsRequestHandler requestHandler = StsRequestHandler.newBuilder( tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory()) - .setHeaders(new HttpHeaders().setContentType("application/x-www-form-urlencoded")) .build(); StsTokenExchangeResponse response = requestHandler.exchangeToken(); diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index bbc8ec0f7..23079b500 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -49,19 +49,12 @@ 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. - */ +/** Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693. */ 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; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 6d967b791..21405b067 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -72,6 +72,7 @@ public class AwsCredentialsTest { private static final Map AWS_CREDENTIAL_SOURCE_MAP = new HashMap() { { + put("environment_id", "aws1"); put("region_url", "regionUrl"); put("url", "url"); put("regional_cred_verification_url", "regionalCredVerificationUrl"); @@ -251,6 +252,38 @@ public void run() throws Throwable { assertEquals("Failed to retrieve AWS credentials.", e.getMessage()); } + @Test + public void retrieveSubjectToken_noRegionUrlProvided() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + Map credentialSource = new HashMap<>(); + credentialSource.put("environment_id", "aws1"); + credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); + + final AwsCredentials awsCredential = + (AwsCredentials) + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(new AwsCredentialSource(credentialSource)) + .build(); + + IOException e = + assertThrows( + IOException.class, + new ThrowingRunnable() { + @Override + public void run() throws IOException { + awsCredential.retrieveSubjectToken(); + } + }); + + assertEquals( + "Unable to determine the AWS region. The credential source does not " + + "contain the region URL.", + e.getMessage()); + } + @Test public void getAwsSecurityCredentials_fromEnvironmentVariablesNoToken() throws IOException { TestAwsCredentials testAwsCredentials = TestAwsCredentials.newBuilder(AWS_CREDENTIAL).build(); @@ -297,6 +330,37 @@ public void getAwsSecurityCredentials_fromMetadataServer() throws IOException { assertEquals("token", credentials.getToken()); } + @Test + public void getAwsSecurityCredentials_fromMetadataServer_noUrlProvided() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + Map credentialSource = new HashMap<>(); + credentialSource.put("environment_id", "aws1"); + credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); + + final AwsCredentials awsCredential = + (AwsCredentials) + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(new AwsCredentialSource(credentialSource)) + .build(); + + IOException e = + assertThrows( + IOException.class, + new ThrowingRunnable() { + @Override + public void run() throws IOException { + awsCredential.getAwsSecurityCredentials(); + } + }); + + assertEquals( + "Unable to determine the AWS IAM role name. The credential source does not contain the url field.", + e.getMessage()); + } + @Test public void createdScoped_clonedCredentialWithAddedScopes() { AwsCredentials credentials = @@ -326,9 +390,68 @@ public void createdScoped_clonedCredentialWithAddedScopes() { assertEquals(newScopes, newCredentials.getScopes()); } + @Test + public void credentialSource_invalidAwsEnvironmentId() { + final Map credentialSource = new HashMap<>(); + credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); + credentialSource.put("environment_id", "azure1"); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + new ThrowingRunnable() { + @Override + public void run() { + new AwsCredentialSource(credentialSource); + } + }); + + assertEquals("Invalid AWS environment ID.", e.getMessage()); + } + + @Test + public void credentialSource_invalidAwsEnvironmentVersion() { + final Map credentialSource = new HashMap<>(); + int environmentVersion = 2; + credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); + credentialSource.put("environment_id", "aws" + environmentVersion); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + new ThrowingRunnable() { + @Override + public void run() { + new AwsCredentialSource(credentialSource); + } + }); + + assertEquals( + String.format("AWS version %s is not supported in the current build.", environmentVersion), + e.getMessage()); + } + + @Test + public void credentialSource_missingRegionalCredVerificationUrl() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + new ThrowingRunnable() { + @Override + public void run() { + new AwsCredentialSource(new HashMap()); + } + }); + + assertEquals( + "A regional_cred_verification_url representing the GetCallerIdentity action URL must be specified.", + e.getMessage()); + } + private static AwsCredentialSource buildAwsCredentialSource( MockExternalAccountCredentialsTransportFactory transportFactory) { Map credentialSourceMap = new HashMap<>(); + credentialSourceMap.put("environment_id", "aws1"); credentialSourceMap.put("region_url", transportFactory.transport.getAwsRegionUrl()); credentialSourceMap.put("url", transportFactory.transport.getAwsCredentialsUrl()); credentialSourceMap.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 730c4e8e9..7111bfd70 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -214,11 +214,6 @@ public void exchange3PICredentialForAccessToken() throws IOException { credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest); assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); - - Map> headers = transportFactory.transport.getRequest().getHeaders(); - - assertTrue(headers.containsKey("content-type")); - assertEquals("application/x-www-form-urlencoded", headers.get("content-type").get(0)); } @Test From 48405f7d227fa59386538c111dd42944b7ba8890 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Thu, 28 Jan 2021 15:02:11 -0800 Subject: [PATCH 12/23] fix: merge --- .../google/auth/oauth2/ExternalAccountCredentials.java | 3 ++- oauth2_http/javatests/com/google/auth/TestUtils.java | 2 +- .../com/google/auth/oauth2/AwsRequestSignerTest.java | 3 ++- .../google/auth/oauth2/IdentityPoolCredentialsTest.java | 8 +++++--- .../oauth2/MockExternalAccountCredentialsTransport.java | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index a909d7b7e..5d7c53094 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -42,6 +42,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -208,7 +209,7 @@ public static ExternalAccountCredentials fromStream( JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); GenericJson fileContents = - parser.parseAndClose(credentialsStream, OAuth2Utils.UTF_8, GenericJson.class); + parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class); return fromJson(fileContents, transportFactory); } diff --git a/oauth2_http/javatests/com/google/auth/TestUtils.java b/oauth2_http/javatests/com/google/auth/TestUtils.java index 463c2b6d7..687b288f6 100644 --- a/oauth2_http/javatests/com/google/auth/TestUtils.java +++ b/oauth2_http/javatests/com/google/auth/TestUtils.java @@ -132,7 +132,7 @@ public static HttpResponseException buildHttpResponseException( String error, @Nullable String errorDescription, @Nullable String errorUri) throws IOException { GenericJson json = new GenericJson(); - json.setFactory(JacksonFactory.getDefaultInstance()); + json.setFactory(GsonFactory.getDefaultInstance()); json.set("error", error); if (errorDescription != null) { json.set("error_description", errorDescription); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java index 2e40109ea..ed175285e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java @@ -39,6 +39,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import org.junit.Before; @@ -532,7 +533,7 @@ public AwsSecurityCredentials retrieveAwsSecurityCredentials() throws IOExceptio JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; JsonObjectParser parser = new JsonObjectParser(jsonFactory); - GenericJson json = parser.parseAndClose(stream, OAuth2Utils.UTF_8, GenericJson.class); + GenericJson json = parser.parseAndClose(stream, StandardCharsets.UTF_8, GenericJson.class); String awsToken = (String) json.get("Token"); String secretAccessKey = (String) json.get("SecretAccessKey"); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 3ce9fdc18..90db231af 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -34,7 +34,6 @@ 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; import static org.junit.Assert.assertThrows; @@ -47,6 +46,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -131,7 +131,8 @@ public void retrieveSubjectToken_fileSourced() throws IOException { String credential = "credential"; OAuth2Utils.writeInputStreamToFile( - new ByteArrayInputStream(credential.getBytes(UTF_8)), file.getAbsolutePath()); + new ByteArrayInputStream(credential.getBytes(StandardCharsets.UTF_8)), + file.getAbsolutePath()); Map credentialSourceMap = new HashMap<>(); credentialSourceMap.put("file", file.getAbsolutePath()); @@ -176,7 +177,8 @@ public void retrieveSubjectToken_fileSourcedWithJsonFormat() throws IOException response.put("subjectToken", "subjectToken"); OAuth2Utils.writeInputStreamToFile( - new ByteArrayInputStream(response.toString().getBytes(UTF_8)), file.getAbsolutePath()); + new ByteArrayInputStream(response.toString().getBytes(StandardCharsets.UTF_8)), + file.getAbsolutePath()); IdentityPoolCredentials credential = (IdentityPoolCredentials) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java index c9dff795f..a91cd674b 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -40,7 +40,7 @@ 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.json.gson.GsonFactory; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.api.client.testing.http.MockLowLevelHttpResponse; @@ -76,7 +76,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport { private static final String SERVICE_ACCOUNT_ACCESS_TOKEN = "serviceAccountAccessToken"; private static final Long EXPIRES_IN = 3600L; - private static final JsonFactory JSON_FACTORY = new JacksonFactory(); + private static final JsonFactory JSON_FACTORY = new GsonFactory(); static final String SERVICE_ACCOUNT_IMPERSONATION_URL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testn@test.iam.gserviceaccount.com:generateAccessToken"; From e481da2148995323eab261aa5d2d8a90d9b14e46 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Fri, 29 Jan 2021 12:12:01 -0800 Subject: [PATCH 13/23] fix: change copyright year to 2021 --- oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java | 2 +- .../java/com/google/auth/oauth2/AwsRequestSignature.java | 2 +- oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java | 2 +- .../java/com/google/auth/oauth2/AwsSecurityCredentials.java | 2 +- .../java/com/google/auth/oauth2/ExternalAccountCredentials.java | 2 +- .../java/com/google/auth/oauth2/IdentityPoolCredentials.java | 2 +- oauth2_http/java/com/google/auth/oauth2/OAuthException.java | 2 +- oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java | 2 +- .../java/com/google/auth/oauth2/StsTokenExchangeRequest.java | 2 +- .../java/com/google/auth/oauth2/StsTokenExchangeResponse.java | 2 +- .../javatests/com/google/auth/oauth2/AwsRequestSignerTest.java | 2 +- .../auth/oauth2/MockExternalAccountCredentialsTransport.java | 2 +- .../javatests/com/google/auth/oauth2/OAuthExceptionTest.java | 2 +- .../javatests/com/google/auth/oauth2/StsRequestHandlerTest.java | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 84fc864fa..ec2fb4185 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java index af90a7603..0bec90eb6 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java index d49f7b908..1bd939264 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java index 04aaeacd7..5c12e71f0 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 5d7c53094..ecf3847aa 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 365d021e2..a67ada1cd 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java index c4304f2b0..d54dc99ef 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index 23079b500..59b183537 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java index bcd3e95a9..9348879bb 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java index 93655ccab..5b83339b4 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java index ed175285e..edc6b4cd4 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java index a91cd674b..109c93573 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java index e9188e723..3d35f5ef2 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java index 175dff042..d8ff4c52a 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are From 36ff0f68e36a65ddbf981f03d0499fd48cc8f2fa Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Fri, 29 Jan 2021 12:25:08 -0800 Subject: [PATCH 14/23] fix: address review comments --- .../google/auth/oauth2/AwsRequestSigner.java | 2 +- .../auth/oauth2/IdentityPoolCredentials.java | 2 +- .../google/auth/oauth2/StsRequestHandler.java | 10 ++++----- .../javatests/com/google/auth/TestUtils.java | 22 +++++++------------ .../auth/oauth2/AwsCredentialsTest.java | 3 +-- .../ExternalAccountCredentialsTest.java | 3 +-- .../oauth2/IdentityPoolCredentialsTest.java | 3 +-- 7 files changed, 18 insertions(+), 27 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java index 1bd939264..5873c99c1 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @@ -89,7 +89,7 @@ class AwsRequestSigner { * @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. + * request */ private AwsRequestSigner( AwsSecurityCredentials awsSecurityCredentials, diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index a67ada1cd..1ef431b52 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -123,7 +123,7 @@ public IdentityPoolCredentialSource(Map credentialSourceMap) { headers.putAll(headersMap); } - // If the format is not provided, we will expect the token to be in the raw text format. + // If the format is not provided, we expect the token to be in the raw text format. credentialFormatType = CredentialFormatType.TEXT; Map formatMap = (Map) credentialSourceMap.get("format"); diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index 59b183537..fe4cabcad 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -67,11 +67,11 @@ public class StsRequestHandler { /** * 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. + * @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, diff --git a/oauth2_http/javatests/com/google/auth/TestUtils.java b/oauth2_http/javatests/com/google/auth/TestUtils.java index 687b288f6..b9c2b6d75 100644 --- a/oauth2_http/javatests/com/google/auth/TestUtils.java +++ b/oauth2_http/javatests/com/google/auth/TestUtils.java @@ -60,11 +60,6 @@ public class TestUtils { private static final JsonFactory JSON_FACTORY = GsonFactory.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); @@ -93,12 +88,12 @@ private static boolean hasBearerToken(Map> metadata, String public static InputStream jsonToInputStream(GenericJson json) throws IOException { json.setFactory(JSON_FACTORY); String text = json.toPrettyString(); - return new ByteArrayInputStream(text.getBytes(UTF_8)); + return new ByteArrayInputStream(text.getBytes("UTF-8")); } public static InputStream stringToInputStream(String text) { try { - return new ByteArrayInputStream(text.getBytes(TestUtils.UTF_8)); + return new ByteArrayInputStream(text.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Unexpected encoding exception", e); } @@ -112,8 +107,8 @@ public static Map parseQuery(String query) throws IOException { if (sides.size() != 2) { throw new IOException("Invalid Query String"); } - String key = URLDecoder.decode(sides.get(0), UTF_8); - String value = URLDecoder.decode(sides.get(1), UTF_8); + String key = URLDecoder.decode(sides.get(0), "UTF-8"); + String value = URLDecoder.decode(sides.get(1), "UTF-8"); map.put(key, value); } return map; @@ -147,11 +142,10 @@ public static HttpResponseException buildHttpResponseException( } 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()); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.add(Calendar.SECOND, 300); + return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(calendar.getTime()); } private TestUtils() {} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 21405b067..e52cc5e45 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -31,7 +31,6 @@ 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; @@ -116,7 +115,7 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept MockExternalAccountCredentialsTransportFactory transportFactory = new MockExternalAccountCredentialsTransportFactory(); - transportFactory.transport.setExpireTime(getDefaultExpireTime()); + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); AwsCredentials awsCredential = (AwsCredentials) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 7111bfd70..d60a49446 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -31,7 +31,6 @@ 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.assertThrows; @@ -219,7 +218,7 @@ public void exchange3PICredentialForAccessToken() throws IOException { @Test public void exchange3PICredentialForAccessToken_withServiceAccountImpersonation() throws IOException { - transportFactory.transport.setExpireTime(getDefaultExpireTime()); + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); ExternalAccountCredentials credential = ExternalAccountCredentials.fromStream( diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 90db231af..50c755866 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -31,7 +31,6 @@ 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 org.junit.Assert.assertEquals; @@ -321,7 +320,7 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept MockExternalAccountCredentialsTransportFactory transportFactory = new MockExternalAccountCredentialsTransportFactory(); - transportFactory.transport.setExpireTime(getDefaultExpireTime()); + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); final IdentityPoolCredentials credential = (IdentityPoolCredentials) IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) From b9b9f8d092339073c3ba4a36e2e29abd43869973 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Mon, 1 Feb 2021 17:34:06 -0800 Subject: [PATCH 15/23] fix: remove assertThrows --- .../auth/oauth2/AwsCredentialsTest.java | 158 +++++++----------- .../ExternalAccountCredentialsTest.java | 107 +++++------- .../oauth2/IdentityPoolCredentialsTest.java | 123 +++++--------- 3 files changed, 149 insertions(+), 239 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index e52cc5e45..ce5fe322e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -34,8 +34,8 @@ 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 static org.junit.Assert.fail; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonParser; @@ -54,7 +54,6 @@ 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; @@ -180,17 +179,12 @@ public void retrieveSubjectToken_noRegion_expectThrows() { .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()); + try { + awsCredential.retrieveSubjectToken(); + fail("Exception should be thrown."); + } catch (IOException e) { + assertEquals("Failed to retrieve AWS region.", e.getMessage()); + } } @Test @@ -209,17 +203,12 @@ public void retrieveSubjectToken_noRole_expectThrows() { .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()); + try { + awsCredential.retrieveSubjectToken(); + fail("Exception should be thrown."); + } catch (IOException e) { + assertEquals("Failed to retrieve AWS IAM role.", e.getMessage()); + } } @Test @@ -238,17 +227,12 @@ public void retrieveSubjectToken_noCredentials_expectThrows() { .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()); + try { + awsCredential.retrieveSubjectToken(); + fail("Exception should be thrown."); + } catch (IOException e) { + assertEquals("Failed to retrieve AWS credentials.", e.getMessage()); + } } @Test @@ -267,20 +251,15 @@ public void retrieveSubjectToken_noRegionUrlProvided() { .setCredentialSource(new AwsCredentialSource(credentialSource)) .build(); - IOException e = - assertThrows( - IOException.class, - new ThrowingRunnable() { - @Override - public void run() throws IOException { - awsCredential.retrieveSubjectToken(); - } - }); - - assertEquals( - "Unable to determine the AWS region. The credential source does not " - + "contain the region URL.", - e.getMessage()); + try { + awsCredential.retrieveSubjectToken(); + fail("Exception should be thrown."); + } catch (IOException e) { + assertEquals( + "Unable to determine the AWS region. The credential source does not " + + "contain the region URL.", + e.getMessage()); + } } @Test @@ -345,19 +324,14 @@ public void getAwsSecurityCredentials_fromMetadataServer_noUrlProvided() { .setCredentialSource(new AwsCredentialSource(credentialSource)) .build(); - IOException e = - assertThrows( - IOException.class, - new ThrowingRunnable() { - @Override - public void run() throws IOException { - awsCredential.getAwsSecurityCredentials(); - } - }); - - assertEquals( - "Unable to determine the AWS IAM role name. The credential source does not contain the url field.", - e.getMessage()); + try { + awsCredential.getAwsSecurityCredentials(); + fail("Exception should be thrown."); + } catch (IOException e) { + assertEquals( + "Unable to determine the AWS IAM role name. The credential source does not contain the url field.", + e.getMessage()); + } } @Test @@ -395,17 +369,12 @@ public void credentialSource_invalidAwsEnvironmentId() { credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); credentialSource.put("environment_id", "azure1"); - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - new ThrowingRunnable() { - @Override - public void run() { - new AwsCredentialSource(credentialSource); - } - }); - - assertEquals("Invalid AWS environment ID.", e.getMessage()); + try { + new AwsCredentialSource(credentialSource); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid AWS environment ID.", e.getMessage()); + } } @Test @@ -415,36 +384,27 @@ public void credentialSource_invalidAwsEnvironmentVersion() { credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); credentialSource.put("environment_id", "aws" + environmentVersion); - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - new ThrowingRunnable() { - @Override - public void run() { - new AwsCredentialSource(credentialSource); - } - }); - - assertEquals( - String.format("AWS version %s is not supported in the current build.", environmentVersion), - e.getMessage()); + try { + new AwsCredentialSource(credentialSource); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals( + String.format( + "AWS version %s is not supported in the current build.", environmentVersion), + e.getMessage()); + } } @Test public void credentialSource_missingRegionalCredVerificationUrl() { - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - new ThrowingRunnable() { - @Override - public void run() { - new AwsCredentialSource(new HashMap()); - } - }); - - assertEquals( - "A regional_cred_verification_url representing the GetCallerIdentity action URL must be specified.", - e.getMessage()); + try { + new AwsCredentialSource(new HashMap()); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals( + "A regional_cred_verification_url representing the GetCallerIdentity action URL must be specified.", + e.getMessage()); + } } private static AwsCredentialSource buildAwsCredentialSource( diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index d60a49446..ec395a999 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -33,8 +33,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.GenericJson; @@ -52,7 +52,6 @@ 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; @@ -104,28 +103,24 @@ public void fromStream_awsCredentials() throws IOException { @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); - } - }); + try { + ExternalAccountCredentials.fromStream( + new ByteArrayInputStream("foo".getBytes()), /* transportFactory= */ null); + fail("Exception should be thrown."); + } catch (NullPointerException | IOException e) { + // Expected. + } } @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); - } - }); + try { + ExternalAccountCredentials.fromStream( + /* credentialsStream= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + fail("Exception should be thrown."); + } catch (NullPointerException | IOException e) { + // Expected. + } } @Test @@ -158,15 +153,12 @@ public void fromJson_awsCredentials() { @Test public void fromJson_nullJson_throws() { - assertThrows( - NullPointerException.class, - new ThrowingRunnable() { - @Override - public void run() { - ExternalAccountCredentials.fromJson( - /* json= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY); - } - }); + try { + ExternalAccountCredentials.fromJson(/* json= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + fail("Exception should be thrown."); + } catch (NullPointerException e) { + // Expected. + } } @Test @@ -174,31 +166,25 @@ 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()); + try { + ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals( + "Unable to determine target principal from service account impersonation URL.", + e.getMessage()); + } } @Test public void fromJson_nullTransport_throws() { - assertThrows( - NullPointerException.class, - new ThrowingRunnable() { - @Override - public void run() { - ExternalAccountCredentials.fromJson( - new HashMap(), /* transportFactory= */ null); - } - }); + try { + ExternalAccountCredentials.fromJson( + new HashMap(), /* transportFactory= */ null); + fail("Exception should be thrown."); + } catch (NullPointerException e) { + // Expected. + } } @Test @@ -252,19 +238,14 @@ public void exchange3PICredentialForAccessToken_throws() throws IOException { final StsTokenExchangeRequest stsTokenExchangeRequest = StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); - OAuthException e = - assertThrows( - OAuthException.class, - new ThrowingRunnable() { - @Override - public void run() throws Throwable { - credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest); - } - }); - - assertEquals(errorCode, e.getErrorCode()); - assertEquals(errorDescription, e.getErrorDescription()); - assertEquals(errorUri, e.getErrorUri()); + try { + credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest); + fail("Exception should be thrown."); + } catch (OAuthException e) { + assertEquals(errorCode, e.getErrorCode()); + assertEquals(errorDescription, e.getErrorDescription()); + assertEquals(errorUri, e.getErrorUri()); + } } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 50c755866..8de3e574b 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -34,7 +34,7 @@ import static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL; import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.GenericJson; @@ -52,7 +52,6 @@ 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; @@ -205,19 +204,14 @@ public void retrieveSubjectToken_noFile_throws() { .setCredentialSource(credentialSource) .build(); - IOException e = - assertThrows( - IOException.class, - new ThrowingRunnable() { - @Override - public void run() throws Throwable { - credentials.retrieveSubjectToken(); - } - }); - - assertEquals( - String.format("Invalid credential location. The file at %s does not exist.", path), - e.getMessage()); + try { + credentials.retrieveSubjectToken(); + fail("Exception should be thrown."); + } catch (IOException e) { + assertEquals( + String.format("Invalid credential location. The file at %s does not exist.", path), + e.getMessage()); + } } @Test @@ -280,20 +274,15 @@ public void retrieveSubjectToken_urlSourcedCredential_throws() { buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) .build(); - IOException e = - assertThrows( - IOException.class, - new ThrowingRunnable() { - @Override - public void run() throws Throwable { - credential.retrieveSubjectToken(); - } - }); - - assertEquals( - String.format( - "Error getting subject token from metadata server: %s", response.getMessage()), - e.getMessage()); + try { + credential.retrieveSubjectToken(); + fail("Exception should be thrown."); + } catch (IOException e) { + assertEquals( + String.format( + "Error getting subject token from metadata server: %s", response.getMessage()), + e.getMessage()); + } } @Test @@ -340,19 +329,14 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept @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()); + try { + new IdentityPoolCredentialSource(new HashMap()); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals( + "Missing credential source file location or URL. At least one must be specified.", + e.getMessage()); + } } @Test @@ -364,17 +348,12 @@ public void identityPoolCredentialSource_invalidFormatType() { 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()); + try { + new IdentityPoolCredentialSource(credentialSourceMap); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid credential source format type: unsupportedType.", e.getMessage()); + } } @Test @@ -386,17 +365,12 @@ public void identityPoolCredentialSource_nullFormatType() { 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()); + try { + new IdentityPoolCredentialSource(credentialSourceMap); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid credential source format type: null.", e.getMessage()); + } } @Test @@ -408,19 +382,14 @@ public void identityPoolCredentialSource_subjectTokenFieldNameUnset() { 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()); + try { + new IdentityPoolCredentialSource(credentialSourceMap); + fail("Exception should be thrown."); + } catch (IllegalArgumentException e) { + assertEquals( + "When specifying a JSON credential type, the subject_token_field_name must be set.", + e.getMessage()); + } } static InputStream writeIdentityPoolCredentialsStream( From a980a19a82824b424b74005f053da13abbe7b799 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Tue, 2 Feb 2021 16:29:45 -0800 Subject: [PATCH 16/23] fix: address review comments --- .../google/auth/oauth2/AwsCredentials.java | 2 +- .../auth/oauth2/AwsRequestSignature.java | 2 +- .../google/auth/oauth2/AwsRequestSigner.java | 6 ++--- .../auth/oauth2/AwsSecurityCredentials.java | 8 +++--- .../oauth2/ExternalAccountCredentials.java | 2 +- .../google/auth/oauth2/GoogleCredentials.java | 4 +-- .../auth/oauth2/IdentityPoolCredentials.java | 12 ++++----- .../google/auth/oauth2/OAuthException.java | 27 ++++++++++--------- .../google/auth/oauth2/StsRequestHandler.java | 4 +-- .../auth/oauth2/StsTokenExchangeRequest.java | 2 +- .../auth/oauth2/StsTokenExchangeResponse.java | 5 ++-- .../auth/oauth2/AwsCredentialsTest.java | 4 +-- .../auth/oauth2/AwsRequestSignerTest.java | 2 +- .../ExternalAccountCredentialsTest.java | 16 +++++------ .../oauth2/IdentityPoolCredentialsTest.java | 4 +-- ...ckExternalAccountCredentialsTransport.java | 2 +- .../auth/oauth2/OAuthExceptionTest.java | 2 +- .../auth/oauth2/StsRequestHandlerTest.java | 2 +- 18 files changed, 52 insertions(+), 54 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index ec2fb4185..2dfce3a0c 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java index 0bec90eb6..463b84676 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java index 5873c99c1..f85376134 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -343,8 +343,8 @@ 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 final String xAmzDate; + private final String originalDate; private AwsDates(String amzDate) { this.xAmzDate = checkNotNull(amzDate); diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java index 5c12e71f0..b7865049a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -39,10 +39,10 @@ */ class AwsSecurityCredentials { - private String accessKeyId; - private String secretAccessKey; + private final String accessKeyId; + private final String secretAccessKey; - @Nullable private String token; + @Nullable private final String token; AwsSecurityCredentials(String accessKeyId, String secretAccessKey, @Nullable String token) { this.accessKeyId = accessKeyId; diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index ecf3847aa..45b8eadbf 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java index 2d634df90..3e61e5d60 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java @@ -31,8 +31,6 @@ 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; @@ -168,7 +166,7 @@ public static GoogleCredentials fromStream( if (SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) { return ServiceAccountCredentials.fromJson(fileContents, transportFactory); } - if (EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) { + if (ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) { return ExternalAccountCredentials.fromJson(fileContents, transportFactory); } throw new IOException( diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 1ef431b52..be8c29907 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -36,7 +36,6 @@ 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.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType; @@ -90,15 +89,15 @@ enum CredentialFormatType { /** * The source of the 3P credential. * - *

If the this a file based 3P credential, the credentials file can be retrieved using the + *

If this is 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. * *

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 + * format can be specified using the `format` header, which returns 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`. @@ -227,8 +226,7 @@ private String parseToken(InputStream inputStream) throws IOException { return CharStreams.toString(reader); } - JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; - JsonObjectParser parser = new JsonObjectParser(jsonFactory); + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); GenericJson fileContents = parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java index d54dc99ef..057e51748 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -36,14 +36,15 @@ import java.io.IOException; import javax.annotation.Nullable; +/** + * Encapsulates the standard OAuth error response. See + * https://tools.ietf.org/html/rfc6749#section-5.2. + */ 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; + private final String errorCode; + @Nullable private final String errorDescription; + @Nullable private final String errorUri; public OAuthException( String errorCode, @Nullable String errorDescription, @Nullable String errorUri) { @@ -54,13 +55,15 @@ public OAuthException( @Override public String getMessage() { - if (errorDescription != null && errorUri != null) { - return String.format(FULL_MESSAGE_FORMAT, errorCode, errorDescription, errorUri); - } + // Fully specified message will have the format Error code %s: %s - %s. + StringBuilder sb = new StringBuilder("Error code " + errorCode); if (errorDescription != null) { - return String.format(ERROR_DESCRIPTION_FORMAT, errorCode, errorDescription); + sb.append(": ").append(errorDescription); + } + if (errorUri != null) { + sb.append(" - ").append(errorUri); } - return String.format(BASE_MESSAGE_FORMAT, errorCode); + return sb.toString(); } public String getErrorCode() { diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index fe4cabcad..4139512a8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -93,7 +93,7 @@ public static Builder newBuilder( return new Builder(tokenExchangeEndpoint, stsTokenExchangeRequest, httpRequestFactory); } - /** Exchanges the provided token for another type of token based on the rfc8693 spec. */ + /** Exchanges the provided token for another type of token based on the RFC 8693 spec. */ public StsTokenExchangeResponse exchangeToken() throws IOException { UrlEncodedContent content = new UrlEncodedContent(buildTokenRequest()); diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java index 9348879bb..d6288cc26 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java index 5b83339b4..c51a9270a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -33,7 +33,6 @@ 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; @@ -61,7 +60,7 @@ private StsTokenExchangeResponse( @Nullable List scopes) { checkNotNull(accessToken); this.expiresIn = checkNotNull(expiresIn); - long expiresAtMilliseconds = Clock.SYSTEM.currentTimeMillis() + expiresIn * 1000L; + long expiresAtMilliseconds = System.currentTimeMillis() + expiresIn * 1000L; this.accessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds)); this.issuedTokenType = checkNotNull(issuedTokenType); this.tokenType = checkNotNull(tokenType); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index ce5fe322e..0eb37a08f 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020, Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java index edc6b4cd4..ebb14091e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsRequestSignerTest.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index ec395a999..7340e9791 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020, Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -102,23 +102,23 @@ public void fromStream_awsCredentials() throws IOException { } @Test - public void fromStream_nullTransport_throws() { + public void fromStream_nullTransport_throws() throws IOException { try { ExternalAccountCredentials.fromStream( new ByteArrayInputStream("foo".getBytes()), /* transportFactory= */ null); - fail("Exception should be thrown."); - } catch (NullPointerException | IOException e) { + fail("NullPointerException should be thrown."); + } catch (NullPointerException e) { // Expected. } } @Test - public void fromStream_nullStream_throws() { + public void fromStream_nullStream_throws() throws IOException { try { ExternalAccountCredentials.fromStream( /* credentialsStream= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY); - fail("Exception should be thrown."); - } catch (NullPointerException | IOException e) { + fail("NullPointerException should be thrown."); + } catch (NullPointerException e) { // Expected. } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 8de3e574b..c55926d7f 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020, Google LLC + * Copyright 2021 Google LLC * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java index 109c93573..46e2add39 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java index 3d35f5ef2..f864f4791 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java index d8ff4c52a..4e54e764e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java @@ -12,7 +12,7 @@ * in the documentation and/or other materials provided with the * distribution. * - * * Neither the name of Google Inc. nor the names of its + * * Neither the name of Google LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * From 6c3982cd186bac6bc31acd66adce57ab5e62745d Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Thu, 11 Feb 2021 20:02:23 -0800 Subject: [PATCH 17/23] fix: review comments --- .../google/auth/oauth2/AwsCredentials.java | 4 +- .../google/auth/oauth2/AwsRequestSigner.java | 16 ++--- .../oauth2/ExternalAccountCredentials.java | 70 +++++++++---------- .../auth/oauth2/IdentityPoolCredentials.java | 6 +- .../google/auth/oauth2/StsRequestHandler.java | 23 +++--- .../auth/oauth2/StsTokenExchangeRequest.java | 33 ++++----- .../auth/oauth2/StsTokenExchangeResponse.java | 25 ++++--- .../ExternalAccountCredentialsTest.java | 12 ++-- ...ckExternalAccountCredentialsTransport.java | 2 +- 9 files changed, 94 insertions(+), 97 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 2dfce3a0c..bc987a1b6 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -54,7 +54,7 @@ /** * AWS credentials representing a third-party identity for calling Google APIs. * - *

By default, attempts to exchange the 3PI credential for a GCP access token. + *

By default, attempts to exchange the external credential for a GCP access token. */ public class AwsCredentials extends ExternalAccountCredentials { @@ -154,7 +154,7 @@ public AccessToken refreshAccessToken() throws IOException { stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); } - return exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build()); + return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build()); } @Override diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java index f85376134..35c106acf 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @@ -31,7 +31,7 @@ package com.google.auth.oauth2; -import static com.google.api.client.util.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.api.client.util.Joiner; @@ -61,7 +61,8 @@ * 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 + * @see AWS + * Signature V4 */ class AwsRequestSigner { @@ -259,8 +260,7 @@ private static byte[] sign(byte[] key, byte[] value) { 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); + throw new RuntimeException("HmacSHA256 must be supported by the JVM.", e); } catch (InvalidKeyException e) { throw new SigningException("Invalid key used when calculating the AWS V4 Signature", e); } @@ -282,10 +282,10 @@ static Builder newBuilder( static class Builder { - private AwsSecurityCredentials awsSecurityCredentials; - private String httpMethod; - private String url; - private String region; + private final AwsSecurityCredentials awsSecurityCredentials; + private final String httpMethod; + private final String url; + private final String region; @Nullable private String requestPayload; @Nullable private Map additionalHeaders; diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 45b8eadbf..b13890e13 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -31,8 +31,8 @@ package com.google.auth.oauth2; -import static com.google.api.client.util.Preconditions.checkNotNull; import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonObjectParser; @@ -58,13 +58,11 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials implements QuotaProjectIdProvider { - /** Base credential source class. Dictates the retrieval method of the 3PI credential. */ + /** Base credential source class. Dictates the retrieval method of the external credential. */ abstract static class CredentialSource { - protected Map credentialSourceMap; - protected CredentialSource(Map credentialSourceMap) { - this.credentialSourceMap = checkNotNull(credentialSourceMap); + checkNotNull(credentialSourceMap); } } @@ -93,24 +91,23 @@ protected CredentialSource(Map credentialSourceMap) { /** * 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 + * @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. + * @param credentialSource the external 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. + * @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, @@ -182,9 +179,9 @@ public Map> getRequestMetadata(URI uri) throws IOException * *

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. + * @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 { @@ -196,11 +193,11 @@ public static ExternalAccountCredentials fromStream(InputStream credentialsStrea * *

This will either return a IdentityPoolCredentials or AwsCredentials. * - * @param credentialsStream the stream with the credential definition. + * @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. + * 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 { @@ -216,10 +213,9 @@ public static ExternalAccountCredentials fromStream( /** * 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. + * @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) { @@ -276,13 +272,13 @@ private static boolean isAwsCredential(Map credentialSource) { } /** - * Exchanges the 3PI credential for a GCP access token. + * Exchanges the external 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. + * @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( + protected AccessToken exchangeExternalCredentialForAccessToken( StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException { // Handle service account impersonation if necessary. if (impersonatedCredentials != null) { @@ -312,12 +308,12 @@ private static String extractTargetPrincipal(String serviceAccountImpersonationU } /** - * Retrieves the 3PI subject token to be exchanged for a GCP access token. + * Retrieves the external 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 + * @return the external subject token */ public abstract String retrieveSubjectToken() throws IOException; diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index be8c29907..307ae34d6 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -64,8 +64,8 @@ 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. + * The IdentityPool credential source. Dictates the retrieval method of the external credential, + * which can either be through a metadata server or a local file. */ static class IdentityPoolCredentialSource extends ExternalAccountCredentials.CredentialSource { @@ -192,7 +192,7 @@ public AccessToken refreshAccessToken() throws IOException { stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); } - return exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build()); + return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build()); } @Override diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index 4139512a8..b0b5dcb4a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -57,12 +57,12 @@ public class StsRequestHandler { "urn:ietf:params:oauth:token-type:access_token"; private static final String PARSE_ERROR_PREFIX = "Error parsing token response."; - private String tokenExchangeEndpoint; - private StsTokenExchangeRequest request; - private HttpRequestFactory httpRequestFactory; + private final String tokenExchangeEndpoint; + private final StsTokenExchangeRequest request; + private final HttpRequestFactory httpRequestFactory; - @Nullable private HttpHeaders headers; - @Nullable private String internalOptions; + @Nullable private final HttpHeaders headers; + @Nullable private final String internalOptions; /** * Internal constructor. @@ -108,11 +108,8 @@ public StsTokenExchangeResponse exchangeToken() throws IOException { 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()); + } catch (HttpResponseException e) { + GenericJson errorResponse = parseJson((e).getContent()); String errorCode = (String) errorResponse.get("error"); String errorDescription = null; String errorUri = null; @@ -195,9 +192,9 @@ private GenericJson parseJson(String json) throws IOException { } public static class Builder { - private String tokenExchangeEndpoint; - private StsTokenExchangeRequest request; - private HttpRequestFactory httpRequestFactory; + private final String tokenExchangeEndpoint; + private final StsTokenExchangeRequest request; + private final HttpRequestFactory httpRequestFactory; @Nullable private HttpHeaders headers; @Nullable private String internalOptions; diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java index d6288cc26..a30e437ef 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @@ -43,14 +43,14 @@ public class StsTokenExchangeRequest { private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; - private String subjectToken; - private String subjectTokenType; + private final String subjectToken; + private final String subjectTokenType; - @Nullable private ActingParty actingParty; - @Nullable private List scopes; - @Nullable private String resource; - @Nullable private String audience; - @Nullable private String requestedTokenType; + @Nullable private final ActingParty actingParty; + @Nullable private final List scopes; + @Nullable private final String resource; + @Nullable private final String audience; + @Nullable private final String requestedTokenType; private StsTokenExchangeRequest( String subjectToken, @@ -131,13 +131,14 @@ public boolean hasActingParty() { } public static class Builder { - String subjectToken; - String subjectTokenType; - String resource; - String audience; - String requestedTokenType; - List scopes; - ActingParty actingParty; + private final String subjectToken; + private final String subjectTokenType; + + @Nullable private String resource; + @Nullable private String audience; + @Nullable private String requestedTokenType; + @Nullable private List scopes; + @Nullable private ActingParty actingParty; private Builder(String subjectToken, String subjectTokenType) { this.subjectToken = subjectToken; @@ -182,8 +183,8 @@ public StsTokenExchangeRequest build() { } static class ActingParty { - private String actorToken; - private String actorTokenType; + private final String actorToken; + private final String actorTokenType; public ActingParty(String actorToken, String actorTokenType) { this.actorToken = checkNotNull(actorToken); diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java index c51a9270a..c993653d6 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @@ -31,7 +31,7 @@ package com.google.auth.oauth2; -import static com.google.api.client.util.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkNotNull; import java.util.ArrayList; import java.util.Date; @@ -43,13 +43,13 @@ * 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; + private final AccessToken accessToken; + private final String issuedTokenType; + private final String tokenType; + private final Long expiresIn; - @Nullable private String refreshToken; - @Nullable private List scopes; + @Nullable private final String refreshToken; + @Nullable private final List scopes; private StsTokenExchangeResponse( String accessToken, @@ -96,14 +96,17 @@ public String getRefreshToken() { @Nullable public List getScopes() { + if (scopes == null) { + return null; + } return new ArrayList<>(scopes); } public static class Builder { - private String accessToken; - private String issuedTokenType; - private String tokenType; - private Long expiresIn; + private final String accessToken; + private final String issuedTokenType; + private final String tokenType; + private final Long expiresIn; @Nullable private String refreshToken; @Nullable private List scopes; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 7340e9791..878511770 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -188,7 +188,7 @@ public void fromJson_nullTransport_throws() { } @Test - public void exchange3PICredentialForAccessToken() throws IOException { + public void exchangeExternalCredentialForAccessToken() throws IOException { ExternalAccountCredentials credential = ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory); @@ -196,13 +196,13 @@ public void exchange3PICredentialForAccessToken() throws IOException { StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); AccessToken accessToken = - credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest); + credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest); assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); } @Test - public void exchange3PICredentialForAccessToken_withServiceAccountImpersonation() + public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersonation() throws IOException { transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); @@ -218,14 +218,14 @@ public void exchange3PICredentialForAccessToken_withServiceAccountImpersonation( StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); AccessToken returnedToken = - credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest); + credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest); assertEquals( transportFactory.transport.getServiceAccountAccessToken(), returnedToken.getTokenValue()); } @Test - public void exchange3PICredentialForAccessToken_throws() throws IOException { + public void exchangeExternalCredentialForAccessToken_throws() throws IOException { final ExternalAccountCredentials credential = ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory); @@ -239,7 +239,7 @@ public void exchange3PICredentialForAccessToken_throws() throws IOException { StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); try { - credential.exchange3PICredentialForAccessToken(stsTokenExchangeRequest); + credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest); fail("Exception should be thrown."); } catch (OAuthException e) { assertEquals(errorCode, e.getErrorCode()); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java index 46e2add39..19cf9eba8 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -55,7 +55,7 @@ import java.util.Queue; /** - * Mock transport that handles the necessary steps to exchange a 3PI credential for a GCP + * Mock transport that handles the necessary steps to exchange an external credential for a GCP * access-token. */ public class MockExternalAccountCredentialsTransport extends MockHttpTransport { From 25c884bd7dbc5f4133a7f61d89977e72149a691f Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Fri, 12 Feb 2021 12:08:54 -0800 Subject: [PATCH 18/23] fix: more review comments --- .../google/auth/oauth2/AwsCredentials.java | 39 ++++++++++--------- .../google/auth/oauth2/AwsRequestSigner.java | 2 +- .../oauth2/ExternalAccountCredentials.java | 34 ++++++++-------- .../auth/oauth2/IdentityPoolCredentials.java | 29 ++++++++------ .../google/auth/oauth2/OAuthException.java | 3 +- .../google/auth/oauth2/StsRequestHandler.java | 2 +- .../auth/oauth2/AwsCredentialsTest.java | 33 ++++++++++++++++ .../oauth2/IdentityPoolCredentialsTest.java | 33 ++++++++++++++++ ...ckExternalAccountCredentialsTransport.java | 2 +- .../auth/oauth2/StsRequestHandlerTest.java | 2 +- 10 files changed, 126 insertions(+), 53 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index bc987a1b6..2c1c85223 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -112,6 +112,8 @@ static class AwsCredentialSource extends CredentialSource { } } + private final AwsCredentialSource awsCredentialSource; + /** * Internal constructor. See {@link * ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String, @@ -141,15 +143,17 @@ static class AwsCredentialSource extends CredentialSource { clientId, clientSecret, scopes); + this.awsCredentialSource = credentialSource; } @Override public AccessToken refreshAccessToken() throws IOException { StsTokenExchangeRequest.Builder stsTokenExchangeRequest = - StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), subjectTokenType) - .setAudience(audience); + StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), getSubjectTokenType()) + .setAudience(getAudience()); // Add scopes, if possible. + Collection scopes = getScopes(); if (scopes != null && !scopes.isEmpty()) { stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); } @@ -167,14 +171,13 @@ public String retrieveSubjectToken() throws IOException { // Generate the signed request to the AWS STS GetCallerIdentity API. Map headers = new HashMap<>(); - headers.put("x-goog-cloud-target-resource", audience); + headers.put("x-goog-cloud-target-resource", getAudience()); AwsRequestSigner signer = AwsRequestSigner.newBuilder( credentials, "POST", - ((AwsCredentialSource) credentialSource) - .regionalCredentialVerificationUrl.replace("{region}", region), + awsCredentialSource.regionalCredentialVerificationUrl.replace("{region}", region), region) .setAdditionalHeaders(headers) .build(); @@ -188,15 +191,15 @@ public String retrieveSubjectToken() throws IOException { public GoogleCredentials createScoped(Collection newScopes) { return new AwsCredentials( transportFactory, - audience, - subjectTokenType, - tokenUrl, - (AwsCredentialSource) credentialSource, - tokenInfoUrl, - serviceAccountImpersonationUrl, - quotaProjectId, - clientId, - clientSecret, + getAudience(), + getSubjectTokenType(), + getTokenUrl(), + awsCredentialSource, + getTokenInfoUrl(), + getServiceAccountImpersonationUrl(), + getQuotaProjectId(), + getClientId(), + getClientSecret(), newScopes); } @@ -222,7 +225,7 @@ private String buildSubjectToken(AwsRequestSignature signature) 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)); + headerList.add(formatTokenHeaderForSts("x-goog-cloud-target-resource", getAudience())); GenericJson token = new GenericJson(); token.setFactory(OAuth2Utils.JSON_FACTORY); @@ -231,8 +234,8 @@ private String buildSubjectToken(AwsRequestSignature signature) token.put("method", signature.getHttpMethod()); token.put( "url", - ((AwsCredentialSource) credentialSource) - .regionalCredentialVerificationUrl.replace("{region}", signature.getRegion())); + awsCredentialSource.regionalCredentialVerificationUrl.replace( + "{region}", signature.getRegion())); return URLEncoder.encode(token.toString(), "UTF-8"); } @@ -243,7 +246,6 @@ private String getAwsRegion() throws IOException { return region; } - AwsCredentialSource awsCredentialSource = (AwsCredentialSource) this.credentialSource; if (awsCredentialSource.regionUrl == null || awsCredentialSource.regionUrl.isEmpty()) { throw new IOException( "Unable to determine the AWS region. The credential source does not contain the region URL."); @@ -267,7 +269,6 @@ AwsSecurityCredentials getAwsSecurityCredentials() throws IOException { } // 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. if (awsCredentialSource.url == null || awsCredentialSource.url.isEmpty()) { diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java index 35c106acf..0d5bea499 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @@ -34,8 +34,8 @@ import static com.google.common.base.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.Joiner; import com.google.common.base.Splitter; import com.google.common.io.BaseEncoding; import java.net.URI; diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index b13890e13..06c2e9cd5 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -31,7 +31,6 @@ package com.google.auth.oauth2; -import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.json.GenericJson; @@ -39,6 +38,7 @@ import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource; import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource; +import com.google.common.base.MoreObjects; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -53,7 +53,7 @@ /** * Base external account credentials class. * - *

Handles initializing third-party credentials, calls to STS and service account impersonation. + *

Handles initializing external credentials, calls to STS, and service account impersonation. */ public abstract class ExternalAccountCredentials extends GoogleCredentials implements QuotaProjectIdProvider { @@ -71,18 +71,18 @@ protected CredentialSource(Map credentialSourceMap) { 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 CredentialSource credentialSource; - protected final Collection scopes; + private final String transportFactoryClassName; + private final String audience; + private final String subjectTokenType; + private final String tokenUrl; + private final CredentialSource credentialSource; + private final Collection scopes; - @Nullable protected final String tokenInfoUrl; - @Nullable protected final String serviceAccountImpersonationUrl; - @Nullable protected final String quotaProjectId; - @Nullable protected final String clientId; - @Nullable protected final String clientSecret; + @Nullable private final String tokenInfoUrl; + @Nullable private final String serviceAccountImpersonationUrl; + @Nullable private final String quotaProjectId; + @Nullable private final String clientId; + @Nullable private final String clientSecret; protected transient HttpTransportFactory transportFactory; @@ -122,7 +122,7 @@ protected ExternalAccountCredentials( @Nullable String clientSecret, @Nullable Collection scopes) { this.transportFactory = - firstNonNull( + MoreObjects.firstNonNull( transportFactory, getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName()); @@ -177,7 +177,7 @@ public Map> getRequestMetadata(URI uri) throws IOException /** * Returns credentials defined by a JSON file stream. * - *

This will either return {@link IdentityPoolCredentials} or AwsCredentials. + *

Returns {@link IdentityPoolCredentials} or {@link AwsCredentials}. * * @param credentialsStream the stream with the credential definition * @return the credential defined by the credentialsStream @@ -191,7 +191,7 @@ public static ExternalAccountCredentials fromStream(InputStream credentialsStrea /** * Returns credentials defined by a JSON file stream. * - *

This will either return a IdentityPoolCredentials or AwsCredentials. + *

Returns a {@link IdentityPoolCredentials} or {@link AwsCredentials}. * * @param credentialsStream the stream with the credential definition * @param transportFactory the HTTP transport factory used to create the transport to get access @@ -217,7 +217,7 @@ public static ExternalAccountCredentials fromStream( * @param transportFactory HTTP transport factory, creates the transport used to get access tokens * @return the credentials defined by the JSON */ - public static ExternalAccountCredentials fromJson( + static ExternalAccountCredentials fromJson( Map json, HttpTransportFactory transportFactory) { checkNotNull(json); checkNotNull(transportFactory); diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 307ae34d6..5d11a6176 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -102,9 +102,14 @@ enum CredentialFormatType { * *

Optional headers can be present, and should be keyed by `headers`. */ - public IdentityPoolCredentialSource(Map credentialSourceMap) { + IdentityPoolCredentialSource(Map credentialSourceMap) { super(credentialSourceMap); + if (credentialSourceMap.containsKey("file") && credentialSourceMap.containsKey("url")) { + throw new IllegalArgumentException( + "Only one credential source type can be set, either file or url."); + } + if (credentialSourceMap.containsKey("file")) { credentialLocation = (String) credentialSourceMap.get("file"); credentialSourceType = IdentityPoolCredentialSourceType.FILE; @@ -186,8 +191,10 @@ private boolean hasHeaders() { public AccessToken refreshAccessToken() throws IOException { String credential = retrieveSubjectToken(); StsTokenExchangeRequest.Builder stsTokenExchangeRequest = - StsTokenExchangeRequest.newBuilder(credential, subjectTokenType).setAudience(audience); + StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType()) + .setAudience(getAudience()); + Collection scopes = getScopes(); if (scopes != null && !scopes.isEmpty()) { stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); } @@ -264,15 +271,15 @@ private String getSubjectTokenFromMetadataServer() throws IOException { public GoogleCredentials createScoped(Collection newScopes) { return new IdentityPoolCredentials( transportFactory, - audience, - subjectTokenType, - tokenUrl, - (IdentityPoolCredentialSource) credentialSource, - tokenInfoUrl, - serviceAccountImpersonationUrl, - quotaProjectId, - clientId, - clientSecret, + getAudience(), + getSubjectTokenType(), + getTokenUrl(), + identityPoolCredentialSource, + getTokenInfoUrl(), + getServiceAccountImpersonationUrl(), + getQuotaProjectId(), + getClientId(), + getClientSecret(), newScopes); } diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java index 057e51748..c85d778c0 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java @@ -46,8 +46,7 @@ class OAuthException extends IOException { @Nullable private final String errorDescription; @Nullable private final String errorUri; - public OAuthException( - String errorCode, @Nullable String errorDescription, @Nullable String errorUri) { + OAuthException(String errorCode, @Nullable String errorDescription, @Nullable String errorUri) { this.errorCode = checkNotNull(errorCode); this.errorDescription = errorDescription; this.errorUri = errorUri; diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index b0b5dcb4a..fdb5d1ae2 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -42,7 +42,7 @@ 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 com.google.common.base.Joiner; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 0eb37a08f..9f000a897 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -407,6 +407,39 @@ public void credentialSource_missingRegionalCredVerificationUrl() { } } + @Test + public void builder() { + List scopes = Arrays.asList("scope1", "scope2"); + + AwsCredentials credentials = + (AwsCredentials) + AwsCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl("tokenUrl") + .setCredentialSource(AWS_CREDENTIAL_SOURCE) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + + assertEquals("audience", credentials.getAudience()); + assertEquals("subjectTokenType", credentials.getSubjectTokenType()); + assertEquals(credentials.getTokenUrl(), "tokenUrl"); + assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl"); + assertEquals( + credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL); + assertEquals(credentials.getCredentialSource(), AWS_CREDENTIAL_SOURCE); + assertEquals(credentials.getQuotaProjectId(), "quotaProjectId"); + assertEquals(credentials.getClientId(), "clientId"); + assertEquals(credentials.getClientSecret(), "clientSecret"); + assertEquals(credentials.getScopes(), scopes); + } + private static AwsCredentialSource buildAwsCredentialSource( MockExternalAccountCredentialsTransportFactory transportFactory) { Map credentialSourceMap = new HashMap<>(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index c55926d7f..5a2279a20 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -392,6 +392,39 @@ public void identityPoolCredentialSource_subjectTokenFieldNameUnset() { } } + @Test + public void builder() { + List scopes = Arrays.asList("scope1", "scope2"); + + IdentityPoolCredentials credentials = + (IdentityPoolCredentials) + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl("tokenUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setTokenInfoUrl("tokenInfoUrl") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + + assertEquals("audience", credentials.getAudience()); + assertEquals("subjectTokenType", credentials.getSubjectTokenType()); + assertEquals(credentials.getTokenUrl(), "tokenUrl"); + assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl"); + assertEquals( + credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL); + assertEquals(credentials.getCredentialSource(), FILE_CREDENTIAL_SOURCE); + assertEquals(credentials.getQuotaProjectId(), "quotaProjectId"); + assertEquals(credentials.getClientId(), "clientId"); + assertEquals(credentials.getClientSecret(), "clientSecret"); + assertEquals(credentials.getScopes(), scopes); + } + static InputStream writeIdentityPoolCredentialsStream( String tokenUrl, String url, @Nullable String serviceAccountImpersonationUrl) 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 19cf9eba8..fc7e0cdb9 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -44,8 +44,8 @@ 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 com.google.common.base.Joiner; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java index 4e54e764e..d712bd241 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java @@ -38,9 +38,9 @@ import com.google.api.client.http.HttpHeaders; 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 com.google.common.base.Joiner; import java.io.IOException; import java.util.Arrays; import java.util.List; From bd49373fccc5ca72d50996457f9742094e5217a8 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Fri, 12 Feb 2021 16:14:04 -0800 Subject: [PATCH 19/23] fix: review --- .../google/auth/oauth2/AwsCredentials.java | 4 +-- .../auth/oauth2/IdentityPoolCredentials.java | 2 +- .../google/auth/oauth2/StsRequestHandler.java | 2 +- .../auth/oauth2/StsTokenExchangeRequest.java | 2 +- .../auth/oauth2/StsTokenExchangeResponse.java | 27 ++++++++++--------- .../auth/oauth2/AwsCredentialsTest.java | 16 +++++------ .../ExternalAccountCredentialsTest.java | 7 +++-- .../oauth2/IdentityPoolCredentialsTest.java | 14 +++++----- .../auth/oauth2/StsRequestHandlerTest.java | 4 +-- 9 files changed, 40 insertions(+), 38 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 2c1c85223..1dcdb17fa 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -324,9 +324,9 @@ public static AwsCredentials.Builder newBuilder(AwsCredentials awsCredentials) { public static class Builder extends ExternalAccountCredentials.Builder { - protected Builder() {} + Builder() {} - protected Builder(AwsCredentials credentials) { + Builder(AwsCredentials credentials) { super(credentials); } diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 5d11a6176..9e05cbf1f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -59,7 +59,7 @@ /** * Url-sourced and file-sourced external account credentials. * - *

By default, attempts to exchange the third-party credential for a GCP access token. + *

By default, attempts to exchange the external credential for a GCP access token. */ public class IdentityPoolCredentials extends ExternalAccountCredentials { diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index fdb5d1ae2..7326a6936 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -50,7 +50,7 @@ import javax.annotation.Nullable; /** Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693. */ -public class StsRequestHandler { +public final 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 = diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java index a30e437ef..c98e7f4eb 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @@ -40,7 +40,7 @@ * Defines an OAuth 2.0 token exchange request. Based on * https://tools.ietf.org/html/rfc8693#section-2.1. */ -public class StsTokenExchangeRequest { +public final class StsTokenExchangeRequest { private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; private final String subjectToken; diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java index c993653d6..e39ca6941 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @@ -42,11 +42,11 @@ * Defines an OAuth 2.0 token exchange successful response. Based on * https://tools.ietf.org/html/rfc8693#section-2.2.1. */ -public class StsTokenExchangeResponse { +public final class StsTokenExchangeResponse { private final AccessToken accessToken; private final String issuedTokenType; private final String tokenType; - private final Long expiresIn; + private final Long expiresInSeconds; @Nullable private final String refreshToken; @Nullable private final List scopes; @@ -55,12 +55,12 @@ private StsTokenExchangeResponse( String accessToken, String issuedTokenType, String tokenType, - Long expiresIn, + Long expiresInSeconds, @Nullable String refreshToken, @Nullable List scopes) { checkNotNull(accessToken); - this.expiresIn = checkNotNull(expiresIn); - long expiresAtMilliseconds = System.currentTimeMillis() + expiresIn * 1000L; + this.expiresInSeconds = checkNotNull(expiresInSeconds); + long expiresAtMilliseconds = System.currentTimeMillis() + expiresInSeconds * 1000L; this.accessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds)); this.issuedTokenType = checkNotNull(issuedTokenType); this.tokenType = checkNotNull(tokenType); @@ -85,8 +85,8 @@ public String getTokenType() { return tokenType; } - public Long getExpiresIn() { - return expiresIn; + public Long getExpiresInSeconds() { + return expiresInSeconds; } @Nullable @@ -106,16 +106,17 @@ public static class Builder { private final String accessToken; private final String issuedTokenType; private final String tokenType; - private final Long expiresIn; + private final Long expiresInSeconds; @Nullable private String refreshToken; @Nullable private List scopes; - private Builder(String accessToken, String issuedTokenType, String tokenType, Long expiresIn) { + private Builder( + String accessToken, String issuedTokenType, String tokenType, Long expiresInSeconds) { this.accessToken = accessToken; this.issuedTokenType = issuedTokenType; this.tokenType = tokenType; - this.expiresIn = expiresIn; + this.expiresInSeconds = expiresInSeconds; } public StsTokenExchangeResponse.Builder setRefreshToken(String refreshToken) { @@ -124,13 +125,15 @@ public StsTokenExchangeResponse.Builder setRefreshToken(String refreshToken) { } public StsTokenExchangeResponse.Builder setScopes(List scopes) { - this.scopes = scopes; + if (scopes != null) { + this.scopes = new ArrayList<>(scopes); + } return this; } public StsTokenExchangeResponse build() { return new StsTokenExchangeResponse( - accessToken, issuedTokenType, tokenType, expiresIn, refreshToken, scopes); + accessToken, issuedTokenType, tokenType, expiresInSeconds, refreshToken, scopes); } } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 9f000a897..dc86a516f 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -172,7 +172,7 @@ public void retrieveSubjectToken_noRegion_expectThrows() { IOException response = new IOException(); transportFactory.transport.addResponseErrorSequence(response); - final AwsCredentials awsCredential = + AwsCredentials awsCredential = (AwsCredentials) AwsCredentials.newBuilder(AWS_CREDENTIAL) .setHttpTransportFactory(transportFactory) @@ -196,7 +196,7 @@ public void retrieveSubjectToken_noRole_expectThrows() { transportFactory.transport.addResponseErrorSequence(response); transportFactory.transport.addResponseSequence(true, false); - final AwsCredentials awsCredential = + AwsCredentials awsCredential = (AwsCredentials) AwsCredentials.newBuilder(AWS_CREDENTIAL) .setHttpTransportFactory(transportFactory) @@ -220,7 +220,7 @@ public void retrieveSubjectToken_noCredentials_expectThrows() { transportFactory.transport.addResponseErrorSequence(response); transportFactory.transport.addResponseSequence(true, true, false); - final AwsCredentials awsCredential = + AwsCredentials awsCredential = (AwsCredentials) AwsCredentials.newBuilder(AWS_CREDENTIAL) .setHttpTransportFactory(transportFactory) @@ -244,7 +244,7 @@ public void retrieveSubjectToken_noRegionUrlProvided() { credentialSource.put("environment_id", "aws1"); credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); - final AwsCredentials awsCredential = + AwsCredentials awsCredential = (AwsCredentials) AwsCredentials.newBuilder(AWS_CREDENTIAL) .setHttpTransportFactory(transportFactory) @@ -317,7 +317,7 @@ public void getAwsSecurityCredentials_fromMetadataServer_noUrlProvided() { credentialSource.put("environment_id", "aws1"); credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); - final AwsCredentials awsCredential = + AwsCredentials awsCredential = (AwsCredentials) AwsCredentials.newBuilder(AWS_CREDENTIAL) .setHttpTransportFactory(transportFactory) @@ -365,7 +365,7 @@ public void createdScoped_clonedCredentialWithAddedScopes() { @Test public void credentialSource_invalidAwsEnvironmentId() { - final Map credentialSource = new HashMap<>(); + Map credentialSource = new HashMap<>(); credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); credentialSource.put("environment_id", "azure1"); @@ -379,7 +379,7 @@ public void credentialSource_invalidAwsEnvironmentId() { @Test public void credentialSource_invalidAwsEnvironmentVersion() { - final Map credentialSource = new HashMap<>(); + Map credentialSource = new HashMap<>(); int environmentVersion = 2; credentialSource.put("regional_cred_verification_url", GET_CALLER_IDENTITY_URL); credentialSource.put("environment_id", "aws" + environmentVersion); @@ -506,7 +506,7 @@ public static TestAwsCredentials.Builder newBuilder(AwsCredentials awsCredential public static class Builder extends AwsCredentials.Builder { - protected Builder(AwsCredentials credentials) { + private Builder(AwsCredentials credentials) { super(credentials); } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 878511770..7e1cab5cc 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -82,7 +82,6 @@ public void setup() { @Test public void fromStream_identityPoolCredentials() throws IOException { GenericJson json = buildJsonIdentityPoolCredential(); - TestUtils.jsonToInputStream(json); ExternalAccountCredentials credential = ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json)); @@ -163,7 +162,7 @@ public void fromJson_nullJson_throws() { @Test public void fromJson_invalidServiceAccountImpersonationUrl_throws() { - final GenericJson json = buildJsonIdentityPoolCredential(); + GenericJson json = buildJsonIdentityPoolCredential(); json.put("service_account_impersonation_url", "invalid_url"); try { @@ -226,7 +225,7 @@ public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersona @Test public void exchangeExternalCredentialForAccessToken_throws() throws IOException { - final ExternalAccountCredentials credential = + ExternalAccountCredentials credential = ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory); String errorCode = "invalidRequest"; @@ -235,7 +234,7 @@ public void exchangeExternalCredentialForAccessToken_throws() throws IOException transportFactory.transport.addResponseErrorSequence( TestUtils.buildHttpResponseException(errorCode, errorDescription, errorUri)); - final StsTokenExchangeRequest stsTokenExchangeRequest = + StsTokenExchangeRequest stsTokenExchangeRequest = StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); try { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 5a2279a20..98674f5e4 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -198,7 +198,7 @@ public void retrieveSubjectToken_noFile_throws() { IdentityPoolCredentialSource credentialSource = new IdentityPoolCredentialSource(credentialSourceMap); - final IdentityPoolCredentials credentials = + IdentityPoolCredentials credentials = (IdentityPoolCredentials) IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) .setCredentialSource(credentialSource) @@ -266,7 +266,7 @@ public void retrieveSubjectToken_urlSourcedCredential_throws() { IOException response = new IOException(); transportFactory.transport.addResponseErrorSequence(response); - final IdentityPoolCredentials credential = + IdentityPoolCredentials credential = (IdentityPoolCredentials) IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) .setHttpTransportFactory(transportFactory) @@ -290,7 +290,7 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc MockExternalAccountCredentialsTransportFactory transportFactory = new MockExternalAccountCredentialsTransportFactory(); - final IdentityPoolCredentials credential = + IdentityPoolCredentials credential = (IdentityPoolCredentials) IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) .setTokenUrl(transportFactory.transport.getStsUrl()) @@ -310,7 +310,7 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept new MockExternalAccountCredentialsTransportFactory(); transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); - final IdentityPoolCredentials credential = + IdentityPoolCredentials credential = (IdentityPoolCredentials) IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) .setTokenUrl(transportFactory.transport.getStsUrl()) @@ -341,7 +341,7 @@ public void identityPoolCredentialSource_invalidSourceType() { @Test public void identityPoolCredentialSource_invalidFormatType() { - final Map credentialSourceMap = new HashMap<>(); + Map credentialSourceMap = new HashMap<>(); credentialSourceMap.put("url", "url"); Map format = new HashMap<>(); @@ -358,7 +358,7 @@ public void identityPoolCredentialSource_invalidFormatType() { @Test public void identityPoolCredentialSource_nullFormatType() { - final Map credentialSourceMap = new HashMap<>(); + Map credentialSourceMap = new HashMap<>(); credentialSourceMap.put("url", "url"); Map format = new HashMap<>(); @@ -375,7 +375,7 @@ public void identityPoolCredentialSource_nullFormatType() { @Test public void identityPoolCredentialSource_subjectTokenFieldNameUnset() { - final Map credentialSourceMap = new HashMap<>(); + Map credentialSourceMap = new HashMap<>(); credentialSourceMap.put("url", "url"); Map format = new HashMap<>(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java index d712bd241..4b20bbcd1 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java @@ -88,7 +88,7 @@ public void exchangeToken() throws IOException { assertEquals(transport.getAccessToken(), response.getAccessToken().getTokenValue()); assertEquals(transport.getTokenType(), response.getTokenType()); assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType()); - assertEquals(transport.getExpiresIn(), response.getExpiresIn()); + assertEquals(transport.getExpiresIn(), response.getExpiresInSeconds()); // Validate request content. GenericData expectedRequestContent = @@ -139,7 +139,7 @@ public void exchangeToken_withOptionalParams() throws IOException { assertEquals(transport.getAccessToken(), response.getAccessToken().getTokenValue()); assertEquals(transport.getTokenType(), response.getTokenType()); assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType()); - assertEquals(transport.getExpiresIn(), response.getExpiresIn()); + assertEquals(transport.getExpiresIn(), response.getExpiresInSeconds()); assertEquals(Arrays.asList("scope1", "scope2", "scope3"), response.getScopes()); assertEquals("refreshToken", response.getRefreshToken()); From de0960df3d087050d99289c06846819d417e4315 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Fri, 12 Feb 2021 16:38:02 -0800 Subject: [PATCH 20/23] fix: review --- .../com/google/auth/oauth2/IdentityPoolCredentials.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 9e05cbf1f..25becfa12 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -268,7 +268,7 @@ private String getSubjectTokenFromMetadataServer() throws IOException { /** Clones the IdentityPoolCredentials with the specified scopes. */ @Override - public GoogleCredentials createScoped(Collection newScopes) { + public IdentityPoolCredentials createScoped(Collection newScopes) { return new IdentityPoolCredentials( transportFactory, getAudience(), @@ -293,9 +293,9 @@ public static Builder newBuilder(IdentityPoolCredentials identityPoolCredentials public static class Builder extends ExternalAccountCredentials.Builder { - protected Builder() {} + Builder() {} - protected Builder(IdentityPoolCredentials credentials) { + Builder(IdentityPoolCredentials credentials) { super(credentials); } From c9ee28216a43fa931b10a791267c25b3f88fa1cd Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Wed, 17 Feb 2021 12:45:14 -0800 Subject: [PATCH 21/23] fix: review --- .../com/google/auth/oauth2/ActingParty.java | 56 +++++++++++ .../java/com/google/auth/oauth2/AwsDates.java | 99 +++++++++++++++++++ .../google/auth/oauth2/AwsRequestSigner.java | 63 ------------ .../oauth2/ExternalAccountCredentials.java | 4 +- .../auth/oauth2/IdentityPoolCredentials.java | 2 +- .../google/auth/oauth2/OAuthException.java | 6 +- .../google/auth/oauth2/StsRequestHandler.java | 2 +- .../auth/oauth2/StsTokenExchangeRequest.java | 20 +--- .../auth/oauth2/StsTokenExchangeResponse.java | 2 +- .../oauth2/IdentityPoolCredentialsTest.java | 21 ++++ .../auth/oauth2/StsRequestHandlerTest.java | 1 - 11 files changed, 185 insertions(+), 91 deletions(-) create mode 100644 oauth2_http/java/com/google/auth/oauth2/ActingParty.java create mode 100644 oauth2_http/java/com/google/auth/oauth2/AwsDates.java diff --git a/oauth2_http/java/com/google/auth/oauth2/ActingParty.java b/oauth2_http/java/com/google/auth/oauth2/ActingParty.java new file mode 100644 index 000000000..ad1d452fc --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/ActingParty.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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 LLC 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; + +/** + * The acting party as defined in OAuth 2.0 Token + * Exchange. + */ +final class ActingParty { + private final String actorToken; + private final String actorTokenType; + + ActingParty(String actorToken, String actorTokenType) { + this.actorToken = checkNotNull(actorToken); + this.actorTokenType = checkNotNull(actorTokenType); + } + + String getActorToken() { + return actorToken; + } + + String getActorTokenType() { + return actorTokenType; + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsDates.java b/oauth2_http/java/com/google/auth/oauth2/AwsDates.java new file mode 100644 index 000000000..abf81add9 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/AwsDates.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 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 LLC 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.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** Formats dates required for AWS Signature V4 request signing. */ +final class AwsDates { + private static final String X_AMZ_DATE_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; + private static final String HTTP_DATE_FORMAT = "E, dd MMM yyyy HH:mm:ss z"; + + private final String xAmzDate; + private final String originalDate; + + 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); + } + + /** + * 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); + } + + 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(HTTP_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); + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java index 0d5bea499..70d930bbe 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java @@ -42,17 +42,13 @@ 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; @@ -337,63 +333,4 @@ AwsRequestSigner build() { 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 final String xAmzDate; - private final String originalDate; - - 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/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 06c2e9cd5..c4f87dc28 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -61,7 +61,7 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials /** Base credential source class. Dictates the retrieval method of the external credential. */ abstract static class CredentialSource { - protected CredentialSource(Map credentialSourceMap) { + CredentialSource(Map credentialSourceMap) { checkNotNull(credentialSourceMap); } } @@ -295,7 +295,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken( } private static String extractTargetPrincipal(String serviceAccountImpersonationUrl) { - // Extract the target principle. + // Extract the target principal int startIndex = serviceAccountImpersonationUrl.lastIndexOf('/'); int endIndex = serviceAccountImpersonationUrl.indexOf(":generateAccessToken"); diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index 25becfa12..a82e3a638 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -133,7 +133,7 @@ enum CredentialFormatType { 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"))) { + if (!"text".equals(type) && !"json".equals(type)) { throw new IllegalArgumentException( String.format("Invalid credential source format type: %s.", type)); } diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java index c85d778c0..b3f612a04 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java @@ -65,17 +65,17 @@ public String getMessage() { return sb.toString(); } - public String getErrorCode() { + String getErrorCode() { return errorCode; } @Nullable - public String getErrorDescription() { + String getErrorDescription() { return errorDescription; } @Nullable - public String getErrorUri() { + 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 index 7326a6936..a6a14fcbf 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -50,7 +50,7 @@ import javax.annotation.Nullable; /** Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693. */ -public final class StsRequestHandler { +final 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 = diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java index c98e7f4eb..b9525bd68 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @@ -40,7 +40,7 @@ * Defines an OAuth 2.0 token exchange request. Based on * https://tools.ietf.org/html/rfc8693#section-2.1. */ -public final class StsTokenExchangeRequest { +final class StsTokenExchangeRequest { private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; private final String subjectToken; @@ -181,22 +181,4 @@ public StsTokenExchangeRequest build() { requestedTokenType); } } - - static class ActingParty { - private final String actorToken; - private final 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 index e39ca6941..a16f5a329 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @@ -42,7 +42,7 @@ * Defines an OAuth 2.0 token exchange successful response. Based on * https://tools.ietf.org/html/rfc8693#section-2.2.1. */ -public final class StsTokenExchangeResponse { +final class StsTokenExchangeResponse { private final AccessToken accessToken; private final String issuedTokenType; private final String tokenType; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 98674f5e4..4095edcad 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -190,6 +190,27 @@ public void retrieveSubjectToken_fileSourcedWithJsonFormat() throws IOException assertEquals("subjectToken", subjectToken); } + @Test + public void retrieveSubjectToken_fileSourcedWithNullFormat_throws() throws IOException { + File file = + File.createTempFile("RETRIEVE_SUBJECT_TOKEN", /* suffix= */ null, /* directory= */ null); + file.deleteOnExit(); + + Map credentialSourceMap = new HashMap<>(); + Map formatMap = new HashMap<>(); + formatMap.put("type", null); + + credentialSourceMap.put("file", file.getAbsolutePath()); + credentialSourceMap.put("format", formatMap); + + try { + new IdentityPoolCredentialSource(credentialSourceMap); + fail("Exception should be thrown due to null format."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid credential source format type: null.", e.getMessage()); + } + } + @Test public void retrieveSubjectToken_noFile_throws() { Map credentialSourceMap = new HashMap<>(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java index 4b20bbcd1..65d2bf90f 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java @@ -39,7 +39,6 @@ import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.api.client.util.GenericData; import com.google.auth.TestUtils; -import com.google.auth.oauth2.StsTokenExchangeRequest.ActingParty; import com.google.common.base.Joiner; import java.io.IOException; import java.util.Arrays; From 623d878ed25fb1b1a627857ae921d59aced2044c Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Wed, 17 Feb 2021 13:38:28 -0800 Subject: [PATCH 22/23] fix: add CredentialFormatException --- .../oauth2/CredentialFormatException.java | 41 +++++++++++++++++++ .../oauth2/ExternalAccountCredentials.java | 6 ++- .../ExternalAccountCredentialsTest.java | 15 ++++++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 oauth2_http/java/com/google/auth/oauth2/CredentialFormatException.java diff --git a/oauth2_http/java/com/google/auth/oauth2/CredentialFormatException.java b/oauth2_http/java/com/google/auth/oauth2/CredentialFormatException.java new file mode 100644 index 000000000..4186bc029 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/CredentialFormatException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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 LLC 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.io.IOException; + +/** Indicates that the provided credential does not adhere to the required format. */ +class CredentialFormatException extends IOException { + CredentialFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index c4f87dc28..b044b6485 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -207,7 +207,11 @@ public static ExternalAccountCredentials fromStream( JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); GenericJson fileContents = parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class); - return fromJson(fileContents, transportFactory); + try { + return fromJson(fileContents, transportFactory); + } catch (ClassCastException e) { + throw new CredentialFormatException("An invalid input stream was provided.", e); + } } /** diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 7e1cab5cc..a9d3ce49b 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -92,7 +92,6 @@ public void fromStream_identityPoolCredentials() throws IOException { @Test public void fromStream_awsCredentials() throws IOException { GenericJson json = buildJsonAwsCredential(); - TestUtils.jsonToInputStream(json); ExternalAccountCredentials credential = ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json)); @@ -100,6 +99,20 @@ public void fromStream_awsCredentials() throws IOException { assertTrue(credential instanceof AwsCredentials); } + @Test + public void fromStream_invalidStream_throws() throws IOException { + GenericJson json = buildJsonAwsCredential(); + + json.put("audience", new HashMap<>()); + + try { + ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json)); + fail("Should fail."); + } catch (CredentialFormatException e) { + assertEquals("An invalid input stream was provided.", e.getMessage()); + } + } + @Test public void fromStream_nullTransport_throws() throws IOException { try { From 1fe5188ccd4a6a6d3d1ec5c3d9d5a664b0988596 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Wed, 17 Feb 2021 15:05:50 -0800 Subject: [PATCH 23/23] fix: review --- .../java/com/google/auth/oauth2/AwsCredentials.java | 4 ++-- .../auth/oauth2/ExternalAccountCredentials.java | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 1dcdb17fa..b12d4e1cf 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -93,8 +93,8 @@ static class AwsCredentialSource extends CredentialSource { String environmentId = (String) credentialSourceMap.get("environment_id"); // Environment version is prefixed by "aws". e.g. "aws1". - Matcher matcher = Pattern.compile("^(aws)([\\d]+)$").matcher(environmentId); - if (!matcher.find()) { + Matcher matcher = Pattern.compile("(aws)([\\d]+)").matcher(environmentId); + if (!matcher.matches()) { throw new IllegalArgumentException("Invalid AWS environment ID."); } diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index b044b6485..1373fcc54 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -234,13 +234,10 @@ static ExternalAccountCredentials fromJson( 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; - String quotaProjectId = - json.containsKey("quota_project_id") ? (String) json.get("quota_project_id") : null; + String tokenInfoUrl = (String) json.get("token_info_url"); + String clientId = (String) json.get("client_id"); + String clientSecret = (String) json.get("client_secret"); + String quotaProjectId = (String) json.get("quota_project_id"); if (isAwsCredential(credentialSourceMap)) { return new AwsCredentials(