Skip to content

Commit 2189e29

Browse files
authored
[SDK-4763] - RIch Authorization Request (RAR) (#637)
1 parent 3c7a61e commit 2189e29

2 files changed

Lines changed: 198 additions & 4 deletions

File tree

src/main/java/com/auth0/client/auth/AuthAPI.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.auth0.client.auth;
22

33
import com.auth0.client.mgmt.ManagementAPI;
4+
import com.auth0.json.ObjectMapperProvider;
45
import com.auth0.json.auth.*;
56
import com.auth0.net.*;
67
import com.auth0.net.client.Auth0HttpClient;
78
import com.auth0.net.client.DefaultHttpClient;
89
import com.auth0.net.client.HttpMethod;
910
import com.auth0.utils.Asserts;
11+
import com.fasterxml.jackson.core.JsonProcessingException;
1012
import com.fasterxml.jackson.core.type.TypeReference;
1113
import okhttp3.HttpUrl;
1214
import okhttp3.OkHttpClient;
@@ -17,6 +19,8 @@
1719
import java.util.Map;
1820
import java.util.Objects;
1921

22+
import static com.auth0.json.ObjectMapperProvider.getMapper;
23+
2024
/**
2125
* Class that provides an implementation of of the Authentication and Authorization API methods defined by the
2226
* <a href="https://auth0.com/docs/api/authentication">Auth0 Authentication API</a>.
@@ -267,6 +271,23 @@ public String authorizeUrlWithJAR(String request) {
267271
* @return a request to execute.
268272
*/
269273
public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String redirectUri, String responseType, Map<String, String> params) {
274+
return pushedAuthorizationRequest(redirectUri, responseType, params, null);
275+
}
276+
277+
/**
278+
* Builds a request to make a Pushed Authorization Request (PAR) to receive a {@code request_uri} to send to the {@code /authorize} endpoint.
279+
* @param redirectUri the URL to redirect to after authorization has been granted by the user. Your Auth0 application
280+
* must have this URL as one of its Allowed Callback URLs. Must be a valid non-encoded URL.
281+
* @param responseType the response type to set. Must not be null.
282+
* @param params an optional map of key/value pairs representing any additional parameters to send on the request.
283+
* @param authorizationDetails A list of maps representing the value of the (optional) {@code authorization_details} parameter, used to perform Rich Authorization Requests. The list will be serialized to JSON and sent on the request.
284+
* @see #pushedAuthorizationRequest(String, String, Map, List)
285+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
286+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9396">RFC 9396</a>
287+
* @see <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow/authorization-code-flow-with-rar">Authorization Code Flow with Rich Authorization Requests (RAR)</a>
288+
* @return a request to execute.
289+
*/
290+
public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String redirectUri, String responseType, Map<String, String> params, List<Map<String, Object>> authorizationDetails) {
270291
Asserts.assertValidUrl(redirectUri, "redirect uri");
271292
Asserts.assertNotNull(responseType, "response type");
272293

@@ -286,18 +307,43 @@ public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String re
286307
if (params != null) {
287308
params.forEach(request::addParameter);
288309
}
310+
try {
311+
if (Objects.nonNull(authorizationDetails)) {
312+
String authDetailsJson = getMapper().writeValueAsString(authorizationDetails);
313+
request.addParameter("authorization_details", authDetailsJson);
314+
}
315+
} catch (JsonProcessingException e) {
316+
throw new IllegalArgumentException("'authorizationDetails' must be a list that can be serialized to JSON", e);
317+
}
289318
return request;
290319
}
291320

