Skip to content

Commit d9574da

Browse files
committed
feat: Implement ClientSideCredentialAccessBoundaryFactory
Set up the ClientSideCredentialAccessBoundaryFactory class and module. Implement the function to fetch intermediary tokens from STS.
1 parent f33d84c commit d9574da

File tree

7 files changed

+189
-10
lines changed

7 files changed

+189
-10
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package com.google.auth.credentialaccessboundary;
2+
3+
import static com.google.auth.oauth2.OAuth2Credentials.getFromServiceLoader;
4+
import static com.google.common.base.MoreObjects.firstNonNull;
5+
import static com.google.common.base.Preconditions.checkNotNull;
6+
7+
import com.google.auth.Credentials;
8+
import com.google.auth.http.HttpTransportFactory;
9+
import com.google.auth.oauth2.AccessToken;
10+
import com.google.auth.oauth2.GoogleCredentials;
11+
import com.google.auth.oauth2.OAuth2Utils;
12+
import com.google.auth.oauth2.StsRequestHandler;
13+
import com.google.auth.oauth2.StsTokenExchangeRequest;
14+
import com.google.auth.oauth2.StsTokenExchangeResponse;
15+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
16+
import java.io.IOException;
17+
18+
public final class ClientSideCredentialAccessBoundaryFactory {
19+
private final GoogleCredentials sourceCredential;
20+
private final transient HttpTransportFactory transportFactory;
21+
private final String tokenExchangeEndpoint;
22+
private String acceessBoundarySessionKey;
23+
private AccessToken intermediaryAccessToken;
24+
25+
private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
26+
this.transportFactory =
27+
firstNonNull(
28+
builder.transportFactory,
29+
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
30+
this.sourceCredential = checkNotNull(builder.sourceCredential);
31+
32+
// Default to GDU when not supplied.
33+
String universeDomain;
34+
if (builder.universeDomain == null || builder.universeDomain.trim().isEmpty()) {
35+
universeDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE;
36+
} else {
37+
universeDomain = builder.universeDomain;
38+
}
39+
40+
// Ensure source credential's universe domain matches.
41+
try {
42+
if (!universeDomain.equals(sourceCredential.getUniverseDomain())) {
43+
throw new IllegalArgumentException(
44+
"The client side access boundary credential's universe domain must be the same as the source "
45+
+ "credential.");
46+
}
47+
} catch (IOException e) {
48+
// Throwing an IOException would be a breaking change, so wrap it here.
49+
throw new IllegalStateException(
50+
"Error occurred when attempting to retrieve source credential universe domain.", e);
51+
}
52+
String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.{universe_domain}/v1/token";
53+
this.tokenExchangeEndpoint =
54+
TOKEN_EXCHANGE_URL_FORMAT.replace("{universe_domain}", universeDomain);
55+
}
56+
57+
public void fetchCredentials() throws IOException {
58+
try {
59+
this.sourceCredential.refreshIfExpired();
60+
} catch (IOException e) {
61+
throw new IOException("Unable to refresh the provided source credential.", e);
62+
}
63+
64+
AccessToken sourceAccessToken = sourceCredential.getAccessToken();
65+
if (sourceAccessToken == null || sourceAccessToken.getTokenValue() == null) {
66+
throw new IOException("The source credential does not have an access token.");
67+
}
68+
69+
StsTokenExchangeRequest request =
70+
StsTokenExchangeRequest.newBuilder(
71+
sourceAccessToken.getTokenValue(), OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN)
72+
.setRequestTokenType(OAuth2Utils.TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN)
73+
.build();
74+
75+
StsRequestHandler handler =
76+
StsRequestHandler.newBuilder(
77+
tokenExchangeEndpoint, request, transportFactory.create().createRequestFactory())
78+
.build();
79+
80+
StsTokenExchangeResponse response = handler.exchangeToken();
81+
this.acceessBoundarySessionKey = response.getAccessBoundarySessionKey();
82+
this.intermediaryAccessToken = response.getAccessToken();
83+
84+
// The STS endpoint will only return the expiration time for the intermediary token
85+
// if the original access token represents a service account.
86+
// The intermediary token's expiration time will always match the source credential expiration.
87+
// When no expires_in is returned, we can copy the source credential's expiration time.
88+
if (response.getAccessToken().getExpirationTime() == null) {
89+
if (sourceAccessToken.getExpirationTime() != null) {
90+
this.intermediaryAccessToken =
91+
new AccessToken(
92+
response.getAccessToken().getTokenValue(), sourceAccessToken.getExpirationTime());
93+
}
94+
}
95+
}
96+
97+
public static Builder newBuilder() {
98+
return new Builder();
99+
}
100+
101+
public static class Builder {
102+
private GoogleCredentials sourceCredential;
103+
private HttpTransportFactory transportFactory;
104+
private String universeDomain;
105+
106+
private Builder() {}
107+
108+
/**
109+
* Sets the required source credential used to acquire the intermediary credential.
110+
*
111+
* @param sourceCredential the {@code GoogleCredentials} to set
112+
* @return this {@code Builder} object
113+
*/
114+
public Builder setSourceCredential(GoogleCredentials sourceCredential) {
115+
this.sourceCredential = sourceCredential;
116+
return this;
117+
}
118+
119+
/**
120+
* Sets the HTTP transport factory.
121+
*
122+
* @param transportFactory the {@code HttpTransportFactory} to set
123+
* @return this {@code Builder} object
124+
*/
125+
@CanIgnoreReturnValue
126+
public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
127+
this.transportFactory = transportFactory;
128+
return this;
129+
}
130+
131+
/**
132+
* Sets the optional universe domain.
133+
*
134+
* @param universeDomain the universe domain to set
135+
* @return this {@code Builder} object
136+
*/
137+
@CanIgnoreReturnValue
138+
public Builder setUniverseDomain(String universeDomain) {
139+
this.universeDomain = universeDomain;
140+
return this;
141+
}
142+
143+
public ClientSideCredentialAccessBoundaryFactory build() {
144+
return new ClientSideCredentialAccessBoundaryFactory(this);
145+
}
146+
}
147+
}

