Skip to content

Commit

Permalink
Feat: add signed id token capability on UserCredentials class.
Browse files Browse the repository at this point in the history
idTokenWithAudience method not tested (I don't know how to do this to be useful)

Depend on this update googleapis/google-http-java-client#1100
  • Loading branch information
guillaumeblaquiere committed Aug 28, 2020
1 parent 6ed60bf commit 397694b
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 13 deletions.
112 changes: 102 additions & 10 deletions oauth2_http/java/com/google/auth/oauth2/UserCredentials.java
Expand Up @@ -35,41 +35,44 @@
import static com.google.auth.oauth2.OAuth2Utils.UTF_8;
import static com.google.common.base.MoreObjects.firstNonNull;

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.http.UrlEncodedContent;
import com.google.api.client.http.*;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Preconditions;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.URI;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;

/** OAuth2 Credentials representing a user's identity and consent. */
public class UserCredentials extends GoogleCredentials implements QuotaProjectIdProvider {
public class UserCredentials extends GoogleCredentials
implements QuotaProjectIdProvider, IdTokenProvider {

private static final String GRANT_TYPE = "refresh_token";
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
private static final long serialVersionUID = -4800758775038679176L;
public static final String GOOGLE_CLIENT_ID = "32555940559.apps.googleusercontent.com";
public static final String GOOGLE_CLIENT_SECRET = "ZmssLNjJy2998hD4CTg2ejr2";
public static final Collection<String> GOOGLE_DEFAULT_SCOPES =
ImmutableSet.<String>of(
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/accounts.reauth");

private final String clientId;
private final String clientSecret;
private final String refreshToken;
private final URI tokenServerUri;
private final String transportFactoryClassName;
private final String quotaProjectId;
private final Collection<String> scopes;

private transient HttpTransportFactory transportFactory;

Expand All @@ -89,13 +92,21 @@ private UserCredentials(
String clientSecret,
String refreshToken,
AccessToken accessToken,
Collection<String> scopes,
HttpTransportFactory transportFactory,
URI tokenServerUri,
String quotaProjectId) {
super(accessToken);
this.clientId = Preconditions.checkNotNull(clientId);
this.clientSecret = Preconditions.checkNotNull(clientSecret);
this.refreshToken = refreshToken;
// Merge the scope with the default and mandatory ones.
Collection<String> mergedScopes = new ArrayList<>();
mergedScopes.addAll(GOOGLE_DEFAULT_SCOPES);
if (scopes != null) {
mergedScopes.addAll(scopes);
}
this.scopes = ImmutableSet.copyOf(mergedScopes);
this.transportFactory =
firstNonNull(
transportFactory,
Expand Down Expand Up @@ -261,6 +272,7 @@ private InputStream getUserCredentialsStream() throws IOException {
if (quotaProjectId != null) {
json.put("quota_project", clientSecret);
}
json.put("scopes", scopes);
json.setFactory(JSON_FACTORY);
String text = json.toPrettyString();
return new ByteArrayInputStream(text.getBytes(UTF_8));
Expand Down Expand Up @@ -290,6 +302,7 @@ public int hashCode() {
clientSecret,
refreshToken,
tokenServerUri,
scopes,
transportFactoryClassName,
quotaProjectId);
}
Expand All @@ -304,6 +317,7 @@ public String toString() {
.add("tokenServerUri", tokenServerUri)
.add("transportFactoryClassName", transportFactoryClassName)
.add("quotaProjectId", quotaProjectId)
.add("scopes", scopes)
.toString();
}

Expand All @@ -319,6 +333,7 @@ public boolean equals(Object obj) {
&& Objects.equals(this.refreshToken, other.refreshToken)
&& Objects.equals(this.tokenServerUri, other.tokenServerUri)
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
&& Objects.equals(this.scopes, other.scopes)
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
}

Expand All @@ -340,6 +355,71 @@ public String getQuotaProjectId() {
return quotaProjectId;
}

/**
* Clone the UserCredential with the specified scopes.
*
* <p>Should be called before use for instances with empty scopes.
*/
@Override
/* public GoogleCredentials createScoped(Collection<String> newScopes) {
this.scopes = scopes;
return this;
}*/

public GoogleCredentials createScoped(Collection<String> newScopes) {
return new UserCredentials(
clientId,
clientSecret,
refreshToken,
getAccessToken(),
newScopes,
transportFactory,
tokenServerUri,
quotaProjectId);
}

/**
* Returns a Google ID Token from the user credential
*
* @param targetAudience currently unused for UserCredential.
* @param options list of Credential specific options for for the token. Currently unused for
* UserCredentials.
* @throws IOException if the attempt to get an IdToken failed
* @return IdToken object which includes the raw id_token and expiration
*/
@Beta
@Override
public IdToken idTokenWithAudience(String targetAudience, List<Option> options)
throws IOException {
JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;

GenericData tokenRequest = new GenericData();
tokenRequest.set("grant_type", GRANT_TYPE);
tokenRequest.set("client_id", GOOGLE_CLIENT_ID);
tokenRequest.set("client_secret", GOOGLE_CLIENT_SECRET);
tokenRequest.set("refresh_token", this.refreshToken);
// build scope value
Iterator<String> it = this.scopes.iterator();
String customScopes = it.next();
while (it.hasNext()) {
customScopes += "+" + it.next();
}
tokenRequest.set("scope", customScopes);

UrlEncodedContent content = new UrlEncodedContent(tokenRequest, true);

HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), content);
request.setParser(new JsonObjectParser(jsonFactory));
request.setCurlLoggingEnabled(true);
HttpResponse response = request.execute();

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

public static class Builder extends GoogleCredentials.Builder {

private String clientId;
Expand All @@ -348,6 +428,7 @@ public static class Builder extends GoogleCredentials.Builder {
private URI tokenServerUri;
private HttpTransportFactory transportFactory;
private String quotaProjectId;
private Collection<String> scopes;

protected Builder() {}

Expand All @@ -358,6 +439,7 @@ protected Builder(UserCredentials credentials) {
this.transportFactory = credentials.transportFactory;
this.tokenServerUri = credentials.tokenServerUri;
this.quotaProjectId = credentials.quotaProjectId;
this.scopes = credentials.scopes;
}

public Builder setClientId(String clientId) {
Expand Down Expand Up @@ -395,6 +477,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
return this;
}

public Builder setScopes(Collection<String> scopes) {
this.scopes = scopes;
return this;
}

public String getClientId() {
return clientId;
}
Expand All @@ -419,12 +506,17 @@ public String getQuotaProjectId() {
return quotaProjectId;
}

public Collection<String> getScopes() {
return scopes;
}

public UserCredentials build() {
return new UserCredentials(
clientId,
clientSecret,
refreshToken,
getAccessToken(),
scopes,
transportFactory,
tokenServerUri,
quotaProjectId);
Expand Down
Expand Up @@ -46,6 +46,7 @@
import com.google.auth.oauth2.GoogleCredentialsTest.MockTokenServerTransportFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
Expand All @@ -71,6 +72,10 @@ public class UserCredentialsTest extends BaseSerializationTest {
private static final String QUOTA_PROJECT = "sample-quota-project-id";
private static final Collection<String> SCOPES = Collections.singletonList("dummy.scope");
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
public static final Collection<String> DEFAULT_SCOPES =
ImmutableSet.<String>of(
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/accounts.reauth");

@Test(expected = IllegalStateException.class)
public void constructor_accessAndRefreshTokenNull_throws() {
Expand Down Expand Up @@ -99,8 +104,9 @@ public void createScoped_same() {
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setScopes(SCOPES)
.build();
assertSame(userCredentials, userCredentials.createScoped(SCOPES));
assertEquals(userCredentials, userCredentials.createScoped(SCOPES));
}

@Test
Expand Down Expand Up @@ -232,6 +238,7 @@ public void equals_true() throws IOException {
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setAccessToken(accessToken)
.setScopes(SCOPES)
.setHttpTransportFactory(transportFactory)
.setTokenServerUri(tokenServer)
.setQuotaProjectId(QUOTA_PROJECT)
Expand All @@ -242,6 +249,7 @@ public void equals_true() throws IOException {
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setAccessToken(accessToken)
.setScopes(SCOPES)
.setHttpTransportFactory(transportFactory)
.setTokenServerUri(tokenServer)
.setQuotaProjectId(QUOTA_PROJECT)
Expand Down Expand Up @@ -331,6 +339,34 @@ public void equals_false_refreshToken() throws IOException {
assertFalse(otherCredentials.equals(credentials));
}

@Test
public void equals_false_scopes() throws IOException {
final URI tokenServer1 = URI.create("https://foo1.com/bar");
AccessToken accessToken = new AccessToken(ACCESS_TOKEN, null);
MockHttpTransportFactory httpTransportFactory = new MockHttpTransportFactory();
OAuth2Credentials credentials =
UserCredentials.newBuilder()
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setAccessToken(accessToken)
.setScopes(SCOPES)
.setHttpTransportFactory(httpTransportFactory)
.setTokenServerUri(tokenServer1)
.build();
OAuth2Credentials otherCredentials =
UserCredentials.newBuilder()
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setRefreshToken("otherRefreshToken")
.setAccessToken(accessToken)
.setHttpTransportFactory(httpTransportFactory)
.setTokenServerUri(tokenServer1)
.build();
assertFalse(credentials.equals(otherCredentials));
assertFalse(otherCredentials.equals(credentials));
}

@Test
public void equals_false_accessToken() throws IOException {
final URI tokenServer1 = URI.create("https://foo1.com/bar");
Expand Down Expand Up @@ -462,7 +498,7 @@ public void toString_containsFields() throws IOException {
String expectedToString =
String.format(
"UserCredentials{requestMetadata=%s, temporaryAccess=%s, clientId=%s, refreshToken=%s, "
+ "tokenServerUri=%s, transportFactoryClassName=%s, quotaProjectId=%s}",
+ "tokenServerUri=%s, transportFactoryClassName=%s, quotaProjectId=%s, scopes=%s}",
ImmutableMap.of(
AuthHttpConstants.AUTHORIZATION,
ImmutableList.of(OAuth2Utils.BEARER_PREFIX + accessToken.getTokenValue())),
Expand All @@ -471,7 +507,8 @@ public void toString_containsFields() throws IOException {
REFRESH_TOKEN,
tokenServer,
MockTokenServerTransportFactory.class.getName(),
QUOTA_PROJECT);
QUOTA_PROJECT,
DEFAULT_SCOPES);
assertEquals(expectedToString, credentials.toString());
}

Expand All @@ -486,6 +523,7 @@ public void hashCode_equals() throws IOException {
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setAccessToken(accessToken)
.setScopes(SCOPES)
.setHttpTransportFactory(transportFactory)
.setTokenServerUri(tokenServer)
.setQuotaProjectId(QUOTA_PROJECT)
Expand All @@ -496,6 +534,7 @@ public void hashCode_equals() throws IOException {
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setAccessToken(accessToken)
.setScopes(SCOPES)
.setHttpTransportFactory(transportFactory)
.setTokenServerUri(tokenServer)
.setQuotaProjectId(QUOTA_PROJECT)
Expand All @@ -514,6 +553,7 @@ public void serialize() throws IOException, ClassNotFoundException {
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setAccessToken(accessToken)
.setScopes(SCOPES)
.setHttpTransportFactory(transportFactory)
.setTokenServerUri(tokenServer)
.build();
Expand Down

0 comments on commit 397694b

Please sign in to comment.