Skip to content

Commit 76b5849

Browse files
authored
[SDK-4143] add support for Pushed Authorization Requests (#531)
1 parent d82d783 commit 76b5849

9 files changed

Lines changed: 364 additions & 1 deletion

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
@@ -5,6 +5,7 @@
55
import com.auth0.client.ProxyOptions;
66
import com.auth0.json.auth.PasswordlessEmailResponse;
77
import com.auth0.json.auth.PasswordlessSmsResponse;
8+
import com.auth0.json.auth.PushedAuthorizationResponse;
89
import com.auth0.json.auth.UserInfo;
910
import com.auth0.net.Request;
1011
import com.auth0.net.*;
@@ -13,8 +14,10 @@
1314
import okhttp3.*;
1415
import okhttp3.logging.HttpLoggingInterceptor;
1516
import okhttp3.logging.HttpLoggingInterceptor.Level;
17+
import org.jetbrains.annotations.NotNull;
1618

1719
import java.io.IOException;
20+
import java.util.Map;
1821
import java.util.concurrent.TimeUnit;
1922

2023
/**
@@ -238,6 +241,57 @@ public AuthorizeUrlBuilder authorizeUrl(String redirectUri) {
238241
return AuthorizeUrlBuilder.newInstance(baseUrl, clientId, redirectUri);
239242
}
240243

244+
/**
245+
* Builds an authorization URL for Pushed Authorization Requests (PAR)
246+
* @param requestUri the {@code request_uri} parameter from a successful pushed authorization request.
247+
* @see AuthAPI#pushedAuthorizationRequest(String, String, Map)
248+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
249+
* @return the {@code request_uri} from a successful pushed authorization request.
250+
*/
251+
public String authorizeUrlWithPAR(String requestUri) {
252+
Asserts.assertNotNull(requestUri, "request uri");
253+
return baseUrl
254+
.newBuilder()
255+
.addPathSegment("authorize")
256+
.addQueryParameter("client_id", clientId)
257+
.addQueryParameter("request_uri", requestUri)
258+
.build()
259+
.toString();
260+
}
261+
262+
/**
263+
* Builds a request to make a Pushed Authorization Request (PAR) to receive a {@code request_uri} to send to the {@code /authorize} endpoint.
264+
* @param redirectUri the URL to redirect to after authorization has been granted by the user. Your Auth0 application
265+
* must have this URL as one of its Allowed Callback URLs. Must be a valid non-encoded URL.
266+
* @param responseType the response type to set. Must not be null.
267+
* @param params an optional map of key/value pairs representing any additional parameters to send on the request.
268+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
269+
* @return a request to execute.
270+
*/
271+
public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String redirectUri, String responseType, Map<String, String> params) {
272+
Asserts.assertValidUrl(redirectUri, "redirect uri");
273+
Asserts.assertNotNull(responseType, "response type");
274+
275+
String url = baseUrl
276+
.newBuilder()
277+
.addPathSegments("oauth/par")
278+
.build()
279+
.toString();
280+
281+
FormBodyRequest<PushedAuthorizationResponse>
282+
request = new FormBodyRequest<>(client, url, "POST", new TypeReference<PushedAuthorizationResponse>() {});
283+
request.addData("client_id", clientId);
284+
request.addData("client_secret", clientSecret);
285+
request.addData("redirect_uri", redirectUri);
286+
request.addData("response_type", responseType);
287+
288+
if (params != null) {
289+
params.forEach(request::addData);
290+
}
291+
292+
return request;
293+
}
294+
241295
/**
242296
* Creates an instance of the {@link LogoutUrlBuilder} with the given return-to url.
243297
* i.e.:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.auth0.json.auth;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
5+
import com.fasterxml.jackson.annotation.JsonInclude;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
8+
@JsonIgnoreProperties(ignoreUnknown = true)
9+
@JsonInclude(JsonInclude.Include.NON_NULL)
10+
public class PushedAuthorizationResponse {
11+
12+
@JsonProperty("request_uri")
13+
private String requestURI;
14+
@JsonProperty("expires_in")
15+
private Integer expiresIn;
16+
17+
@JsonCreator
18+
public PushedAuthorizationResponse(@JsonProperty("request_uri") String requestURI, @JsonProperty("expires_in") Integer expiresIn) {
19+
this.requestURI = requestURI;
20+
this.expiresIn = expiresIn;
21+
}
22+
23+
public String getRequestURI() {
24+
return requestURI;
25+
}
26+
27+
public Integer getExpiresIn() {
28+
return expiresIn;
29+
}
30+
}

src/main/java/com/auth0/json/mgmt/client/Client.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ public class Client {
9090
private Boolean crossOriginAuth;
9191
@JsonProperty("cross_origin_loc")
9292
private String crossOriginLoc;
93+
@JsonProperty("require_pushed_authorization_requests")
94+
private Boolean requiresPushedAuthorizationRequests;
9395

9496
/**
9597
* Getter for the name of the tenant this client belongs to.
@@ -793,5 +795,20 @@ public void setCrossOriginLoc(String crossOriginLoc) {
793795
public String getCrossOriginLoc() {
794796
return crossOriginLoc;
795797
}
798+
799+
/**
800+
* @return whether this client requires pushed authorization requests or not.
801+
*/
802+
public Boolean getRequiresPushedAuthorizationRequests() {
803+
return requiresPushedAuthorizationRequests;
804+
}
805+
806+
/**
807+
* Sets whether the client requires pushed authorization requests or not.
808+
* @param requiresPushedAuthorizationRequests true if the client should require pushed authorization requests, false if not.
809+
*/
810+
public void setRequiresPushedAuthorizationRequests(Boolean requiresPushedAuthorizationRequests) {
811+
this.requiresPushedAuthorizationRequests = requiresPushedAuthorizationRequests;
812+
}
796813
}
797814

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.auth0.net;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import okhttp3.*;
6+
7+
import java.io.File;
8+
import java.io.IOException;
9+
import java.util.HashMap;
10+
11+
import static com.auth0.utils.Asserts.assertNotNull;
12+
13+
/**
14+
* Request class that represents a application/x-www-form-urlencoded request
15+
*
16+
* @param <T> The type expected to be received as part of the response.
17+
* @see ExtendedBaseRequest
18+
*/
19+
public class FormBodyRequest<T> extends ExtendedBaseRequest<T> {
20+
21+
private static final String CONTENT_TYPE_FORM_DATA = "application/x-www-form-urlencoded";
22+
23+
private final TypeReference<T> tType;
24+
private final ObjectMapper mapper;
25+
private final FormBody.Builder bodyBuilder;
26+
27+
FormBodyRequest(OkHttpClient client, String url, String method, ObjectMapper mapper, TypeReference<T> tType, FormBody.Builder bodyBuilder) {
28+
super(client, url, method, mapper);
29+
if ("GET".equalsIgnoreCase(method)) {
30+
throw new IllegalArgumentException("application/x-www-form-urlencoded requests do not support the GET method.");
31+
}
32+
this.mapper = mapper;
33+
this.tType = tType;
34+
this.bodyBuilder = bodyBuilder;
35+
}
36+
37+
public FormBodyRequest(OkHttpClient client, String url, String method, TypeReference<T> tType) {
38+
this(client, url, method, new ObjectMapper(), tType, new FormBody.Builder());
39+
}
40+
41+
@Override
42+
protected String getContentType() {
43+
return CONTENT_TYPE_FORM_DATA;
44+
}
45+
46+
@Override
47+
protected RequestBody createRequestBody() throws IOException {
48+
return bodyBuilder.build();
49+
}
50+
51+
@Override
52+
protected T readResponseBody(ResponseBody body) throws IOException {
53+
String payload = body.string();
54+
return mapper.readValue(payload, tType);
55+
}
56+
57+
public FormBodyRequest<T> addData(String name, String value) {
58+
assertNotNull(name, "name");
59+
assertNotNull(value, "value");
60+
bodyBuilder.add(name, value);
61+
return this;
62+
}
63+
}