oauth2_http/java/com/google/auth/oauth2/CredentialAccessBoundary.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public final class CredentialAccessBoundary {
6767
/**
6868
* Internal method that returns the JSON string representation of the credential access boundary.
6969
*/
70-
String toJson() {
70+
public String toJson() {
7171
List<GenericJson> rules = new ArrayList<>();
7272
for (AccessBoundaryRule rule : accessBoundaryRules) {
7373
GenericJson ruleJson = new GenericJson();

oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ protected static <T> T newInstance(String className) throws IOException, ClassNo
484484
}
485485
}
486486

487-
protected static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInstance) {
487+
public static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInstance) {
488488
return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance);
489489
}
490490

oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,15 @@
7070
import java.util.Set;
7171

7272
/** Internal utilities for the com.google.auth.oauth2 namespace. */
73-
class OAuth2Utils {
73+
public class OAuth2Utils {
74+
7475
static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
7576

76-
static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
77+
public static final String TOKEN_TYPE_ACCESS_TOKEN =
78+
"urn:ietf:params:oauth:token-type:access_token";
7779
static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange";
80+
public static final String TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN =
81+
"urn:ietf:params:oauth:token-type:access_boundary_intermediary_token";
7882
static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
7983

8084
// generateIdToken endpoint is to be formatted with universe domain and client email
@@ -93,7 +97,8 @@ class OAuth2Utils {
9397

9498
static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
9599

96-
static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = new DefaultHttpTransportFactory();
100+
public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY =
101+
new DefaultHttpTransportFactory();
97102

98103
static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
99104

oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
import javax.annotation.Nullable;
5252

5353
/** Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693. */
54-
final class StsRequestHandler {
54+
public final class StsRequestHandler {
5555
private static final String TOKEN_EXCHANGE_GRANT_TYPE =
5656
"urn:ietf:params:oauth:grant-type:token-exchange";
5757
private static final String PARSE_ERROR_PREFIX = "Error parsing token response.";
@@ -175,6 +175,11 @@ private StsTokenExchangeResponse buildResponse(GenericData responseData) throws
175175
String scope = OAuth2Utils.validateString(responseData, "scope", PARSE_ERROR_PREFIX);
176176
builder.setScopes(Arrays.asList(scope.trim().split("\\s+")));
177177
}
178+
if (responseData.containsKey("access_boundary_session_key")) {
179+
builder.setAccessBoundarySessionKey(
180+
OAuth2Utils.validateString(
181+
responseData, "access_boundary_session_key", PARSE_ERROR_PREFIX));
182+
}
178183
return builder.build();
179184
}
180185

oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
* Defines an OAuth 2.0 token exchange request. Based on
4242
* https://tools.ietf.org/html/rfc8693#section-2.1.
4343
*/
44-
final class StsTokenExchangeRequest {
44+
public final class StsTokenExchangeRequest {
4545
private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
4646

4747
private final String subjectToken;

oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,24 @@
4343
* Defines an OAuth 2.0 token exchange successful response. Based on
4444
* https://tools.ietf.org/html/rfc8693#section-2.2.1.
4545
*/
46-
final class StsTokenExchangeResponse {
46+
public final class StsTokenExchangeResponse {
4747
private final AccessToken accessToken;
4848
private final String issuedTokenType;
4949
private final String tokenType;
5050

5151
@Nullable private final Long expiresInSeconds;
5252
@Nullable private final String refreshToken;
5353
@Nullable private final List<String> scopes;
54+
@Nullable private final String accessBoundarySessionKey;
5455

5556
private StsTokenExchangeResponse(
5657
String accessToken,
5758
String issuedTokenType,
5859
String tokenType,
5960
@Nullable Long expiresInSeconds,
6061
@Nullable String refreshToken,
61-
@Nullable List<String> scopes) {
62+
@Nullable List<String> scopes,
63+
@Nullable String accessBoundarySessionKey) {
6264
checkNotNull(accessToken);
6365

6466
this.expiresInSeconds = expiresInSeconds;
@@ -71,6 +73,7 @@ private StsTokenExchangeResponse(
7173
this.tokenType = checkNotNull(tokenType);
7274
this.refreshToken = refreshToken;
7375
this.scopes = scopes;
76+
this.accessBoundarySessionKey = accessBoundarySessionKey;
7477
}
7578

7679
public static Builder newBuilder(String accessToken, String issuedTokenType, String tokenType) {
@@ -107,6 +110,11 @@ public List<String> getScopes() {
107110
return new ArrayList<>(scopes);
108111
}
109112

113+
@Nullable
114+
public String getAccessBoundarySessionKey() {
115+
return accessBoundarySessionKey;
116+
}
117+
110118
public static class Builder {
111119
private final String accessToken;
112120
private final String issuedTokenType;
@@ -115,6 +123,7 @@ public static class Builder {
115123
@Nullable private Long expiresInSeconds;
116124
@Nullable private String refreshToken;
117125
@Nullable private List<String> scopes;
126+
@Nullable private String accessBoundarySessionKey;
118127

119128
private Builder(String accessToken, String issuedTokenType, String tokenType) {
120129
this.accessToken = accessToken;
@@ -142,9 +151,22 @@ public StsTokenExchangeResponse.Builder setScopes(List<String> scopes) {
142151
return this;
143152
}
144153

154+
@CanIgnoreReturnValue
155+
public StsTokenExchangeResponse.Builder setAccessBoundarySessionKey(
156+
String accessBoundarySessionKey) {
157+
this.accessBoundarySessionKey = accessBoundarySessionKey;
158+
return this;
159+
}
160+
145161
public StsTokenExchangeResponse build() {
146162
return new StsTokenExchangeResponse(
147-
accessToken, issuedTokenType, tokenType, expiresInSeconds, refreshToken, scopes);
163+
accessToken,
164+
issuedTokenType,
165+
tokenType,
166+
expiresInSeconds,
167+
refreshToken,
168+
scopes,
169+
accessBoundarySessionKey);
148170
}
149171
}
150172
}

0 commit comments

Comments
 (0)