Skip to content

Commit

Permalink
[SDK-4763] - RIch Authorization Request (RAR) (#637)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmyjames committed Apr 26, 2024
1 parent 3c7a61e commit 2189e29
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 4 deletions.
54 changes: 54 additions & 0 deletions src/main/java/com/auth0/client/auth/AuthAPI.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.auth0.client.auth;

import com.auth0.client.mgmt.ManagementAPI;
import com.auth0.json.ObjectMapperProvider;
import com.auth0.json.auth.*;
import com.auth0.net.*;
import com.auth0.net.client.Auth0HttpClient;
import com.auth0.net.client.DefaultHttpClient;
import com.auth0.net.client.HttpMethod;
import com.auth0.utils.Asserts;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
Expand All @@ -17,6 +19,8 @@
import java.util.Map;
import java.util.Objects;

import static com.auth0.json.ObjectMapperProvider.getMapper;

/**
* Class that provides an implementation of of the Authentication and Authorization API methods defined by the
* <a href="https://auth0.com/docs/api/authentication">Auth0 Authentication API</a>.
Expand Down Expand Up @@ -267,6 +271,23 @@ public String authorizeUrlWithJAR(String request) {
* @return a request to execute.
*/
public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String redirectUri, String responseType, Map<String, String> params) {
return pushedAuthorizationRequest(redirectUri, responseType, params, null);
}

/**
* Builds a request to make a Pushed Authorization Request (PAR) to receive a {@code request_uri} to send to the {@code /authorize} endpoint.
* @param redirectUri the URL to redirect to after authorization has been granted by the user. Your Auth0 application
* must have this URL as one of its Allowed Callback URLs. Must be a valid non-encoded URL.
* @param responseType the response type to set. Must not be null.
* @param params an optional map of key/value pairs representing any additional parameters to send on the request.
* @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.
* @see #pushedAuthorizationRequest(String, String, Map, List)
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9396">RFC 9396</a>
* @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>
* @return a request to execute.
*/
public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String redirectUri, String responseType, Map<String, String> params, List<Map<String, Object>> authorizationDetails) {
Asserts.assertValidUrl(redirectUri, "redirect uri");
Asserts.assertNotNull(responseType, "response type");

Expand All @@ -286,18 +307,43 @@ public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String re
if (params != null) {
params.forEach(request::addParameter);
}
try {
if (Objects.nonNull(authorizationDetails)) {
String authDetailsJson = getMapper().writeValueAsString(authorizationDetails);
request.addParameter("authorization_details", authDetailsJson);
}
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("'authorizationDetails' must be a list that can be serialized to JSON", e);
}
return request;
}

/**
* 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.
* @param request The signed JWT containing the authorization parameters as claims.
* @see #pushedAuthorizationRequestWithJAR(String, List)
* @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>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9101">RFC 9101</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
* @return a request to execute.
*/
public Request<PushedAuthorizationResponse> pushedAuthorizationRequestWithJAR(String request) {
return pushedAuthorizationRequestWithJAR(request, null);
}

/**
* 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.
* @param request The signed JWT containing the authorization parameters as claims.
* @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.
* @see #pushedAuthorizationRequestWithJAR(String)
* @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>
* @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>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9101">RFC 9101</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9396">RFC 9396</a>
* @return a request to execute.
*/
public Request<PushedAuthorizationResponse> pushedAuthorizationRequestWithJAR(String request, List<Map<String, Object>> authorizationDetails) {
Asserts.assertNotNull(request, "request");

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

try {
if (Objects.nonNull(authorizationDetails)) {
String authDetailsJson = getMapper().writeValueAsString(authorizationDetails);
req.addParameter("authorization_details", authDetailsJson);
}
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("'authorizationDetails' must be a list that can be serialized to JSON", e);
}
return req;
}

Expand Down
148 changes: 144 additions & 4 deletions src/test/java/com/auth0/client/auth/AuthAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.auth0.client.MockServer;
import com.auth0.exception.APIException;
import com.auth0.json.ObjectMapperProvider;
import com.auth0.json.auth.*;
import com.auth0.net.BaseRequest;
import com.auth0.net.Request;
Expand All @@ -11,6 +12,7 @@
import com.auth0.net.client.Auth0HttpRequest;
import com.auth0.net.client.Auth0HttpResponse;
import com.auth0.net.client.HttpMethod;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.mockwebserver.RecordedRequest;
Expand All @@ -19,11 +21,11 @@
import org.junit.jupiter.api.Test;

