Skip to content

Commit 3a2c5d3

Browse files
authored
feat: Introduce a way to pass additional parameters to auhtorization url (#1134)
* feat: Introduce a way to pass additional parameters to auhtorization url * casing * Add custom params to token endpoint * minor updates * modify test to check for persistence of additional params
1 parent 5fa7039 commit 3a2c5d3

File tree

3 files changed

+171
-3
lines changed

3 files changed

+171
-3
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.util.Collection;
5151
import java.util.Date;
5252
import java.util.List;
53+
import java.util.Map;
5354

5455
/** Handles an interactive 3-Legged-OAuth2 (3LO) user consent authorization. */
5556
public class UserAuthorizer {
@@ -168,6 +169,20 @@ public TokenStore getTokenStore() {
168169
* @return The URL that can be navigated or redirected to.
169170
*/
170171
public URL getAuthorizationUrl(String userId, String state, URI baseUri) {
172+
return this.getAuthorizationUrl(userId, state, baseUri, null);
173+
}
174+
175+
/**
176+
* Return an URL that performs the authorization consent prompt web UI.
177+
*
178+
* @param userId Application's identifier for the end user.
179+
* @param state State that is passed on to the OAuth2 callback URI after the consent.
180+
* @param baseUri The URI to resolve the OAuth2 callback URI relative to.
181+
* @param additionalParameters Additional query parameters to be added to the authorization URL.
182+
* @return The URL that can be navigated or redirected to.
183+
*/
184+
public URL getAuthorizationUrl(
185+
String userId, String state, URI baseUri, Map<String, String> additionalParameters) {
171186
URI resolvedCallbackUri = getCallbackUri(baseUri);
172187
String scopesString = Joiner.on(' ').join(scopes);
173188

@@ -185,6 +200,13 @@ public URL getAuthorizationUrl(String userId, String state, URI baseUri) {
185200
url.put("login_hint", userId);
186201
}
187202
url.put("include_granted_scopes", true);
203+
204+
if (additionalParameters != null) {
205+
for (Map.Entry<String, String> entry : additionalParameters.entrySet()) {
206+
url.put(entry.getKey(), entry.getValue());
207+
}
208+
}
209+
188210
if (pkce != null) {
189211
url.put("code_challenge", pkce.getCodeChallenge());
190212
url.put("code_challenge_method", pkce.getCodeChallengeMethod());
@@ -247,6 +269,21 @@ public UserCredentials getCredentials(String userId) throws IOException {
247269
* @throws IOException An error from the server API call to get the tokens.
248270
*/
249271
public UserCredentials getCredentialsFromCode(String code, URI baseUri) throws IOException {
272+
return getCredentialsFromCode(code, baseUri, null);
273+
}
274+
275+
/**
276+
* Returns a UserCredentials instance by exchanging an OAuth2 authorization code for tokens.
277+
*
278+
* @param code Code returned from OAuth2 consent prompt.
279+
* @param baseUri The URI to resolve the OAuth2 callback URI relative to.
280+
* @param additionalParameters Additional parameters to be added to the post body of token
281+
* endpoint request.
282+
* @return the UserCredentials instance created from the authorization code.
283+
* @throws IOException An error from the server API call to get the tokens.
284+
*/
285+
public UserCredentials getCredentialsFromCode(
286+
String code, URI baseUri, Map<String, String> additionalParameters) throws IOException {
250287
Preconditions.checkNotNull(code);
251288
URI resolvedCallbackUri = getCallbackUri(baseUri);
252289

@@ -257,6 +294,12 @@ public UserCredentials getCredentialsFromCode(String code, URI baseUri) throws I
257294
tokenData.put("redirect_uri", resolvedCallbackUri);
258295
tokenData.put("grant_type", "authorization_code");
259296

297+
if (additionalParameters != null) {
298+
for (Map.Entry<String, String> entry : additionalParameters.entrySet()) {
299+
tokenData.put(entry.getKey(), entry.getValue());
300+
}
301+
}
302+
260303
if (pkce != null) {
261304
tokenData.put("code_verifier", pkce.getCodeVerifier());
262305
}

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public class MockTokenServerTransport extends MockHttpTransport {
6565
final Map<String, String> serviceAccounts = new HashMap<String, String>();
6666
final Map<String, String> gdchServiceAccounts = new HashMap<String, String>();
6767
final Map<String, String> codes = new HashMap<String, String>();
68+
final Map<String, Map<String, String>> additionalParameters =
69+
new HashMap<String, Map<String, String>>();
6870
URI tokenServerUri = OAuth2Utils.TOKEN_SERVER_URI;
6971
private IOException error;
7072
private final Queue<Future<LowLevelHttpResponse>> responseSequence = new ArrayDeque<>();
@@ -81,10 +83,18 @@ public void setTokenServerUri(URI tokenServerUri) {
8183
}
8284

8385
public void addAuthorizationCode(
84-
String code, String refreshToken, String accessToken, String grantedScopes) {
86+
String code,
87+
String refreshToken,
88+
String accessToken,
89+
String grantedScopes,
90+
Map<String, String> additionalParameters) {
8591
codes.put(code, refreshToken);
8692
refreshTokens.put(refreshToken, accessToken);
8793
this.grantedScopes.put(refreshToken, grantedScopes);
94+
95+
if (additionalParameters != null) {
96+
this.additionalParameters.put(refreshToken, additionalParameters);
97+
}
8898
}
8999

90100
public void addClient(String clientId, String clientSecret) {
@@ -220,6 +230,29 @@ public LowLevelHttpResponse execute() throws IOException {
220230
if (grantedScopes.containsKey(refreshToken)) {
221231
grantedScopesString = grantedScopes.get(refreshToken);
222232
}
233+
234+
if (additionalParameters.containsKey(refreshToken)) {
235+
Map<String, String> additionalParametersMap = additionalParameters.get(refreshToken);
236+
for (Map.Entry<String, String> entry : additionalParametersMap.entrySet()) {
237+
String key = entry.getKey();
238+
String expectedValue = entry.getValue();
239+
if (!query.containsKey(key)) {
240+
throw new IllegalArgumentException("Missing additional parameter: " + key);
241+
} else {
242+
String actualValue = query.get(key);
243+
if (!expectedValue.equals(actualValue)) {
244+
throw new IllegalArgumentException(
245+
"For additional parameter "
246+
+ key
247+
+ ", Actual value: "
248+
+ actualValue
249+
+ ", Expected value: "
250+
+ expectedValue);
251+
}
252+
}
253+
}
254+
}
255+
223256
} else if (query.containsKey("grant_type")) {
224257
String grantType = query.get("grant_type");
225258
String assertion = query.get("assertion");

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

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
package com.google.auth.oauth2;
3333

3434
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertFalse;
3536
import static org.junit.Assert.assertNull;
3637
import static org.junit.Assert.assertSame;
3738
import static org.junit.Assert.fail;
@@ -43,6 +44,7 @@
4344
import java.net.URL;
4445
import java.util.Arrays;
4546
import java.util.Date;
47+
import java.util.HashMap;
4648
import java.util.List;
4749
import java.util.Map;
4850
import org.junit.Test;
@@ -170,6 +172,50 @@ public void getAuthorizationUrl() throws IOException {
170172
assertEquals(pkce.getCodeChallengeMethod(), parameters.get("code_challenge_method"));
171173
}
172174

175+
@Test
176+
public void getAuthorizationUrl_additionalParameters() throws IOException {
177+
final String CUSTOM_STATE = "custom_state";
178+
final String PROTOCOL = "https";
179+
final String HOST = "accounts.test.com";
180+
final String PATH = "/o/o/oauth2/auth";
181+
final URI AUTH_URI = URI.create(PROTOCOL + "://" + HOST + PATH);
182+
final String EXPECTED_CALLBACK = "http://example.com" + CALLBACK_URI.toString();
183+
UserAuthorizer authorizer =
184+
UserAuthorizer.newBuilder()
185+
.setClientId(CLIENT_ID)
186+
.setScopes(DUMMY_SCOPES)
187+
.setCallbackUri(CALLBACK_URI)
188+
.setUserAuthUri(AUTH_URI)
189+
.build();
190+
Map<String, String> additionalParameters = new HashMap<String, String>();
191+
additionalParameters.put("param1", "value1");
192+
additionalParameters.put("param2", "value2");
193+
194+
// Verify that the authorization URL doesn't include the additional parameters if they are not
195+
// passed in.
196+
URL authorizationUrl = authorizer.getAuthorizationUrl(USER_ID, CUSTOM_STATE, BASE_URI);
197+
String query = authorizationUrl.getQuery();
198+
Map<String, String> parameters = TestUtils.parseQuery(query);
199+
assertFalse(parameters.containsKey("param1"));
200+
assertFalse(parameters.containsKey("param2"));
201+
202+
// Verify that the authorization URL includes the additional parameters if they are passed in.
203+
authorizationUrl =
204+
authorizer.getAuthorizationUrl(USER_ID, CUSTOM_STATE, BASE_URI, additionalParameters);
205+
query = authorizationUrl.getQuery();
206+
parameters = TestUtils.parseQuery(query);
207+
assertEquals("value1", parameters.get("param1"));
208+
assertEquals("value2", parameters.get("param2"));
209+
210+
// Verify that the authorization URL doesn't include the additional parameters passed in the
211+
// previous call to the authorizer
212+
authorizationUrl = authorizer.getAuthorizationUrl(USER_ID, CUSTOM_STATE, BASE_URI);
213+
query = authorizationUrl.getQuery();
214+
parameters = TestUtils.parseQuery(query);
215+
assertFalse(parameters.containsKey("param1"));
216+
assertFalse(parameters.containsKey("param2"));
217+
}
218+
173219
@Test
174220
public void getCredentials_noCredentials_returnsNull() throws IOException {
175221
UserAuthorizer authorizer =
@@ -340,7 +386,41 @@ public void getCredentialsFromCode_conevertsCodeToTokens() throws IOException {
340386
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
341387
transportFactory.transport.addClient(CLIENT_ID_VALUE, CLIENT_SECRET);
342388
transportFactory.transport.addAuthorizationCode(
343-
CODE, REFRESH_TOKEN, ACCESS_TOKEN_VALUE, GRANTED_SCOPES_STRING);
389+
CODE, REFRESH_TOKEN, ACCESS_TOKEN_VALUE, GRANTED_SCOPES_STRING, null);
390+
TokenStore tokenStore = new MemoryTokensStorage();
391+
UserAuthorizer authorizer =
392+
UserAuthorizer.newBuilder()
393+
.setClientId(CLIENT_ID)
394+
.setScopes(DUMMY_SCOPES)
395+
.setTokenStore(tokenStore)
396+
.setHttpTransportFactory(transportFactory)
397+
.build();
398+
399+
UserCredentials credentials = authorizer.getCredentialsFromCode(CODE, BASE_URI);
400+
401+
assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
402+
assertEquals(ACCESS_TOKEN_VALUE, credentials.getAccessToken().getTokenValue());
403+
assertEquals(GRANTED_SCOPES, credentials.getAccessToken().getScopes());
404+
}
405+
406+
@Test
407+
public void getCredentialsFromCode_additionalParameters() throws IOException {
408+
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
409+
transportFactory.transport.addClient(CLIENT_ID_VALUE, CLIENT_SECRET);
410+
411+
Map<String, String> additionalParameters = new HashMap<String, String>();
412+
additionalParameters.put("param1", "value1");
413+
additionalParameters.put("param2", "value2");
414+
415+
String code2 = "code2";
416+
String refreshToken2 = "refreshToken2";
417+
String accessTokenValue2 = "accessTokenValue2";
418+
419+
transportFactory.transport.addAuthorizationCode(
420+
CODE, REFRESH_TOKEN, ACCESS_TOKEN_VALUE, GRANTED_SCOPES_STRING, null);
421+
transportFactory.transport.addAuthorizationCode(
422+
code2, refreshToken2, accessTokenValue2, GRANTED_SCOPES_STRING, additionalParameters);
423+
344424
TokenStore tokenStore = new MemoryTokensStorage();
345425
UserAuthorizer authorizer =
346426
UserAuthorizer.newBuilder()
@@ -350,8 +430,20 @@ public void getCredentialsFromCode_conevertsCodeToTokens() throws IOException {
350430
.setHttpTransportFactory(transportFactory)
351431
.build();
352432

433+
// Verify that the additional parameters are not attached to the post body when not specified
353434
UserCredentials credentials = authorizer.getCredentialsFromCode(CODE, BASE_URI);
435+
assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
436+
assertEquals(ACCESS_TOKEN_VALUE, credentials.getAccessToken().getTokenValue());
437+
assertEquals(GRANTED_SCOPES, credentials.getAccessToken().getScopes());
438+
439+
// Verify that the additional parameters are attached to the post body when specified
440+
credentials = authorizer.getCredentialsFromCode(code2, BASE_URI, additionalParameters);
441+
assertEquals(refreshToken2, credentials.getRefreshToken());
442+
assertEquals(accessTokenValue2, credentials.getAccessToken().getTokenValue());
443+
assertEquals(GRANTED_SCOPES, credentials.getAccessToken().getScopes());
354444

445+
// Verify that the additional parameters from previous request are not attached to the post body
446+
credentials = authorizer.getCredentialsFromCode(CODE, BASE_URI);
355447
assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
356448
assertEquals(ACCESS_TOKEN_VALUE, credentials.getAccessToken().getTokenValue());
357449
assertEquals(GRANTED_SCOPES, credentials.getAccessToken().getScopes());
@@ -376,7 +468,7 @@ public void getAndStoreCredentialsFromCode_getAndStoresCredentials() throws IOEx
376468
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
377469
transportFactory.transport.addClient(CLIENT_ID_VALUE, CLIENT_SECRET);
378470
transportFactory.transport.addAuthorizationCode(
379-
CODE, REFRESH_TOKEN, accessTokenValue1, GRANTED_SCOPES_STRING);
471+
CODE, REFRESH_TOKEN, accessTokenValue1, GRANTED_SCOPES_STRING, null);
380472
TokenStore tokenStore = new MemoryTokensStorage();
381473
UserAuthorizer authorizer =
382474
UserAuthorizer.newBuilder()

0 commit comments

Comments
 (0)