292321
/**
293322
* Builds a request to make a Pushed Authorization Request (PAR) with JWT-Secured Authorization Requests (JAR), to receive a {@code request_uri} to send to the {@code /authorize} endpoint.
294323
* @param request The signed JWT containing the authorization parameters as claims.
324+
* @see #pushedAuthorizationRequestWithJAR(String, List)
295325
* @see <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow/authorization-code-flow-with-par-and-jar">Authorization Code Flow with PAR and JAR</a>
296326
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9101">RFC 9101</a>
297327
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
298328
* @return a request to execute.
299329
*/
300330
public Request<PushedAuthorizationResponse> pushedAuthorizationRequestWithJAR(String request) {
331+
return pushedAuthorizationRequestWithJAR(request, null);
332+
}
333+
334+
/**
335+
* Builds a request to make a Pushed Authorization Request (PAR) with JWT-Secured Authorization Requests (JAR), to receive a {@code request_uri} to send to the {@code /authorize} endpoint.
336+
* @param request The signed JWT containing the authorization parameters as claims.
337+
* @param authorizationDetails A list of maps representing the value of the (optional) {@code authorization_details} parameter, used to perform Rich Authorization Requests. The list will be serialized to JSON and sent on the request.
338+
* @see #pushedAuthorizationRequestWithJAR(String)
339+
* @see <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow/authorization-code-flow-with-par-and-jar">Authorization Code Flow with PAR and JAR</a>
340+
* @see <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow/authorization-code-flow-with-rar">Authorization Code Flow with Rich Authorization Requests (RAR)</a>
341+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9101">RFC 9101</a>
342+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
343+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9396">RFC 9396</a>
344+
* @return a request to execute.
345+
*/
346+
public Request<PushedAuthorizationResponse> pushedAuthorizationRequestWithJAR(String request, List<Map<String, Object>> authorizationDetails) {
301347
Asserts.assertNotNull(request, "request");
302348

303349
String url = baseUrl
@@ -313,6 +359,14 @@ public Request<PushedAuthorizationResponse> pushedAuthorizationRequestWithJAR(St
313359
req.addParameter("client_secret", clientSecret);
314360
}
315361

362+
try {
363+
if (Objects.nonNull(authorizationDetails)) {
364+
String authDetailsJson = getMapper().writeValueAsString(authorizationDetails);
365+
req.addParameter("authorization_details", authDetailsJson);
366+
}
367+
} catch (JsonProcessingException e) {
368+
throw new IllegalArgumentException("'authorizationDetails' must be a list that can be serialized to JSON", e);
369+
}
316370
return req;
317371
}
318372

src/test/java/com/auth0/client/auth/AuthAPITest.java

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.auth0.client.MockServer;
44
import com.auth0.exception.APIException;
5+
import com.auth0.json.ObjectMapperProvider;
56
import com.auth0.json.auth.*;
67
import com.auth0.net.BaseRequest;
78
import com.auth0.net.Request;
@@ -11,6 +12,7 @@
1112
import com.auth0.net.client.Auth0HttpRequest;
1213
import com.auth0.net.client.Auth0HttpResponse;
1314
import com.auth0.net.client.HttpMethod;
15+
import com.fasterxml.jackson.core.JsonProcessingException;
1416
import com.fasterxml.jackson.core.type.TypeReference;
1517
import com.fasterxml.jackson.databind.ObjectMapper;
1618
import okhttp3.mockwebserver.RecordedRequest;
@@ -19,11 +21,11 @@
1921
import org.junit.jupiter.api.Test;
2022

2123
import java.io.FileReader;
22-
import java.util.Collections;
23-
import java.util.HashMap;
24-
import java.util.List;
25-
import java.util.Map;
24+
import java.net.URLDecoder;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.*;
2627
import java.util.concurrent.CompletableFuture;
28+
import java.util.stream.Collectors;
2729

2830
import static com.auth0.AssertsUtil.verifyThrows;
2931
import static com.auth0.client.MockServer.*;
@@ -35,6 +37,8 @@
3537
import static org.hamcrest.Matchers.*;
3638
import static org.hamcrest.collection.IsMapContaining.hasEntry;
3739
import static org.hamcrest.collection.IsMapContaining.hasKey;
40+
import static org.mockito.Mockito.mock;
41+
import static org.mockito.Mockito.when;
3842