src/test/java/com/auth0/client/MockServer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public class MockServer {
102102
public static final String MULTIPART_SAMPLE = "src/test/resources/mgmt/multipart_sample.json";
103103
public static final String PASSWORDLESS_EMAIL_RESPONSE = "src/test/resources/auth/passwordless_email.json";
104104
public static final String PASSWORDLESS_SMS_RESPONSE = "src/test/resources/auth/passwordless_sms.json";
105+
public static final String PUSHED_AUTHORIZATION_RESPONSE = "src/test/resources/auth/pushed_authorization_response.json";
105106
public static final String ORGANIZATION = "src/test/resources/mgmt/organization.json";
106107
public static final String ORGANIZATIONS_LIST = "src/test/resources/mgmt/organizations_list.json";
107108
public static final String ORGANIZATIONS_PAGED_LIST = "src/test/resources/mgmt/organizations_paged_list.json";

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,4 +1416,79 @@ public void shouldCreateExchangeMfaOtpRequest() throws Exception {
14161416
assertThat(response.getTokenType(), not(emptyOrNullString()));
14171417
assertThat(response.getExpiresIn(), is(notNullValue()));
14181418
}
1419+
1420+
@Test
1421+
public void authorizeUrlWithPARShouldThrowWhenRequestUriNull() {
1422+
exception.expect(IllegalArgumentException.class);
1423+
exception.expectMessage("'request uri' cannot be null!");
1424+
api.authorizeUrlWithPAR(null);
1425+
}
1426+
1427+
@Test
1428+
public void shouldBuildAuthorizeUrlWithPAR() {
1429+
AuthAPI api = new AuthAPI("domain.auth0.com", CLIENT_ID, CLIENT_SECRET);
1430+
String url = api.authorizeUrlWithPAR("urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2");
1431+
assertThat(url, is(notNullValue()));
1432+
assertThat(url, isUrl("https", "domain.auth0.com", "/authorize"));
1433+
1434+
assertThat(url, hasQueryParameter("request_uri", "urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2"));
1435+
assertThat(url, hasQueryParameter("client_id", CLIENT_ID));
1436+
}
1437+
1438+
@Test
1439+
public void pushedAuthorizationRequestShouldThrowWhenRedirectUriIsNull() {
1440+
exception.expect(IllegalArgumentException.class);
1441+
exception.expectMessage("'redirect uri' must be a valid URL!");
1442+
api.pushedAuthorizationRequest(null, "code", Collections.emptyMap());
1443+
}
1444+
1445+
@Test
1446+
public void pushedAuthorizationRequestShouldThrowWhenResponseTypeIsNull() {
1447+
exception.expect(IllegalArgumentException.class);
1448+
exception.expectMessage("'response type' cannot be null!");
1449+
api.pushedAuthorizationRequest("https://domain.com/callback", null, Collections.emptyMap());
1450+
}
1451+
1452+
@Test
1453+
public void shouldCreatePushedAuthorizationRequestWithNullAdditionalParams() throws Exception {
1454+
Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequest("https://domain.com/callback", "code", null);
1455+
assertThat(request, is(notNullValue()));
1456+
1457+
server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
1458+
PushedAuthorizationResponse response = request.execute();
1459+
RecordedRequest recordedRequest = server.takeRequest();
1460+
1461+
assertThat(recordedRequest, hasMethodAndPath("POST", "/oauth/par"));
1462+
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));
1463+
1464+
String expected = "client_id=" + CLIENT_ID + "&client_secret=" + CLIENT_SECRET + "&redirect_uri=https%3A%2F%2Fdomain.com%2Fcallback&response_type=code";
1465+
assertThat(expected, is(readFromRequest(recordedRequest)));
1466+
1467+
assertThat(response, is(notNullValue()));
1468+
assertThat(response.getRequestURI(), not(emptyOrNullString()));
1469+
assertThat(response.getExpiresIn(), notNullValue());
1470+
}
1471+
1472+
@Test
1473+
public void shouldCreatePushedAuthorizationRequestWithAdditionalParams() throws Exception {
1474+
Map<String, String> additionalParams = new HashMap<>();
1475+
additionalParams.put("audience", "aud");
1476+
additionalParams.put("connection", "conn");
1477+
Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequest("https://domain.com/callback", "code", additionalParams);
1478+
assertThat(request, is(notNullValue()));
1479+
1480+
server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
1481+
PushedAuthorizationResponse response = request.execute();
1482+
RecordedRequest recordedRequest = server.takeRequest();
1483+
1484+
assertThat(recordedRequest, hasMethodAndPath("POST", "/oauth/par"));
1485+
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));
1486+
1487+
String expected = "client_id=" + CLIENT_ID + "&client_secret=" + CLIENT_SECRET + "&redirect_uri=https%3A%2F%2Fdomain.com%2Fcallback&response_type=code&audience=aud&connection=conn";
1488+
assertThat(expected, is(readFromRequest(recordedRequest)));
1489+
1490+
assertThat(response, is(notNullValue()));
1491+
assertThat(response.getRequestURI(), not(emptyOrNullString()));
1492+
assertThat(response.getExpiresIn(), notNullValue());
1493+
}
14191494
}

