diff --git a/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java b/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java new file mode 100644 index 000000000..26d2479d6 --- /dev/null +++ b/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java @@ -0,0 +1,209 @@ +/* + * Copyright 2024, 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.credentialaccessboundary; + +import static com.google.auth.oauth2.OAuth2Credentials.getFromServiceLoader; +import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auth.Credentials; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.CredentialAccessBoundary; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.OAuth2Utils; +import com.google.auth.oauth2.StsRequestHandler; +import com.google.auth.oauth2.StsTokenExchangeRequest; +import com.google.auth.oauth2.StsTokenExchangeResponse; +import com.google.common.base.Strings; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; + +public final class ClientSideCredentialAccessBoundaryFactory { + private final GoogleCredentials sourceCredential; + private final transient HttpTransportFactory transportFactory; + private final String tokenExchangeEndpoint; + private String accessBoundarySessionKey; + private AccessToken intermediateAccessToken; + + private ClientSideCredentialAccessBoundaryFactory(Builder builder) { + this.transportFactory = builder.transportFactory; + this.sourceCredential = builder.sourceCredential; + this.tokenExchangeEndpoint = builder.tokenExchangeEndpoint; + } + + /** + * Refreshes the source credential and exchanges it for an intermediary access token using the STS + * endpoint. + * + *

If the source credential is expired, it will be refreshed. A token exchange request is then + * made to the STS endpoint. The resulting intermediary access token and access boundary session + * key are stored. The intermediary access token's expiration time is determined as follows: + * + *

    + *
  1. If the STS response includes `expires_in`, that value is used. + *
  2. Otherwise, if the source credential has an expiration time, that value is used. + *
  3. Otherwise, the intermediary token will have no expiration time. + *
+ * + * @throws IOException If an error occurs during credential refresh or token exchange. + */ + private void refreshCredentials() throws IOException { + try { + this.sourceCredential.refreshIfExpired(); + } catch (IOException e) { + throw new IOException("Unable to refresh the provided source credential.", e); + } + + AccessToken sourceAccessToken = sourceCredential.getAccessToken(); + if (sourceAccessToken == null || Strings.isNullOrEmpty(sourceAccessToken.getTokenValue())) { + throw new IllegalStateException("The source credential does not have an access token."); + } + + StsTokenExchangeRequest request = + StsTokenExchangeRequest.newBuilder( + sourceAccessToken.getTokenValue(), OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN) + .setRequestTokenType(OAuth2Utils.TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN) + .build(); + + StsRequestHandler handler = + StsRequestHandler.newBuilder( + tokenExchangeEndpoint, request, transportFactory.create().createRequestFactory()) + .build(); + + StsTokenExchangeResponse response = handler.exchangeToken(); + this.accessBoundarySessionKey = response.getAccessBoundarySessionKey(); + this.intermediateAccessToken = response.getAccessToken(); + + // The STS endpoint will only return the expiration time for the intermediary token + // if the original access token represents a service account. + // The intermediary token's expiration time will always match the source credential expiration. + // When no expires_in is returned, we can copy the source credential's expiration time. + if (response.getAccessToken().getExpirationTime() == null) { + if (sourceAccessToken.getExpirationTime() != null) { + this.intermediateAccessToken = + new AccessToken( + response.getAccessToken().getTokenValue(), sourceAccessToken.getExpirationTime()); + } + } + } + + private void refreshCredentialsIfRequired() { + // TODO(negarb): Implement refreshCredentialsIfRequired + throw new UnsupportedOperationException("refreshCredentialsIfRequired is not yet implemented."); + } + + public AccessToken generateToken(CredentialAccessBoundary accessBoundary) { + // TODO(negarb/jiahuah): Implement generateToken + throw new UnsupportedOperationException("generateToken is not yet implemented."); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private GoogleCredentials sourceCredential; + private HttpTransportFactory transportFactory; + private String universeDomain; + private String tokenExchangeEndpoint; + + private Builder() {} + + /** + * Sets the required source credential. + * + * @param sourceCredential the {@code GoogleCredentials} to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setSourceCredential(GoogleCredentials sourceCredential) { + this.sourceCredential = sourceCredential; + return this; + } + + /** + * Sets the HTTP transport factory. + * + * @param transportFactory the {@code HttpTransportFactory} to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { + this.transportFactory = transportFactory; + return this; + } + + /** + * Sets the optional universe domain. + * + * @param universeDomain the universe domain to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setUniverseDomain(String universeDomain) { + this.universeDomain = universeDomain; + return this; + } + + public ClientSideCredentialAccessBoundaryFactory build() { + checkNotNull(sourceCredential, "Source credential must not be null."); + + // Use the default HTTP transport factory if none was provided. + if (transportFactory == null) { + this.transportFactory = + getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + } + + // Default to GDU when not supplied. + if (Strings.isNullOrEmpty(universeDomain)) { + this.universeDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE; + } + + // Ensure source credential's universe domain matches. + try { + if (!universeDomain.equals(sourceCredential.getUniverseDomain())) { + throw new IllegalArgumentException( + "The client side access boundary credential's universe domain must be the same as the source " + + "credential."); + } + } catch (IOException e) { + // Throwing an IOException would be a breaking change, so wrap it here. + throw new IllegalStateException( + "Error occurred when attempting to retrieve source credential universe domain.", e); + } + + this.tokenExchangeEndpoint = String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain); + return new ClientSideCredentialAccessBoundaryFactory(this); + } + } +} diff --git a/cab-token-generator/pom.xml b/cab-token-generator/pom.xml new file mode 100644 index 000000000..9b8e50341 --- /dev/null +++ b/cab-token-generator/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + com.google.auth + google-auth-library-parent + 1.29.1-SNAPSHOT + + + + cab-token-generator + Google Auth Library for Java - Cab Token Generator + + + java + + + + com.google.auth + google-auth-library-oauth2-http + + + com.google.auth + google-auth-library-credentials + + + com.google.http-client + google-http-client + + + com.google.errorprone + error_prone_annotations + + + com.google.guava + guava + + + + diff --git a/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java b/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java index 7232a234d..2207d34a3 100644 --- a/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java @@ -31,6 +31,7 @@ package com.google.auth.oauth2; +import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; @@ -88,7 +89,6 @@ */ public final class DownscopedCredentials extends OAuth2Credentials { - private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.{universe_domain}/v1/token"; private final GoogleCredentials sourceCredential; private final CredentialAccessBoundary credentialAccessBoundary; private final String universeDomain; @@ -125,8 +125,7 @@ private DownscopedCredentials(Builder builder) { throw new IllegalStateException( "Error occurred when attempting to retrieve source credential universe domain.", e); } - this.tokenExchangeEndpoint = - TOKEN_EXCHANGE_URL_FORMAT.replace("{universe_domain}", universeDomain); + this.tokenExchangeEndpoint = String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain); } @Override diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java index 20e1c92e5..3d569b02f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java @@ -484,7 +484,16 @@ protected static T newInstance(String className) throws IOException, ClassNo } } - protected static T getFromServiceLoader(Class clazz, T defaultInstance) { + /** + * Returns the first service provider from the given service loader. + * + * @param clazz The class of the service provider to load. + * @param defaultInstance The default instance to return if no service providers are found. + * @param The type of the service provider. + * @return The first service provider from the service loader, or the {@code defaultInstance} if + * no service providers are found. + */ + public static T getFromServiceLoader(Class clazz, T defaultInstance) { return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance); } diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java index 8c1e80096..9c3f295df 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java @@ -70,11 +70,15 @@ import java.util.Set; /** Internal utilities for the com.google.auth.oauth2 namespace. */ -class OAuth2Utils { +public class OAuth2Utils { + static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; - static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token"; + public static final String TOKEN_TYPE_ACCESS_TOKEN = + "urn:ietf:params:oauth:token-type:access_token"; static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange"; + public static final String TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN = + "urn:ietf:params:oauth:token-type:access_boundary_intermediary_token"; static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; // generateIdToken endpoint is to be formatted with universe domain and client email @@ -85,6 +89,7 @@ class OAuth2Utils { "https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateAccessToken"; static final String SIGN_BLOB_ENDPOINT_FORMAT = "https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:signBlob"; + public static final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.%s/v1/token"; static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token"); @@ -93,7 +98,8 @@ class OAuth2Utils { static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); - static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = new DefaultHttpTransportFactory(); + public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = + new DefaultHttpTransportFactory(); static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index ba7d27b81..bd7a2d21f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -50,8 +50,18 @@ import java.util.List; import javax.annotation.Nullable; -/** Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693. */ -final class StsRequestHandler { +/** + * Implements the OAuth 2.0 token exchange based on RFC 8693. + * + *

This class handles the process of exchanging one type of token for another using the Security + * Token Service (STS). It constructs and sends the token exchange request to the STS endpoint and + * parses the response to create an {@link StsTokenExchangeResponse} object. + * + *

Use the {@link #newBuilder(String, StsTokenExchangeRequest, HttpRequestFactory)} method to + * create a new builder for constructing an instance of this class. + */ +public final class StsRequestHandler { private static final String TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; private static final String PARSE_ERROR_PREFIX = "Error parsing token response."; @@ -85,6 +95,14 @@ private StsRequestHandler( this.internalOptions = internalOptions; } + /** + * Returns a new builder for creating an instance of {@link StsRequestHandler}. + * + * @param tokenExchangeEndpoint The STS token exchange endpoint. + * @param stsTokenExchangeRequest The STS token exchange request. + * @param httpRequestFactory The HTTP request factory to use for sending the request. + * @return A new builder instance. + */ public static Builder newBuilder( String tokenExchangeEndpoint, StsTokenExchangeRequest stsTokenExchangeRequest, @@ -175,6 +193,11 @@ private StsTokenExchangeResponse buildResponse(GenericData responseData) throws String scope = OAuth2Utils.validateString(responseData, "scope", PARSE_ERROR_PREFIX); builder.setScopes(Arrays.asList(scope.trim().split("\\s+"))); } + if (responseData.containsKey("access_boundary_session_key")) { + builder.setAccessBoundarySessionKey( + OAuth2Utils.validateString( + responseData, "access_boundary_session_key", PARSE_ERROR_PREFIX)); + } return builder.build(); } diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java index a231fe383..f0ce390ed 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @@ -38,10 +38,18 @@ import javax.annotation.Nullable; /** - * Defines an OAuth 2.0 token exchange request. Based on - * https://tools.ietf.org/html/rfc8693#section-2.1. + * Represents an OAuth 2.0 token exchange request, as defined in RFC 8693, Section 2.1. + * + *

This class encapsulates the parameters necessary for making a token exchange request to Google + * Security Token Service (STS). It includes the subject token, subject token type, optional + * parameters like acting party, scopes, resource, audience, requested token type, and internal + * options. + * + *

Instances of this class are immutable. Use the {@link #newBuilder(String, String)} method to + * create a new builder. */ -final class StsTokenExchangeRequest { +public final class StsTokenExchangeRequest { private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; private final String subjectToken; @@ -73,6 +81,15 @@ private StsTokenExchangeRequest( this.internalOptions = internalOptions; } + /** + * Returns a new {@link StsTokenExchangeRequest.Builder} instance. + * + * @param subjectToken The token being exchanged. This represents the credentials being used to + * authorize the token exchange request. + * @param subjectTokenType The type of the {@code subjectToken}. For example, {@link + * OAuth2Utils#TOKEN_TYPE_ACCESS_TOKEN}. + * @return A new builder for creating {@code StsTokenExchangeRequest} instances. + */ public static Builder newBuilder(String subjectToken, String subjectTokenType) { return new Builder(subjectToken, subjectTokenType); } diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java index 90a94e16d..62275778a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @@ -40,10 +40,18 @@ 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. + * Represents a successful OAuth 2.0 token exchange response from the Google Security Token Service + * (STS), as defined in RFC 8693, + * Section 2.2.1. + * + *

This class provides access to the exchanged access token, issued token type, token type, + * expiration time, refresh token (optional), scopes (optional), and the access boundary session key + * (optional). + * + *

Instances are immutable. Use {@link #newBuilder(String, String, String)} to create an + * instance. */ -final class StsTokenExchangeResponse { +public final class StsTokenExchangeResponse { private final AccessToken accessToken; private final String issuedTokenType; private final String tokenType; @@ -51,6 +59,7 @@ final class StsTokenExchangeResponse { @Nullable private final Long expiresInSeconds; @Nullable private final String refreshToken; @Nullable private final List scopes; + @Nullable private final String accessBoundarySessionKey; private StsTokenExchangeResponse( String accessToken, @@ -58,7 +67,8 @@ private StsTokenExchangeResponse( String tokenType, @Nullable Long expiresInSeconds, @Nullable String refreshToken, - @Nullable List scopes) { + @Nullable List scopes, + @Nullable String accessBoundarySessionKey) { checkNotNull(accessToken); this.expiresInSeconds = expiresInSeconds; @@ -71,8 +81,18 @@ private StsTokenExchangeResponse( this.tokenType = checkNotNull(tokenType); this.refreshToken = refreshToken; this.scopes = scopes; + this.accessBoundarySessionKey = accessBoundarySessionKey; } + /** + * Returns a new {@link StsTokenExchangeResponse.Builder} instance. + * + * @param accessToken The exchanged access token. + * @param issuedTokenType The issued token type. For example, {@link + * OAuth2Utils#TOKEN_TYPE_ACCESS_TOKEN}. + * @param tokenType The token type (e.g., "Bearer"). + * @return A new builder for creating {@link StsTokenExchangeResponse} instances. + */ public static Builder newBuilder(String accessToken, String issuedTokenType, String tokenType) { return new Builder(accessToken, issuedTokenType, tokenType); } @@ -107,6 +127,16 @@ public List getScopes() { return new ArrayList<>(scopes); } + /** + * Returns the access boundary session key if present. + * + * @return the access boundary session key or {@code null} if not present + */ + @Nullable + public String getAccessBoundarySessionKey() { + return accessBoundarySessionKey; + } + public static class Builder { private final String accessToken; private final String issuedTokenType; @@ -115,6 +145,7 @@ public static class Builder { @Nullable private Long expiresInSeconds; @Nullable private String refreshToken; @Nullable private List scopes; + @Nullable private String accessBoundarySessionKey; private Builder(String accessToken, String issuedTokenType, String tokenType) { this.accessToken = accessToken; @@ -142,9 +173,28 @@ public StsTokenExchangeResponse.Builder setScopes(List scopes) { return this; } + /** + * Sets the access boundary session key. + * + * @param accessBoundarySessionKey the access boundary session key to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public StsTokenExchangeResponse.Builder setAccessBoundarySessionKey( + String accessBoundarySessionKey) { + this.accessBoundarySessionKey = accessBoundarySessionKey; + return this; + } + public StsTokenExchangeResponse build() { return new StsTokenExchangeResponse( - accessToken, issuedTokenType, tokenType, expiresInSeconds, refreshToken, scopes); + accessToken, + issuedTokenType, + tokenType, + expiresInSeconds, + refreshToken, + scopes, + accessBoundarySessionKey); } } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java index 06fb13ebe..9dc5d4eea 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java @@ -32,6 +32,7 @@ package com.google.auth.oauth2; import static com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE; +import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; @@ -50,7 +51,6 @@ @RunWith(JUnit4.class) public class DownscopedCredentialsTest { - private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.%s/v1/token"; private static final String SA_PRIVATE_KEY_PKCS8 = "-----BEGIN PRIVATE KEY-----\n" + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i" diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java index 14ff35800..3a60809dc 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java @@ -65,12 +65,14 @@ public final class MockStsTransport extends MockHttpTransport { private static final String ACCESS_TOKEN = "accessToken"; private static final String TOKEN_TYPE = "Bearer"; private static final Long EXPIRES_IN = 3600L; + private static final String ACCESS_BOUNDARY_SESSION_KEY_VALUE = "accessBoundarySessionKey"; private final Queue responseErrorSequence = new ArrayDeque<>(); private final Queue> scopeSequence = new ArrayDeque<>(); private final Queue refreshTokenSequence = new ArrayDeque<>(); private boolean returnExpiresIn = true; + private boolean returnAccessBoundarySessionKey = false; private MockLowLevelHttpRequest request; public void addResponseErrorSequence(IOException... errors) { @@ -133,6 +135,11 @@ public LowLevelHttpResponse execute() throws IOException { response.put("refresh_token", refreshTokenSequence.poll()); } } + + if (returnAccessBoundarySessionKey) { + response.put("access_boundary_session_key", ACCESS_BOUNDARY_SESSION_KEY_VALUE); + } + return new MockLowLevelHttpResponse() .setContentType(Json.MEDIA_TYPE) .setContent(response.toPrettyString()); @@ -169,7 +176,15 @@ public Long getExpiresIn() { return EXPIRES_IN; } + public String getAccessBoundarySessionKey() { + return ACCESS_BOUNDARY_SESSION_KEY_VALUE; + } + public void setReturnExpiresIn(boolean returnExpiresIn) { this.returnExpiresIn = returnExpiresIn; } + + public void setReturnAccessBoundarySessionKey(boolean returnAccessBoundarySessionKey) { + this.returnAccessBoundarySessionKey = returnAccessBoundarySessionKey; + } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java index dd0cc7ce9..eb9294ac1 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java @@ -275,4 +275,22 @@ public void exchangeToken_noExpiresInReturned() throws IOException { assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType()); assertNull(response.getExpiresInSeconds()); } + + @Test + public void exchangeToken_withAccessBoundarySessionKey() throws IOException { + transport.setReturnAccessBoundarySessionKey(/* returnAccessBoundarySessionKey= */ true); + + StsTokenExchangeRequest stsTokenExchangeRequest = + StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); + + StsRequestHandler requestHandler = + StsRequestHandler.newBuilder( + TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) + .build(); + + StsTokenExchangeResponse response = requestHandler.exchangeToken(); + + // Validate response. + assertEquals(transport.getAccessBoundarySessionKey(), response.getAccessBoundarySessionKey()); + } } diff --git a/pom.xml b/pom.xml index 42552b38b..205b2933b 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ oauth2_http appengine bom + cab-token-generator