Skip to content

Commit 240c26b

Browse files
authored
feat: Expose scopes granted by user (#1107)
* 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
1 parent aeb1218 commit 240c26b

File tree

7 files changed

+327
-25
lines changed

7 files changed

+327
-25
lines changed

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

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333

3434
import com.google.common.base.MoreObjects;
3535
import java.io.Serializable;
36+
import java.util.Arrays;
3637
import java.util.Date;
38+
import java.util.List;
3739
import java.util.Objects;
3840

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

4446
private final String tokenValue;
4547
private final Long expirationTimeMillis;
48+
private final List<String> scopes;
4649

4750
/**
4851
* @param tokenValue String representation of the access token.
@@ -51,6 +54,32 @@ public class AccessToken implements Serializable {
5154
public AccessToken(String tokenValue, Date expirationTime) {
5255
this.tokenValue = tokenValue;
5356
this.expirationTimeMillis = (expirationTime == null) ? null : expirationTime.getTime();
57+
this.scopes = null;
58+
}
59+
60+
private AccessToken(Builder builder) {
61+
this.tokenValue = builder.getTokenValue();
62+
Date expirationTime = builder.getExpirationTime();
63+
this.expirationTimeMillis = (expirationTime == null) ? null : expirationTime.getTime();
64+
this.scopes = builder.getScopes();
65+
}
66+
67+
public static Builder newBuilder() {
68+
return new Builder();
69+
}
70+
71+
public Builder toBuilder() {
72+
return new Builder(this);
73+
}
74+
75+
/**
76+
* Scopes from the access token response. Not all credentials provide scopes in response and as
77+
* per https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 it is optional in the response.
78+
*
79+
* @return List of scopes
80+
*/
81+
public List<String> getScopes() {
82+
return scopes;
5483
}
5584

5685
/**
@@ -80,14 +109,15 @@ Long getExpirationTimeMillis() {
80109

81110
@Override
82111
public int hashCode() {
83-
return Objects.hash(tokenValue, expirationTimeMillis);
112+
return Objects.hash(tokenValue, expirationTimeMillis, scopes);
84113
}
85114

86115
@Override
87116
public String toString() {
88117
return MoreObjects.toStringHelper(this)
89118
.add("tokenValue", tokenValue)
90119
.add("expirationTimeMillis", expirationTimeMillis)
120+
.add("scopes", scopes)
91121
.toString();
92122
}
93123

@@ -98,6 +128,55 @@ public boolean equals(Object obj) {
98128
}
99129
AccessToken other = (AccessToken) obj;
100130
return Objects.equals(this.tokenValue, other.tokenValue)
101-
&& Objects.equals(this.expirationTimeMillis, other.expirationTimeMillis);
131+
&& Objects.equals(this.expirationTimeMillis, other.expirationTimeMillis)
132+
&& Objects.equals(this.scopes, other.scopes);
133+
}
134+
135+
public static class Builder {
136+
private String tokenValue;
137+
private Date expirationTime;
138+
private List<String> scopes;
139+
140+
protected Builder() {}
141+
142+
protected Builder(AccessToken accessToken) {
143+
this.tokenValue = accessToken.getTokenValue();
144+
this.expirationTime = accessToken.getExpirationTime();
145+
this.scopes = accessToken.getScopes();
146+
}
147+
148+
public String getTokenValue() {
149+
return this.tokenValue;
150+
}
151+
152+
public List<String> getScopes() {
153+
return this.scopes;
154+
}
155+
156+
public Date getExpirationTime() {
157+
return this.expirationTime;
158+
}
159+
160+
public Builder setTokenValue(String tokenValue) {
161+
this.tokenValue = tokenValue;
162+
return this;
163+
}
164+
165+
public Builder setScopes(String scopes) {
166+
if (scopes != null) {
167+
this.scopes = Arrays.asList(scopes.split(" "));
168+
}
169+
170+
return this;
171+
}
172+
173+
public Builder setExpirationTime(Date expirationTime) {
174+
this.expirationTime = expirationTime;
175+
return this;
176+
}
177+
178+
public AccessToken build() {
179+
return new AccessToken(this);
180+
}
102181
}
103182
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ class OAuth2Utils {
8989

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

92+
static final String TOKEN_RESPONSE_SCOPE = "scope";
93+
9294
// Includes expected server errors from Google token endpoint
9395
// Other 5xx codes are either not used or retries are unlikely to succeed
9496
public static final Set<Integer> TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES =

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.net.URL;
4949
import java.util.Collection;
5050
import java.util.Date;
51+
import java.util.List;
5152

5253
/** Handles an interactive 3-Legged-OAuth2 (3LO) user consent authorization. */
5354
public class UserAuthorizer {
@@ -204,7 +205,15 @@ public UserCredentials getCredentials(String userId) throws IOException {
204205
Long expirationMillis =
205206
OAuth2Utils.validateLong(tokenJson, "expiration_time_millis", TOKEN_STORE_ERROR);
206207
Date expirationTime = new Date(expirationMillis);
207-
AccessToken accessToken = new AccessToken(accessTokenValue, expirationTime);
208+
String scopes =
209+
OAuth2Utils.validateOptionalString(
210+
tokenJson, OAuth2Utils.TOKEN_RESPONSE_SCOPE, FETCH_TOKEN_ERROR);
211+
AccessToken accessToken =
212+
AccessToken.newBuilder()
213+
.setExpirationTime(expirationTime)
214+
.setTokenValue(accessTokenValue)
215+
.setScopes(scopes)
216+
.build();
208217
String refreshToken =
209218
OAuth2Utils.validateOptionalString(tokenJson, "refresh_token", TOKEN_STORE_ERROR);
210219
UserCredentials credentials =
@@ -251,7 +260,15 @@ public UserCredentials getCredentialsFromCode(String code, URI baseUri) throws I
251260
OAuth2Utils.validateString(parsedTokens, "access_token", FETCH_TOKEN_ERROR);
252261
int expiresInSecs = OAuth2Utils.validateInt32(parsedTokens, "expires_in", FETCH_TOKEN_ERROR);
253262
Date expirationTime = new Date(new Date().getTime() + expiresInSecs * 1000);
254-
AccessToken accessToken = new AccessToken(accessTokenValue, expirationTime);
263+
String scopes =
264+
OAuth2Utils.validateOptionalString(
265+
parsedTokens, OAuth2Utils.TOKEN_RESPONSE_SCOPE, FETCH_TOKEN_ERROR);
266+
AccessToken accessToken =
267+
AccessToken.newBuilder()
268+
.setExpirationTime(expirationTime)
269+
.setTokenValue(accessTokenValue)
270+
.setScopes(scopes)
271+
.build();
255272
String refreshToken =
256273
OAuth2Utils.validateOptionalString(parsedTokens, "refresh_token", FETCH_TOKEN_ERROR);
257274

@@ -343,15 +360,22 @@ public void storeCredentials(String userId, UserCredentials credentials) throws
343360
}
344361
AccessToken accessToken = credentials.getAccessToken();
345362
String acessTokenValue = null;
363+
String scopes = null;
346364
Date expiresBy = null;
347365
if (accessToken != null) {
348366
acessTokenValue = accessToken.getTokenValue();
349367
expiresBy = accessToken.getExpirationTime();
368+
List<String> grantedScopes = accessToken.getScopes();
369+
370+
if (grantedScopes != null) {
371+
scopes = String.join(" ", grantedScopes);
372+
}
350373
}
351374
String refreshToken = credentials.getRefreshToken();
352375
GenericJson tokenStateJson = new GenericJson();
353376
tokenStateJson.setFactory(OAuth2Utils.JSON_FACTORY);
354377
tokenStateJson.put("access_token", acessTokenValue);
378+
tokenStateJson.put(OAuth2Utils.TOKEN_RESPONSE_SCOPE, scopes);
355379
tokenStateJson.put("expiration_time_millis", expiresBy.getTime());
356380
if (refreshToken != null) {
357381
tokenStateJson.put("refresh_token", refreshToken);

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,14 @@ public AccessToken refreshAccessToken() throws IOException {
180180
int expiresInSeconds =
181181
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
182182
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
183-
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
183+
String scopes =
184+
OAuth2Utils.validateOptionalString(
185+
responseData, OAuth2Utils.TOKEN_RESPONSE_SCOPE, PARSE_ERROR_PREFIX);
186+
return AccessToken.newBuilder()
187+
.setExpirationTime(new Date(expiresAtMilliseconds))
188+
.setTokenValue(accessToken)
189+
.setScopes(scopes)
190+
.build();
184191
}
185192

186193
/**

oauth2_http/javatests/com/google/auth/oauth2/AccessTokenTest.java

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131

3232
package com.google.auth.oauth2;
3333

34+
import static org.junit.Assert.assertArrayEquals;
3435
import static org.junit.Assert.assertEquals;
3536
import static org.junit.Assert.assertFalse;
3637
import static org.junit.Assert.assertTrue;
3738

3839
import java.io.IOException;
40+
import java.util.Arrays;
3941
import java.util.Date;
4042
import org.junit.Test;
4143
import org.junit.runner.RunWith;
@@ -47,59 +49,154 @@ public class AccessTokenTest extends BaseSerializationTest {
4749

4850
private static final String TOKEN = "AccessToken";
4951
private static final Date EXPIRATION_DATE = new Date();
52+
private static final String SCOPES = "scope1 scope2";
5053

5154
@Test
5255
public void constructor() {
5356
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
5457
assertEquals(TOKEN, accessToken.getTokenValue());
5558
assertEquals(EXPIRATION_DATE, accessToken.getExpirationTime());
5659
assertEquals(EXPIRATION_DATE.getTime(), (long) accessToken.getExpirationTimeMillis());
60+
assertEquals(null, accessToken.getScopes());
61+
}
62+
63+
@Test
64+
public void builder() {
65+
AccessToken accessToken =
66+
AccessToken.newBuilder()
67+
.setExpirationTime(EXPIRATION_DATE)
68+
.setTokenValue(TOKEN)
69+
.setScopes(SCOPES)
70+
.build();
71+
assertEquals(TOKEN, accessToken.getTokenValue());
72+
assertEquals(EXPIRATION_DATE, accessToken.getExpirationTime());
73+
assertEquals(EXPIRATION_DATE.getTime(), (long) accessToken.getExpirationTimeMillis());
74+
assertArrayEquals(SCOPES.split(" "), accessToken.getScopes().toArray());
5775
}
5876

5977
@Test
6078
public void equals_true() throws IOException {
61-
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
62-
AccessToken otherAccessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
79+
AccessToken accessToken =
80+
AccessToken.newBuilder()
81+
.setExpirationTime(EXPIRATION_DATE)
82+
.setTokenValue(TOKEN)
83+
.setScopes(SCOPES)
84+
.build();
85+
86+
AccessToken otherAccessToken =
87+
AccessToken.newBuilder()
88+
.setExpirationTime(EXPIRATION_DATE)
89+
.setTokenValue(TOKEN)
90+
.setScopes(SCOPES)
91+
.build();
92+
6393
assertTrue(accessToken.equals(otherAccessToken));
6494
assertTrue(otherAccessToken.equals(accessToken));
6595
}
6696

97+
@Test
98+
public void equals_false_scopes() throws IOException {
99+
AccessToken accessToken =
100+
AccessToken.newBuilder()
101+
.setExpirationTime(EXPIRATION_DATE)
102+
.setTokenValue(TOKEN)
103+
.setScopes(SCOPES)
104+
.build();
105+
106+
AccessToken otherAccessToken =
107+
AccessToken.newBuilder()
108+
.setExpirationTime(EXPIRATION_DATE)
109+
.setTokenValue(TOKEN)
110+
.setScopes("scope1")
111+
.build();
112+
113+
assertFalse(accessToken.equals(otherAccessToken));
114+
assertFalse(otherAccessToken.equals(accessToken));
115+
}
116+
67117
@Test
68118
public void equals_false_token() throws IOException {
69-
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
70-
AccessToken otherAccessToken = new AccessToken("otherToken", EXPIRATION_DATE);
119+
AccessToken accessToken =
120+
AccessToken.newBuilder()
121+
.setExpirationTime(EXPIRATION_DATE)
122+
.setTokenValue(TOKEN)
123+
.setScopes(SCOPES)
124+
.build();
125+
126+
AccessToken otherAccessToken =
127+
AccessToken.newBuilder()
128+
.setExpirationTime(EXPIRATION_DATE)
129+
.setTokenValue("otherToken")
130+
.setScopes(SCOPES)
131+
.build();
132+
71133
assertFalse(accessToken.equals(otherAccessToken));
72134
assertFalse(otherAccessToken.equals(accessToken));
73135
}
74136

75137
@Test
76138
public void equals_false_expirationDate() throws IOException {
77-
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
78-
AccessToken otherAccessToken = new AccessToken(TOKEN, new Date(EXPIRATION_DATE.getTime() + 42));
139+
AccessToken accessToken =
140+
AccessToken.newBuilder()
141+
.setExpirationTime(EXPIRATION_DATE)
142+
.setTokenValue(TOKEN)
143+
.setScopes(SCOPES)
144+
.build();
145+
146+
AccessToken otherAccessToken =
147+
AccessToken.newBuilder()
148+
.setExpirationTime(new Date(EXPIRATION_DATE.getTime() + 42))
149+
.setTokenValue(TOKEN)
150+
.setScopes(SCOPES)
151+
.build();
152+
79153
assertFalse(accessToken.equals(otherAccessToken));
80154
assertFalse(otherAccessToken.equals(accessToken));
81155
}
82156

83157
@Test
84158
public void toString_containsFields() {
85-
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
159+
AccessToken accessToken =
160+
AccessToken.newBuilder()
161+
.setExpirationTime(EXPIRATION_DATE)
162+
.setTokenValue(TOKEN)
163+
.setScopes(SCOPES)
164+
.build();
86165
String expectedToString =
87166
String.format(
88-
"AccessToken{tokenValue=%s, expirationTimeMillis=%d}",
89-
TOKEN, EXPIRATION_DATE.getTime());
167+
"AccessToken{tokenValue=%s, expirationTimeMillis=%d, scopes=%s}",
168+
TOKEN, EXPIRATION_DATE.getTime(), Arrays.asList(SCOPES.split(" ")));
90169
assertEquals(expectedToString, accessToken.toString());
91170
}
92171

93172
@Test
94173
public void hashCode_equals() throws IOException {
95-
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
96-
AccessToken otherAccessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
174+
AccessToken accessToken =
175+
AccessToken.newBuilder()
176+
.setExpirationTime(EXPIRATION_DATE)
177+
.setTokenValue(TOKEN)
178+
.setScopes(SCOPES)
179+
.build();
180+
181+
AccessToken otherAccessToken =
182+
AccessToken.newBuilder()
183+
.setExpirationTime(EXPIRATION_DATE)
184+
.setTokenValue(TOKEN)
185+
.setScopes(SCOPES)
186+
.build();
187+
97188
assertEquals(accessToken.hashCode(), otherAccessToken.hashCode());
98189
}
99190

100191
@Test
101192
public void serialize() throws IOException, ClassNotFoundException {
102-
AccessToken accessToken = new AccessToken(TOKEN, EXPIRATION_DATE);
193+
AccessToken accessToken =
194+
AccessToken.newBuilder()
195+
.setExpirationTime(EXPIRATION_DATE)
196+
.setTokenValue(TOKEN)
197+
.setScopes(SCOPES)
198+
.build();
199+
103200
AccessToken deserializedAccessToken = serializeAndDeserialize(accessToken);
104201
assertEquals(accessToken, deserializedAccessToken);
105202
assertEquals(accessToken.hashCode(), deserializedAccessToken.hashCode());

0 commit comments

Comments
 (0)