3943
public class AuthAPITest {
4044

@@ -256,6 +260,7 @@ public void shouldCreateUserInfoRequest() throws Exception {
256260
assertThat(response.getValues(), hasEntry("created_at", "2016-12-05T11:16:59.640Z"));
257261
assertThat(response.getValues(), hasEntry("sub", "auth0|58454..."));
258262
assertThat(response.getValues(), hasKey("identities"));
263+
@SuppressWarnings("unchecked")
259264
List<Map<String, Object>> identities = (List<Map<String, Object>>) response.getValues().get("identities");
260265
assertThat(identities, hasSize(1));
261266
assertThat(identities.get(0), hasEntry("user_id", "58454..."));
@@ -497,6 +502,7 @@ public void shouldCreateSignUpRequestWithCustomParameters() throws Exception {
497502
assertThat(body, hasEntry("connection", "db-connection"));
498503
assertThat(body, hasEntry("client_id", CLIENT_ID));
499504
assertThat(body, hasKey("user_metadata"));
505+
@SuppressWarnings("unchecked")
500506
Map<String, String> metadata = (Map<String, String>) body.get("user_metadata");
501507
assertThat(metadata, hasEntry("age", "25"));
502508
assertThat(metadata, hasEntry("address", "123, fake street"));
@@ -1004,6 +1010,7 @@ public void shouldCreateStartEmailPasswordlessFlowRequestWithCustomParams() thro
10041010
assertThat(body, hasEntry("client_secret", CLIENT_SECRET));
10051011
assertThat(body, hasEntry("email", "user@domain.com"));
10061012
assertThat(body, hasKey("authParams"));
1013+
@SuppressWarnings("unchecked")
10071014
Map<String, String> authParamsSent = (Map<String, String>) body.get("authParams");
10081015
assertThat(authParamsSent, hasEntry("scope", authParams.get("scope")));
10091016
assertThat(authParamsSent, hasEntry("state", authParams.get("state")));
@@ -1745,6 +1752,68 @@ public void shouldCreatePushedAuthorizationRequestWithAdditionalParams() throws
17451752
assertThat(response.getExpiresIn(), notNullValue());
17461753
}
17471754

1755+
@Test
1756+
@SuppressWarnings("unchecked")
1757+
public void shouldCreatePushedAuthorizationRequestWithAuthDetails() throws Exception {
1758+
Map<String, Object> authorizationDetails = new HashMap<>();
1759+
authorizationDetails.put("type", "account information");
1760+
authorizationDetails.put("locations", Collections.singletonList("https://example.com/customers"));
1761+
authorizationDetails.put("actions", Arrays.asList("read", "write"));
1762+
List<Map<String, Object>> authDetailsList = Collections.singletonList(authorizationDetails);
1763+
1764+
Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequest("https://domain.com/callback", "code", null, authDetailsList);
1765+
assertThat(request, is(notNullValue()));
1766+
1767+
server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
1768+
PushedAuthorizationResponse response = request.execute().getBody();
1769+
RecordedRequest recordedRequest = server.takeRequest();
1770+
1771+
assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/par"));
1772+
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));
1773+
1774+
String body = readFromRequest(recordedRequest);
1775+
assertThat(body, containsString("client_id=" + CLIENT_ID));
1776+
assertThat(body, containsString("redirect_uri=" + "https%3A%2F%2Fdomain.com%2Fcallback"));
1777+
assertThat(body, containsString("response_type=" + "code"));
1778+
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));
1779+
1780+
String authDetailsParam = getQueryMap(body).get("authorization_details");
1781+
String decodedAuthDetails = URLDecoder.decode(authDetailsParam, StandardCharsets.UTF_8.name());
1782+
TypeReference<List<Map<String, Object>>> typeReference = new TypeReference<List<Map<String, Object>>>() {
1783+
};
1784+
List<Map<String, Object>> deserialized = ObjectMapperProvider.getMapper().readValue(decodedAuthDetails, typeReference);
1785+
assertThat(deserialized, notNullValue());
1786+
assertThat(deserialized, hasSize(1));
1787+
assertThat(deserialized.get(0).get("type"), is("account information"));
1788+
1789+
List<String> locations = (List<String>) deserialized.get(0).get("locations");
1790+
List<String> actions = (List<String>) deserialized.get(0).get("actions");
1791+
1792+
assertThat(locations, hasSize(1));
1793+
assertThat(locations.get(0), is("https://example.com/customers"));
1794+
assertThat(actions, hasSize(2));
1795+
assertThat(actions, contains("read", "write"));
1796+
1797+
assertThat(response, is(notNullValue()));
1798+
assertThat(response.getRequestURI(), not(emptyOrNullString()));
1799+
assertThat(response.getExpiresIn(), notNullValue());
1800+
}
1801+
1802+
@Test
1803+
public void shouldThrowWhenCreatePushedAuthorizationRequestWithInvalidAuthDetails() {
1804+
// force Jackson to throw error on serialization
1805+
// see https://stackoverflow.com/questions/26716020/how-to-get-a-jsonprocessingexception-using-jackson
1806+
@SuppressWarnings("unchecked")
1807+
List<Map<String, Object>> mockList = mock(List.class);
1808+
when(mockList.toString()).thenReturn(mockList.getClass().getName());
1809+
1810+
IllegalArgumentException e = verifyThrows(IllegalArgumentException.class,
1811+
() -> api.pushedAuthorizationRequest("https://domain.com/callback", "code", null, mockList));
1812+
1813+
assertThat(e.getMessage(), is("'authorizationDetails' must be a list that can be serialized to JSON"));
1814+
assertThat(e.getCause(), instanceOf(JsonProcessingException.class));
1815+
}
1816+
17481817
@Test
17491818
public void shouldCreatePushedAuthorizationRequestWithoutSecret() throws Exception {
17501819
AuthAPI api = AuthAPI.newBuilder(server.getBaseUrl(), CLIENT_ID).build();
@@ -1842,6 +1911,77 @@ public void shouldCreatePushedAuthorizationJarRequestWithoutSecret() throws Exce
18421911
assertThat(response.getExpiresIn(), notNullValue());
18431912
}
18441913

