Skip to content

Commit

Permalink
Merge branch 'main' into better-assertion-msg
Browse files Browse the repository at this point in the history
  • Loading branch information
lqiu96 authored Aug 29, 2024
2 parents 98eac04 + e1b1a33 commit 6c8ab88
Show file tree
Hide file tree
Showing 9 changed files with 456 additions and 239 deletions.
4 changes: 4 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ class OAuth2Utils {
static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange";
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
static final String IAM_ID_TOKEN_ENDPOINT_FORMAT =
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateIdToken";

static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token");
static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke");
static final URI USER_AUTH_URI = URI.create("https://accounts.google.com/o/oauth2/auth");
Expand Down
116 changes: 93 additions & 23 deletions oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpBackOffIOExceptionHandler;
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
import com.google.api.client.http.HttpContent;
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;
Expand All @@ -52,9 +54,12 @@
import com.google.auth.Credentials;
import com.google.auth.RequestMetadataCallback;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
Expand Down Expand Up @@ -547,7 +552,9 @@ public AccessToken refreshAccessToken() throws IOException {
}

/**
* Returns a Google ID Token from the metadata server on ComputeEngine.
* Returns a Google ID Token from either the Oauth or IAM Endpoint. For Credentials that are in
* the Google Default Universe (googleapis.com), the ID Token will be retrieved from the Oauth
* Endpoint. Otherwise, it will be retrieved from the IAM Endpoint.
*
* @param targetAudience the aud: field the IdToken should include.
* @param options list of Credential specific options for the token. Currently, unused for
Expand All @@ -558,21 +565,90 @@ public AccessToken refreshAccessToken() throws IOException {
@Override
public IdToken idTokenWithAudience(String targetAudience, List<Option> options)
throws IOException {
return isDefaultUniverseDomain()
? getIdTokenOauthEndpoint(targetAudience)
: getIdTokenIamEndpoint(targetAudience);
}

JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
/**
* Uses the Oauth Endpoint to generate an ID token. Assertions and grant_type are sent in the
* request body.
*/
private IdToken getIdTokenOauthEndpoint(String targetAudience) throws IOException {
long currentTime = clock.currentTimeMillis();
String assertion =
createAssertionForIdToken(
jsonFactory, currentTime, tokenServerUri.toString(), targetAudience);
createAssertionForIdToken(currentTime, tokenServerUri.toString(), targetAudience);

Map<String, Object> requestParams =
ImmutableMap.of("grant_type", GRANT_TYPE, "assertion", assertion);
GenericData tokenRequest = new GenericData();
tokenRequest.set("grant_type", GRANT_TYPE);
tokenRequest.set("assertion", assertion);
requestParams.forEach(tokenRequest::set);
UrlEncodedContent content = new UrlEncodedContent(tokenRequest);

HttpRequest request = buildIdTokenRequest(tokenServerUri, transportFactory, content);
HttpResponse httpResponse = executeRequest(request);

GenericData responseData = httpResponse.parseAs(GenericData.class);
String rawToken = OAuth2Utils.validateString(responseData, "id_token", PARSE_ERROR_PREFIX);
return IdToken.create(rawToken);
}

/**
* Use IAM generateIdToken endpoint to obtain an ID token.
*
* <p>This flow works as follows:
*
* <ol>
* <li>Create a self-signed jwt with `https://www.googleapis.com/auth/iam` as the scope.
* <li>Use the self-signed jwt as the access token, and make a POST request to IAM
* generateIdToken endpoint.
* <li>If the request is successfully, it will return {"token":"the ID token"}. Extract the ID
* token.
* </ol>
*/
private IdToken getIdTokenIamEndpoint(String targetAudience) throws IOException {
JwtCredentials selfSignedJwtCredentials =
createSelfSignedJwtCredentials(
null, ImmutableList.of("https://www.googleapis.com/auth/iam"));
Map<String, List<String>> responseMetadata = selfSignedJwtCredentials.getRequestMetadata(null);
// JwtCredentials will return a map with one entry ("Authorization" -> List with size 1)
String accessToken = responseMetadata.get(AuthHttpConstants.AUTHORIZATION).get(0);

// Do not check user options. These params are always set regardless of options configured
Map<String, Object> requestParams =
ImmutableMap.of("audience", targetAudience, "includeEmail", "true", "useEmailAzp", "true");
GenericData tokenRequest = new GenericData();
requestParams.forEach(tokenRequest::set);
UrlEncodedContent content = new UrlEncodedContent(tokenRequest);

// Create IAM Token URI in this method instead of in the constructor because
// `getUniverseDomain()` throws an IOException that would need to be caught
URI iamIdTokenUri =
URI.create(
String.format(
OAuth2Utils.IAM_ID_TOKEN_ENDPOINT_FORMAT, getUniverseDomain(), clientEmail));
HttpRequest request = buildIdTokenRequest(iamIdTokenUri, transportFactory, content);
// Use the Access Token from the SSJWT to request the ID Token from IAM Endpoint
request.setHeaders(new HttpHeaders().set(AuthHttpConstants.AUTHORIZATION, accessToken));
HttpResponse httpResponse = executeRequest(request);

GenericData responseData = httpResponse.parseAs(GenericData.class);
// IAM Endpoint returns `token` instead of `id_token`
String rawToken = OAuth2Utils.validateString(responseData, "token", PARSE_ERROR_PREFIX);
return IdToken.create(rawToken);
}

// Build a default POST HttpRequest to be used for both Oauth and IAM endpoints
private HttpRequest buildIdTokenRequest(
URI uri, HttpTransportFactory transportFactory, HttpContent content) throws IOException {
JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), content);
HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(uri), content);
request.setParser(new JsonObjectParser(jsonFactory));
return request;
}