import java.io.FileReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import static com.auth0.AssertsUtil.verifyThrows;
import static com.auth0.client.MockServer.*;
Expand All @@ -35,6 +37,8 @@
import static org.hamcrest.Matchers.*;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
import static org.hamcrest.collection.IsMapContaining.hasKey;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class AuthAPITest {

Expand Down Expand Up @@ -256,6 +260,7 @@ public void shouldCreateUserInfoRequest() throws Exception {
assertThat(response.getValues(), hasEntry("created_at", "2016-12-05T11:16:59.640Z"));
assertThat(response.getValues(), hasEntry("sub", "auth0|58454..."));
assertThat(response.getValues(), hasKey("identities"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> identities = (List<Map<String, Object>>) response.getValues().get("identities");
assertThat(identities, hasSize(1));
assertThat(identities.get(0), hasEntry("user_id", "58454..."));
Expand Down Expand Up @@ -497,6 +502,7 @@ public void shouldCreateSignUpRequestWithCustomParameters() throws Exception {
assertThat(body, hasEntry("connection", "db-connection"));
assertThat(body, hasEntry("client_id", CLIENT_ID));
assertThat(body, hasKey("user_metadata"));
@SuppressWarnings("unchecked")
Map<String, String> metadata = (Map<String, String>) body.get("user_metadata");
assertThat(metadata, hasEntry("age", "25"));
assertThat(metadata, hasEntry("address", "123, fake street"));
Expand Down Expand Up @@ -1004,6 +1010,7 @@ public void shouldCreateStartEmailPasswordlessFlowRequestWithCustomParams() thro
assertThat(body, hasEntry("client_secret", CLIENT_SECRET));
assertThat(body, hasEntry("email", "[email protected]"));
assertThat(body, hasKey("authParams"));
@SuppressWarnings("unchecked")
Map<String, String> authParamsSent = (Map<String, String>) body.get("authParams");
assertThat(authParamsSent, hasEntry("scope", authParams.get("scope")));
assertThat(authParamsSent, hasEntry("state", authParams.get("state")));
Expand Down Expand Up @@ -1745,6 +1752,68 @@ public void shouldCreatePushedAuthorizationRequestWithAdditionalParams() throws
assertThat(response.getExpiresIn(), notNullValue());
}

@Test
@SuppressWarnings("unchecked")
public void shouldCreatePushedAuthorizationRequestWithAuthDetails() throws Exception {
Map<String, Object> authorizationDetails = new HashMap<>();
authorizationDetails.put("type", "account information");
authorizationDetails.put("locations", Collections.singletonList("https://example.com/customers"));
authorizationDetails.put("actions", Arrays.asList("read", "write"));
List<Map<String, Object>> authDetailsList = Collections.singletonList(authorizationDetails);

Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequest("https://domain.com/callback", "code", null, authDetailsList);
assertThat(request, is(notNullValue()));

server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
PushedAuthorizationResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/par"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));

String body = readFromRequest(recordedRequest);
assertThat(body, containsString("client_id=" + CLIENT_ID));
assertThat(body, containsString("redirect_uri=" + "https%3A%2F%2Fdomain.com%2Fcallback"));
assertThat(body, containsString("response_type=" + "code"));
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));

String authDetailsParam = getQueryMap(body).get("authorization_details");
String decodedAuthDetails = URLDecoder.decode(authDetailsParam, StandardCharsets.UTF_8.name());
TypeReference<List<Map<String, Object>>> typeReference = new TypeReference<List<Map<String, Object>>>() {
};
List<Map<String, Object>> deserialized = ObjectMapperProvider.getMapper().readValue(decodedAuthDetails, typeReference);
assertThat(deserialized, notNullValue());
assertThat(deserialized, hasSize(1));
assertThat(deserialized.get(0).get("type"), is("account information"));

List<String> locations = (List<String>) deserialized.get(0).get("locations");
List<String> actions = (List<String>) deserialized.get(0).get("actions");

assertThat(locations, hasSize(1));
assertThat(locations.get(0), is("https://example.com/customers"));
assertThat(actions, hasSize(2));
assertThat(actions, contains("read", "write"));

assertThat(response, is(notNullValue()));
assertThat(response.getRequestURI(), not(emptyOrNullString()));
assertThat(response.getExpiresIn(), notNullValue());
}

@Test
public void shouldThrowWhenCreatePushedAuthorizationRequestWithInvalidAuthDetails() {
// force Jackson to throw error on serialization
// see https://stackoverflow.com/questions/26716020/how-to-get-a-jsonprocessingexception-using-jackson
@SuppressWarnings("unchecked")
List<Map<String, Object>> mockList = mock(List.class);
when(mockList.toString()).thenReturn(mockList.getClass().getName());

IllegalArgumentException e = verifyThrows(IllegalArgumentException.class,
() -> api.pushedAuthorizationRequest("https://domain.com/callback", "code", null, mockList));

assertThat(e.getMessage(), is("'authorizationDetails' must be a list that can be serialized to JSON"));
assertThat(e.getCause(), instanceOf(JsonProcessingException.class));
}

@Test
public void shouldCreatePushedAuthorizationRequestWithoutSecret() throws Exception {
AuthAPI api = AuthAPI.newBuilder(server.getBaseUrl(), CLIENT_ID).build();
Expand Down Expand Up @@ -1842,6 +1911,77 @@ public void shouldCreatePushedAuthorizationJarRequestWithoutSecret() throws Exce
assertThat(response.getExpiresIn(), notNullValue());
}

@Test
@SuppressWarnings("unchecked")
public void shouldCreatePushedAuthorizationJarRequestWithoutAuthDetails() throws Exception {
String requestJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiIxMjM0NTYiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJyZXNwb25zZV90eXBlIjoiY29kZSIsIm5vbmNlIjoiMTIzNCIsInN0YXRlIjoiNzhkeXVma2poZGYifQ.UQDz8hBIabaqatY75BvqGyiPoOqNYJQIsimUKg4_VrU";
Map<String, Object> authorizationDetails = new HashMap<>();
authorizationDetails.put("type", "account information");
authorizationDetails.put("locations", Collections.singletonList("https://example.com/customers"));
authorizationDetails.put("actions", Arrays.asList("read", "write"));
List<Map<String, Object>> authDetailsList = Collections.singletonList(authorizationDetails);

Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequestWithJAR(requestJwt, authDetailsList);
assertThat(request, is(notNullValue()));

server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
PushedAuthorizationResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/par"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));

String body = readFromRequest(recordedRequest);
assertThat(body, containsString("client_id=" + CLIENT_ID));
assertThat(body, containsString("request=" + requestJwt));
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));

String authDetailsParam = getQueryMap(body).get("authorization_details");
String decodedAuthDetails = URLDecoder.decode(authDetailsParam, StandardCharsets.UTF_8.name());
TypeReference<List<Map<String, Object>>> typeReference = new TypeReference<List<Map<String, Object>>>() {
};
List<Map<String, Object>> deserialized = ObjectMapperProvider.getMapper().readValue(decodedAuthDetails, typeReference);
assertThat(deserialized, notNullValue());
assertThat(deserialized, hasSize(1));
assertThat(deserialized.get(0).get("type"), is("account information"));

List<String> locations = (List<String>) deserialized.get(0).get("locations");
List<String> actions = (List<String>) deserialized.get(0).get("actions");

assertThat(locations, hasSize(1));
assertThat(locations.get(0), is("https://example.com/customers"));
assertThat(actions, hasSize(2));
assertThat(actions, contains("read", "write"));

assertThat(response, is(notNullValue()));
assertThat(response.getRequestURI(), not(emptyOrNullString()));
assertThat(response.getExpiresIn(), notNullValue());
}