1914+
@Test
1915+
@SuppressWarnings("unchecked")
1916+
public void shouldCreatePushedAuthorizationJarRequestWithoutAuthDetails() throws Exception {
1917+
String requestJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiIxMjM0NTYiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJyZXNwb25zZV90eXBlIjoiY29kZSIsIm5vbmNlIjoiMTIzNCIsInN0YXRlIjoiNzhkeXVma2poZGYifQ.UQDz8hBIabaqatY75BvqGyiPoOqNYJQIsimUKg4_VrU";
1918+
Map<String, Object> authorizationDetails = new HashMap<>();
1919+
authorizationDetails.put("type", "account information");
1920+
authorizationDetails.put("locations", Collections.singletonList("https://example.com/customers"));
1921+
authorizationDetails.put("actions", Arrays.asList("read", "write"));
1922+
List<Map<String, Object>> authDetailsList = Collections.singletonList(authorizationDetails);
1923+
1924+
Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequestWithJAR(requestJwt, authDetailsList);
1925+
assertThat(request, is(notNullValue()));
1926+
1927+
server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
1928+
PushedAuthorizationResponse response = request.execute().getBody();
1929+
RecordedRequest recordedRequest = server.takeRequest();
1930+
1931+
assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/par"));
1932+
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));
1933+
1934+
String body = readFromRequest(recordedRequest);
1935+
assertThat(body, containsString("client_id=" + CLIENT_ID));
1936+
assertThat(body, containsString("request=" + requestJwt));
1937+
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));
1938+
1939+
String authDetailsParam = getQueryMap(body).get("authorization_details");
1940+
String decodedAuthDetails = URLDecoder.decode(authDetailsParam, StandardCharsets.UTF_8.name());
1941+
TypeReference<List<Map<String, Object>>> typeReference = new TypeReference<List<Map<String, Object>>>() {
1942+
};
1943+
List<Map<String, Object>> deserialized = ObjectMapperProvider.getMapper().readValue(decodedAuthDetails, typeReference);
1944+
assertThat(deserialized, notNullValue());
1945+
assertThat(deserialized, hasSize(1));
1946+
assertThat(deserialized.get(0).get("type"), is("account information"));
1947+
1948+
List<String> locations = (List<String>) deserialized.get(0).get("locations");
1949+
List<String> actions = (List<String>) deserialized.get(0).get("actions");
1950+
1951+
assertThat(locations, hasSize(1));
1952+
assertThat(locations.get(0), is("https://example.com/customers"));
1953+
assertThat(actions, hasSize(2));
1954+
assertThat(actions, contains("read", "write"));
1955+
1956+
assertThat(response, is(notNullValue()));
1957+
assertThat(response.getRequestURI(), not(emptyOrNullString()));
1958+
assertThat(response.getExpiresIn(), notNullValue());
1959+
}
1960+
1961+
@Test
1962+
@SuppressWarnings("unchecked")
1963+
public void shouldThrowWhenCreatePushedAuthorizationJarRequestWithInvalidAuthDetails() {
1964+
String requestJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiIxMjM0NTYiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJyZXNwb25zZV90eXBlIjoiY29kZSIsIm5vbmNlIjoiMTIzNCIsInN0YXRlIjoiNzhkeXVma2poZGYifQ.UQDz8hBIabaqatY75BvqGyiPoOqNYJQIsimUKg4_VrU";
1965+
// force Jackson to throw error on serialization
1966+
// see https://stackoverflow.com/questions/26716020/how-to-get-a-jsonprocessingexception-using-jackson
1967+
List mockList = mock(List.class);
1968+
when(mockList.toString()).thenReturn(mockList.getClass().getName());
1969+
1970+
IllegalArgumentException e = verifyThrows(IllegalArgumentException.class,
1971+
() -> api.pushedAuthorizationRequestWithJAR(requestJwt, mockList));
1972+
1973+
assertThat(e.getMessage(), is("'authorizationDetails' must be a list that can be serialized to JSON"));
1974+
assertThat(e.getCause(), instanceOf(JsonProcessingException.class));
1975+
}
1976+
1977+
private Map<String, String> getQueryMap(String input) {
1978+
String[] params = input.split("&");
1979+
1980+
return Arrays.stream(params)
1981+
.map(param -> param.split("="))
1982+
.collect(Collectors.toMap(p -> p[0], p -> p[1]));
1983+
}
1984+
18451985
static class TestAssertionSigner implements ClientAssertionSigner {
18461986

18471987
private final String token;

0 commit comments

Comments
 (0)