private HttpResponse executeRequest(HttpRequest request) throws IOException {
HttpResponse response;
try {
response = request.execute();
Expand All @@ -583,11 +659,7 @@ public IdToken idTokenWithAudience(String targetAudience, List<Option> options)
e.getMessage(), getIssuer()),
e);
}

GenericData responseData = response.parseAs(GenericData.class);
String rawToken = OAuth2Utils.validateString(responseData, "id_token", PARSE_ERROR_PREFIX);

return IdToken.create(rawToken);
return response;
}

/**
Expand Down Expand Up @@ -826,9 +898,9 @@ String createAssertion(JsonFactory jsonFactory, long currentTime) throws IOExcep
}

@VisibleForTesting
String createAssertionForIdToken(
JsonFactory jsonFactory, long currentTime, String audience, String targetAudience)
String createAssertionForIdToken(long currentTime, String audience, String targetAudience)
throws IOException {
JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
JsonWebSignature.Header header = new JsonWebSignature.Header();
header.setAlgorithm("RS256");
header.setType("JWT");
Expand All @@ -849,9 +921,7 @@ String createAssertionForIdToken(
try {
payload.set("target_audience", targetAudience);

String assertion =
JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
return assertion;
return JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
} catch (GeneralSecurityException e) {
throw new IOException(
"Error signing service account access token request with private key.", e);
Expand All @@ -877,18 +947,18 @@ static URI getUriForSelfSignedJWT(URI uri) {

@VisibleForTesting
JwtCredentials createSelfSignedJwtCredentials(final URI uri) {
return createSelfSignedJwtCredentials(uri, scopes.isEmpty() ? defaultScopes : scopes);
}

@VisibleForTesting
JwtCredentials createSelfSignedJwtCredentials(final URI uri, Collection<String> scopes) {
// Create a JwtCredentials for self-signed JWT. See https://google.aip.dev/auth/4111.
JwtClaims.Builder claimsBuilder =
JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail);

if (uri == null) {
// If uri is null, use scopes.
String scopeClaim = "";
if (!scopes.isEmpty()) {
scopeClaim = Joiner.on(' ').join(scopes);
} else {
scopeClaim = Joiner.on(' ').join(defaultScopes);
}
String scopeClaim = Joiner.on(' ').join(scopes);
claimsBuilder.setAdditionalClaims(Collections.singletonMap("scope", scopeClaim));
} else {
// otherwise, use audience with the uri.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.ExternalAccountAuthorizedUserCredentialsTest.MockExternalAccountAuthorizedUserCredentialsTransportFactory;
import com.google.auth.oauth2.IdentityPoolCredentialsTest.MockExternalAccountCredentialsTransportFactory;
import com.google.auth.oauth2.ImpersonatedCredentialsTest.MockIAMCredentialsServiceTransportFactory;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -604,13 +603,17 @@ public void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOE

MockIAMCredentialsServiceTransportFactory transportFactory =
new MockIAMCredentialsServiceTransportFactory();
transportFactory.transport.setTargetPrincipal(
ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
transportFactory.transport.setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
transportFactory.transport.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
transportFactory.transport.setAccessTokenEndpoint(
ImpersonatedCredentialsTest.IMPERSONATION_URL);
transportFactory.transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
transportFactory
.getTransport()
.setTargetPrincipal(ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
transportFactory.getTransport().setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
transportFactory
.getTransport()
.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
transportFactory
.getTransport()
.setAccessTokenEndpoint(ImpersonatedCredentialsTest.IMPERSONATION_URL);
transportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");

InputStream impersonationCredentialsStream =
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
Expand Down Expand Up @@ -665,13 +668,17 @@ public void fromStream_Impersonation_providesToken_WithoutQuotaProject() throws

MockIAMCredentialsServiceTransportFactory transportFactory =
new MockIAMCredentialsServiceTransportFactory();
transportFactory.transport.setTargetPrincipal(
ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
transportFactory.transport.setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
transportFactory.transport.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
transportFactory.transport.setAccessTokenEndpoint(
ImpersonatedCredentialsTest.IMPERSONATION_URL);
transportFactory.transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
transportFactory
.getTransport()
.setTargetPrincipal(ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
transportFactory.getTransport().setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
transportFactory
.getTransport()
.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
transportFactory
.getTransport()
.setAccessTokenEndpoint(ImpersonatedCredentialsTest.IMPERSONATION_URL);
transportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");

InputStream impersonationCredentialsStream =
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
Expand Down
Loading

0 comments on commit 6c8ab88

Please sign in to comment.