-
Notifications
You must be signed in to change notification settings - Fork 263
feat: Implement ClientSideCredentialAccessBoundaryFactory #1562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d9574da
6b04cc1
5633cc1
c50a10f
62b501c
76d30b9
bd01ab3
a64cebd
272db77
2056ca3
5911813
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
nbayati marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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. | ||
| * | ||
| * <p>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: | ||
| * | ||
| * <ol> | ||
| * <li>If the STS response includes `expires_in`, that value is used. | ||
| * <li>Otherwise, if the source credential has an expiration time, that value is used. | ||
| * <li>Otherwise, the intermediary token will have no expiration time. | ||
| * </ol> | ||
| * | ||
| * @throws IOException If an error occurs during credential refresh or token exchange. | ||
| */ | ||
| private void refreshCredentials() throws IOException { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you be able to write some quick comments that describes some of the behavior regarding the source access token and the response access token and how they form the intermediate access token? From what I gather from the code, but it would be helpful for a comment to help confirm: What happens when both access tokens don't have an expiration time? Will this end up failing downstream? Will this just live forever?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added documentation to the function. Please let me know if additional clarification is needed.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, that makes sense! Is there any issue if the intermediary token doesn't have an expiration time?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the intermediary token should still work as expected without any issues even if there's no expiration time.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, would it be possible confirm that? If does end up failing, could you document that or link to docs that detail that? |
||
| try { | ||
| this.sourceCredential.refreshIfExpired(); | ||
| } catch (IOException e) { | ||
| throw new IOException("Unable to refresh the provided source credential.", e); | ||
| } | ||
|
Comment on lines
+81
to
+85
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you remind me why we are re-throwing this exception? If there is no reason, I think we should have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From @aeitzman: It's just to give more easily readable details about what part of the auth flow broke vs just returning the stack trace. I don't think its strictly necessary but it makes the library easier to debug.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'm fine with this for now, but I'd rather have this be captured via logs (that is probably a separate project). CC: @zhumin8 |
||
|
|
||
| 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(); | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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. | ||
nbayati marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (response.getAccessToken().getExpirationTime() == null) { | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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."); | ||
| } | ||
nbayati marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) { | ||
nbayati marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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())) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. qq, does CAB plan to restrict the sourceCredentials to a subset of GoogleCredentials (i.e. only Compute or SA)? Or can any GoogleCredential be used?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, client-side CAB only works with service account tokens. In the future we might add support for other types of tokens.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be helpful to add validation for that. One reason is that universe_domain retrieval should never throw an IOException for SACredentials. It was added for ComputeEngineCredentials because of MDS. Also it could be helpful to get an error message earlier, rather than finding out when trying to refreshCredentials |
||
| 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); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||||||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||||||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||||||
| <modelVersion>4.0.0</modelVersion> | ||||||||
| <parent> | ||||||||
| <groupId>com.google.auth</groupId> | ||||||||
| <artifactId>google-auth-library-parent</artifactId> | ||||||||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| <version>1.29.1-SNAPSHOT | ||||||||
| </version><!-- {x-version-update:google-auth-library-parent:current} --> | ||||||||
|
Comment on lines
+9
to
+10
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This version is going to change as we cut new releases of auth. Last thing before we merge will be to update this to the latest version.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll leave this comment open as a reminder to update the version before the feature is merged to the main branch. |
||||||||
| </parent> | ||||||||
|
|
||||||||
| <artifactId>cab-token-generator</artifactId> | ||||||||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| <name>Google Auth Library for Java - Cab Token Generator</name> | ||||||||
|
|
||||||||
| <build> | ||||||||
| <sourceDirectory>java</sourceDirectory> | ||||||||
| </build> | ||||||||
| <dependencies> | ||||||||
| <dependency> | ||||||||
| <groupId>com.google.auth</groupId> | ||||||||
| <artifactId>google-auth-library-oauth2-http</artifactId> | ||||||||
| </dependency> | ||||||||
| <dependency> | ||||||||
| <groupId>com.google.auth</groupId> | ||||||||
| <artifactId>google-auth-library-credentials</artifactId> | ||||||||
| </dependency> | ||||||||
| <dependency> | ||||||||
| <groupId>com.google.http-client</groupId> | ||||||||
| <artifactId>google-http-client</artifactId> | ||||||||
| </dependency> | ||||||||
| <dependency> | ||||||||
| <groupId>com.google.errorprone</groupId> | ||||||||
| <artifactId>error_prone_annotations</artifactId> | ||||||||
| </dependency> | ||||||||
| <dependency> | ||||||||
| <groupId>com.google.guava</groupId> | ||||||||
| <artifactId>guava</artifactId> | ||||||||
| </dependency> | ||||||||
| </dependencies> | ||||||||
|
|
||||||||
| </project> | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,11 +70,15 @@ | |
| import java.util.Set; | ||
|
|
||
| /** Internal utilities for the com.google.auth.oauth2 namespace. */ | ||
| class OAuth2Utils { | ||
| public class OAuth2Utils { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if it makes sense for this to be public @lqiu96
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One option for this to see if we can keep this package-private and move some of these constants to STSRequestHandler. It looks like
Otherwise, I think I'm ok with making it public given the module constraints we have.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to leave this comment open and keep it as-is for now so I can merge this PR and unblock #1571. I'll address this comment in the next PR. |
||
|
|
||
| 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(); | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.