@Test
@SuppressWarnings("unchecked")
public void shouldThrowWhenCreatePushedAuthorizationJarRequestWithInvalidAuthDetails() {
String requestJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiIxMjM0NTYiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJyZXNwb25zZV90eXBlIjoiY29kZSIsIm5vbmNlIjoiMTIzNCIsInN0YXRlIjoiNzhkeXVma2poZGYifQ.UQDz8hBIabaqatY75BvqGyiPoOqNYJQIsimUKg4_VrU";
// force Jackson to throw error on serialization
// see https://stackoverflow.com/questions/26716020/how-to-get-a-jsonprocessingexception-using-jackson
List mockList = mock(List.class);
when(mockList.toString()).thenReturn(mockList.getClass().getName());

IllegalArgumentException e = verifyThrows(IllegalArgumentException.class,
() -> api.pushedAuthorizationRequestWithJAR(requestJwt, mockList));

assertThat(e.getMessage(), is("'authorizationDetails' must be a list that can be serialized to JSON"));
assertThat(e.getCause(), instanceOf(JsonProcessingException.class));
}

private Map<String, String> getQueryMap(String input) {
String[] params = input.split("&");

return Arrays.stream(params)
.map(param -> param.split("="))
.collect(Collectors.toMap(p -> p[0], p -> p[1]));
}

static class TestAssertionSigner implements ClientAssertionSigner {

private final String token;
Expand Down

0 comments on commit 2189e29

Please sign in to comment.