Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-4763] - RIch Authorization Request (RAR) #637

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading