Skip to content

Commit

Permalink
feat: Expose scopes granted by user (#1107)
Browse files Browse the repository at this point in the history
* feat: Expose scopes granted by user

* Fix Access Token failure

* Update more AT tests

* scopes as list

* fix compilation error

* fix tests

* Add more tests

* update test
  • Loading branch information
sai-sunder-s committed Dec 21, 2022
1 parent aeb1218 commit 240c26b
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 25 deletions.
83 changes: 81 additions & 2 deletions oauth2_http/java/com/google/auth/oauth2/AccessToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@

import com.google.common.base.MoreObjects;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;

/** Represents a temporary OAuth2 access token and its expiration information. */
Expand All @@ -43,6 +45,7 @@ public class AccessToken implements Serializable {

private final String tokenValue;
private final Long expirationTimeMillis;
private final List<String> scopes;

/**
* @param tokenValue String representation of the access token.
Expand All @@ -51,6 +54,32 @@ public class AccessToken implements Serializable {
public AccessToken(String tokenValue, Date expirationTime) {
this.tokenValue = tokenValue;
this.expirationTimeMillis = (expirationTime == null) ? null : expirationTime.getTime();
this.scopes = null;
}

private AccessToken(Builder builder) {
this.tokenValue = builder.getTokenValue();
Date expirationTime = builder.getExpirationTime();
this.expirationTimeMillis = (expirationTime == null) ? null : expirationTime.getTime();
this.scopes = builder.getScopes();
}

public static Builder newBuilder() {
return new Builder();
}

public Builder toBuilder() {
return new Builder(this);
}

/**
* Scopes from the access token response. Not all credentials provide scopes in response and as
* per https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 it is optional in the response.
*
* @return List of scopes
*/
public List<String> getScopes() {
return scopes;
}

/**
Expand Down Expand Up @@ -80,14 +109,15 @@ Long getExpirationTimeMillis() {

@Override
public int hashCode() {
return Objects.hash(tokenValue, expirationTimeMillis);
return Objects.hash(tokenValue, expirationTimeMillis, scopes);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("tokenValue", tokenValue)
.add("expirationTimeMillis", expirationTimeMillis)
.add("scopes", scopes)
.toString();
}

Expand All @@ -98,6 +128,55 @@ public boolean equals(Object obj) {
}
AccessToken other = (AccessToken) obj;
return Objects.equals(this.tokenValue, other.tokenValue)
&& Objects.equals(this.expirationTimeMillis, other.expirationTimeMillis);
&& Objects.equals(this.expirationTimeMillis, other.expirationTimeMillis)
&& Objects.equals(this.scopes, other.scopes);
}

public static class Builder {
private String tokenValue;
private Date expirationTime;
private List<String> scopes;

protected Builder() {}

protected Builder(AccessToken accessToken) {
this.tokenValue = accessToken.getTokenValue();
this.expirationTime = accessToken.getExpirationTime();
this.scopes = accessToken.getScopes();
}

public String getTokenValue() {
return this.tokenValue;
}

public List<String> getScopes() {
return this.scopes;
}

public Date getExpirationTime() {
return this.expirationTime;
}

public Builder setTokenValue(String tokenValue) {
this.tokenValue = tokenValue;
return this;
}

public Builder setScopes(String scopes) {
if (scopes != null) {
this.scopes = Arrays.asList(scopes.split(" "));
}

return this;
}

public Builder setExpirationTime(Date expirationTime) {
this.expirationTime = expirationTime;
return this;
}

public AccessToken build() {
return new AccessToken(this);
}
}
}
2 changes: 2 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class OAuth2Utils {

static final String BEARER_PREFIX = AuthHttpConstants.BEARER + " ";

static final String TOKEN_RESPONSE_SCOPE = "scope";

// Includes expected server errors from Google token endpoint
// Other 5xx codes are either not used or retries are unlikely to succeed
public static final Set<Integer> TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES =
Expand Down
28 changes: 26 additions & 2 deletions oauth2_http/java/com/google/auth/oauth2/UserAuthorizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import java.net.URL;
import java.util.Collection;
import java.util.Date;
import java.util.List;

/** Handles an interactive 3-Legged-OAuth2 (3LO) user consent authorization. */
public class UserAuthorizer {
Expand Down Expand Up @@ -204,7 +205,15 @@ public UserCredentials getCredentials(String userId) throws IOException {
Long expirationMillis =
OAuth2Utils.validateLong(tokenJson, "expiration_time_millis", TOKEN_STORE_ERROR);
Date expirationTime = new Date(expirationMillis);
AccessToken accessToken = new AccessToken(accessTokenValue, expirationTime);
String scopes =
OAuth2Utils.validateOptionalString(
tokenJson, OAuth2Utils.TOKEN_RESPONSE_SCOPE, FETCH_TOKEN_ERROR);
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(expirationTime)
.setTokenValue(accessTokenValue)
.setScopes(scopes)
.build();
String refreshToken =
OAuth2Utils.validateOptionalString(tokenJson, "refresh_token", TOKEN_STORE_ERROR);
UserCredentials credentials =
Expand Down Expand Up @@ -251,7 +260,15 @@ public UserCredentials getCredentialsFromCode(String code, URI baseUri) throws I
OAuth2Utils.validateString(parsedTokens, "access_token", FETCH_TOKEN_ERROR);
int expiresInSecs = OAuth2Utils.validateInt32(parsedTokens, "expires_in", FETCH_TOKEN_ERROR);
Date expirationTime = new Date(new Date().getTime() + expiresInSecs * 1000);
AccessToken accessToken = new AccessToken(accessTokenValue, expirationTime);
String scopes =
OAuth2Utils.validateOptionalString(
parsedTokens, OAuth2Utils.TOKEN_RESPONSE_SCOPE, FETCH_TOKEN_ERROR);
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(expirationTime)
.setTokenValue(accessTokenValue)
.setScopes(scopes)
.build();
String refreshToken =
OAuth2Utils.validateOptionalString(parsedTokens, "refresh_token", FETCH_TOKEN_ERROR);

Expand Down Expand Up @@ -343,15 +360,22 @@ public void storeCredentials(String userId, UserCredentials credentials) throws
}
AccessToken accessToken = credentials.getAccessToken();
String acessTokenValue = null;
String scopes = null;
Date expiresBy = null;
if (accessToken != null) {
acessTokenValue = accessToken.getTokenValue();
expiresBy = accessToken.getExpirationTime();
List<String> grantedScopes = accessToken.getScopes();

if (grantedScopes != null) {
scopes = String.join(" ", grantedScopes);
}
}
String refreshToken = credentials.getRefreshToken();
GenericJson tokenStateJson = new GenericJson();
tokenStateJson.setFactory(OAuth2Utils.JSON_FACTORY);
tokenStateJson.put("access_token", acessTokenValue);
tokenStateJson.put(OAuth2Utils.TOKEN_RESPONSE_SCOPE, scopes);
tokenStateJson.put("expiration_time_millis", expiresBy.getTime());
if (refreshToken != null) {
tokenStateJson.put("refresh_token", refreshToken);
Expand Down
9 changes: 8 additions & 1 deletion oauth2_http/java/com/google/auth/oauth2/UserCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,14 @@ public AccessToken refreshAccessToken() throws IOException {
int expiresInSeconds =
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
String scopes =
OAuth2Utils.validateOptionalString(
responseData, OAuth2Utils.TOKEN_RESPONSE_SCOPE, PARSE_ERROR_PREFIX);
return AccessToken.newBuilder()
.setExpirationTime(new Date(expiresAtMilliseconds))
.setTokenValue(accessToken)
.setScopes(scopes)
.build();
}

/**
Expand Down
121 changes: 109 additions & 12 deletions oauth2_http/javatests/com/google/auth/oauth2/AccessTokenTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@

package com.google.auth.oauth2;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -47,59 +49,154 @@ public class AccessTokenTest extends BaseSerializationTest {

private static final String TOKEN = "AccessToken";
private static final Date EXPIRATION_DATE = new Date();
private static final String SCOPES = "scope1 scope2";

@Test
public void constructor() {
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
assertEquals(TOKEN, accessToken.getTokenValue());
assertEquals(EXPIRATION_DATE, accessToken.getExpirationTime());
assertEquals(EXPIRATION_DATE.getTime(), (long) accessToken.getExpirationTimeMillis());
assertEquals(null, accessToken.getScopes());
}

@Test
public void builder() {
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();
assertEquals(TOKEN, accessToken.getTokenValue());
assertEquals(EXPIRATION_DATE, accessToken.getExpirationTime());
assertEquals(EXPIRATION_DATE.getTime(), (long) accessToken.getExpirationTimeMillis());
assertArrayEquals(SCOPES.split(" "), accessToken.getScopes().toArray());
}

@Test
public void equals_true() throws IOException {
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken otherAccessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

AccessToken otherAccessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

assertTrue(accessToken.equals(otherAccessToken));
assertTrue(otherAccessToken.equals(accessToken));
}

@Test
public void equals_false_scopes() throws IOException {
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

AccessToken otherAccessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes("scope1")
.build();

assertFalse(accessToken.equals(otherAccessToken));
assertFalse(otherAccessToken.equals(accessToken));
}

@Test
public void equals_false_token() throws IOException {
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken otherAccessToken = new AccessToken("otherToken", EXPIRATION_DATE);
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

AccessToken otherAccessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue("otherToken")
.setScopes(SCOPES)
.build();

assertFalse(accessToken.equals(otherAccessToken));
assertFalse(otherAccessToken.equals(accessToken));
}

@Test
public void equals_false_expirationDate() throws IOException {
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken otherAccessToken = new AccessToken(TOKEN, new Date(EXPIRATION_DATE.getTime() + 42));
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

AccessToken otherAccessToken =
AccessToken.newBuilder()
.setExpirationTime(new Date(EXPIRATION_DATE.getTime() + 42))
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

assertFalse(accessToken.equals(otherAccessToken));
assertFalse(otherAccessToken.equals(accessToken));
}

@Test
public void toString_containsFields() {
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();
String expectedToString =
String.format(
"AccessToken{tokenValue=%s, expirationTimeMillis=%d}",
TOKEN, EXPIRATION_DATE.getTime());
"AccessToken{tokenValue=%s, expirationTimeMillis=%d, scopes=%s}",
TOKEN, EXPIRATION_DATE.getTime(), Arrays.asList(SCOPES.split(" ")));
assertEquals(expectedToString, accessToken.toString());
}

@Test
public void hashCode_equals() throws IOException {
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken otherAccessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

AccessToken otherAccessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

assertEquals(accessToken.hashCode(), otherAccessToken.hashCode());
}

@Test
public void serialize() throws IOException, ClassNotFoundException {
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
AccessToken accessToken =
AccessToken.newBuilder()
.setExpirationTime(EXPIRATION_DATE)
.setTokenValue(TOKEN)
.setScopes(SCOPES)
.build();

AccessToken deserializedAccessToken = serializeAndDeserialize(accessToken);
assertEquals(accessToken, deserializedAccessToken);
assertEquals(accessToken.hashCode(), deserializedAccessToken.hashCode());
Expand Down
Loading

0 comments on commit 240c26b

Please sign in to comment.