src/test/java/com/auth0/json/mgmt/client/ClientTest.java

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,80 @@
1515
public class ClientTest extends JsonTest<Client> {
1616

1717
private static final String readOnlyJson = "{\"client_id\":\"clientId\",\"is_heroku_app\":true,\"signing_keys\":[{\"cert\":\"ce\",\"pkcs7\":\"pk\",\"subject\":\"su\"}]}";
18-
private static final String json = "{\"name\":\"name\",\"description\":\"description\",\"client_secret\":\"secret\",\"app_type\":\"type\",\"logo_uri\":\"uri\",\"oidc_conformant\":true,\"is_first_party\":true,\"initiate_login_uri\":\"https://myhome.com/login\",\"callbacks\":[\"value\"],\"allowed_origins\":[\"value\"],\"web_origins\":[\"value\"],\"grant_types\":[\"value\"],\"client_aliases\":[\"value\"],\"allowed_clients\":[\"value\"],\"allowed_logout_urls\":[\"value\"],\"organization_usage\":\"allow\",\"organization_require_behavior\":\"no_prompt\",\"jwt_configuration\":{\"lifetime_in_seconds\":100,\"scopes\":\"openid\",\"alg\":\"alg\"},\"encryption_key\":{\"pub\":\"pub\",\"cert\":\"cert\"},\"sso\":true,\"sso_disabled\":true,\"custom_login_page_on\":true,\"custom_login_page\":\"custom\",\"custom_login_page_preview\":\"preview\",\"form_template\":\"template\",\"addons\":{\"rms\":{},\"mscrm\":{},\"slack\":{},\"layer\":{}},\"token_endpoint_auth_method\":\"method\",\"client_metadata\":{\"key\":\"value\"},\"mobile\":{\"android\":{\"app_package_name\":\"pkg\",\"sha256_cert_fingerprints\":[\"256\"]},\"ios\":{\"team_id\":\"team\",\"app_bundle_identifier\":\"id\"}},\"refresh_token\":{\"rotation_type\":\"non-rotating\"}}";
18+
private static final String json = "{\n" +
19+
" \"name\": \"name\",\n" +
20+
" \"description\": \"description\",\n" +
21+
" \"client_secret\": \"secret\",\n" +
22+
" \"app_type\": \"type\",\n" +
23+
" \"logo_uri\": \"uri\",\n" +
24+
" \"oidc_conformant\": true,\n" +
25+
" \"is_first_party\": true,\n" +
26+
" \"initiate_login_uri\": \"https://myhome.com/login\",\n" +
27+
" \"callbacks\": [\n" +
28+
" \"value\"\n" +
29+
" ],\n" +
30+
" \"allowed_origins\": [\n" +
31+
" \"value\"\n" +
32+
" ],\n" +
33+
" \"web_origins\": [\n" +
34+
" \"value\"\n" +
35+
" ],\n" +
36+
" \"grant_types\": [\n" +
37+
" \"value\"\n" +
38+
" ],\n" +
39+
" \"client_aliases\": [\n" +
40+
" \"value\"\n" +
41+
" ],\n" +
42+
" \"allowed_clients\": [\n" +
43+
" \"value\"\n" +
44+
" ],\n" +
45+
" \"allowed_logout_urls\": [\n" +
46+
" \"value\"\n" +
47+
" ],\n" +
48+
" \"organization_usage\": \"allow\",\n" +
49+
" \"organization_require_behavior\": \"no_prompt\",\n" +
50+
" \"jwt_configuration\": {\n" +
51+
" \"lifetime_in_seconds\": 100,\n" +
52+
" \"scopes\": \"openid\",\n" +
53+
" \"alg\": \"alg\"\n" +
54+
" },\n" +
55+
" \"encryption_key\": {\n" +
56+
" \"pub\": \"pub\",\n" +
57+
" \"cert\": \"cert\"\n" +
58+
" },\n" +
59+
" \"sso\": true,\n" +
60+
" \"sso_disabled\": true,\n" +
61+
" \"custom_login_page_on\": true,\n" +
62+
" \"custom_login_page\": \"custom\",\n" +
63+
" \"custom_login_page_preview\": \"preview\",\n" +
64+
" \"form_template\": \"template\",\n" +
65+
" \"addons\": {\n" +
66+
" \"rms\": {},\n" +
67+
" \"mscrm\": {},\n" +
68+
" \"slack\": {},\n" +
69+
" \"layer\": {}\n" +
70+
" },\n" +
71+
" \"token_endpoint_auth_method\": \"method\",\n" +
72+
" \"client_metadata\": {\n" +
73+
" \"key\": \"value\"\n" +
74+
" },\n" +
75+
" \"mobile\": {\n" +
76+
" \"android\": {\n" +
77+
" \"app_package_name\": \"pkg\",\n" +
78+
" \"sha256_cert_fingerprints\": [\n" +
79+
" \"256\"\n" +
80+
" ]\n" +
81+
" },\n" +
82+
" \"ios\": {\n" +
83+
" \"team_id\": \"team\",\n" +
84+
" \"app_bundle_identifier\": \"id\"\n" +
85+
" }\n" +
86+
" },\n" +
87+
" \"refresh_token\": {\n" +
88+
" \"rotation_type\": \"non-rotating\"\n" +
89+
" },\n" +
90+
" \"require_pushed_authorization_requests\": true\n" +
91+
"}";
1992

2093
@Test
2194
public void shouldSerialize() throws Exception {
@@ -58,6 +131,7 @@ public void shouldSerialize() throws Exception {
58131
client.setRefreshToken(refreshToken);
59132
client.setOrganizationUsage("require");
60133
client.setOrganizationRequireBehavior("pre_login_prompt");
134+
client.setRequiresPushedAuthorizationRequests(true);
61135

62136
String serialized = toJSON(client);
63137
assertThat(serialized, is(notNullValue()));
@@ -91,6 +165,7 @@ public void shouldSerialize() throws Exception {
91165
assertThat(serialized, JsonMatcher.hasEntry("refresh_token", notNullValue()));
92166
assertThat(serialized, JsonMatcher.hasEntry("organization_usage", "require"));
93167
assertThat(serialized, JsonMatcher.hasEntry("organization_require_behavior", "pre_login_prompt"));
168+
assertThat(serialized, JsonMatcher.hasEntry("require_pushed_authorization_requests", true));
94169
}
95170

96171
@Test
@@ -134,6 +209,7 @@ public void shouldDeserialize() throws Exception {
134209
assertThat(client.getRefreshToken(), is(notNullValue()));
135210
assertThat(client.getOrganizationUsage(), is("allow"));
136211
assertThat(client.getOrganizationRequireBehavior(), is("no_prompt"));
212+
assertThat(client.getRequiresPushedAuthorizationRequests(), is(true));
137213
}
138214

139215
@Test

0 commit comments

Comments
 (0)