Skip to content

Commit cd96fc4

Browse files
authored
Support Pushed Authorize Requests (PAR) (#534)
1 parent 9eab0cc commit cd96fc4

14 files changed

Lines changed: 436 additions & 5 deletions

File tree

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.util.Collections;
1616
import java.util.List;
17+
import java.util.Map;
1718
import java.util.Objects;
1819

1920
/**
@@ -218,6 +219,56 @@ public AuthorizeUrlBuilder authorizeUrl(String redirectUri) {
218219
return AuthorizeUrlBuilder.newInstance(baseUrl, clientId, redirectUri);
219220
}
220221

222+
/**
223+
* Builds an authorization URL for Pushed Authorization Requests (PAR)
224+
* @param requestUri the {@code request_uri} parameter from a successful pushed authorization request.
225+
* @see AuthAPI#pushedAuthorizationRequest(String, String, Map)
226+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
227+
* @return the {@code request_uri} from a successful pushed authorization request.
228+
*/
229+
public String authorizeUrlWithPAR(String requestUri) {
230+
Asserts.assertNotNull(requestUri, "request uri");
231+
return baseUrl
232+
.newBuilder()
233+
.addPathSegment("authorize")
234+
.addQueryParameter("client_id", clientId)
235+
.addQueryParameter("request_uri", requestUri)
236+
.build()
237+
.toString();
238+
}
239+
240+
/**
241+
* Builds a request to make a Pushed Authorization Request (PAR) to receive a {@code request_uri} to send to the {@code /authorize} endpoint.
242+
* @param redirectUri the URL to redirect to after authorization has been granted by the user. Your Auth0 application
243+
* must have this URL as one of its Allowed Callback URLs. Must be a valid non-encoded URL.
244+
* @param responseType the response type to set. Must not be null.
245+
* @param params an optional map of key/value pairs representing any additional parameters to send on the request.
246+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
247+
* @return a request to execute.
248+
*/
249+
public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String redirectUri, String responseType, Map<String, String> params) {
250+
Asserts.assertValidUrl(redirectUri, "redirect uri");
251+
Asserts.assertNotNull(responseType, "response type");
252+
253+
String url = baseUrl
254+
.newBuilder()
255+
.addPathSegments("oauth/par")
256+
.build()
257+
.toString();
258+
259+
FormBodyRequest<PushedAuthorizationResponse> request = new FormBodyRequest<>(client, null, url, HttpMethod.POST, new TypeReference<PushedAuthorizationResponse>() {});
260+
request.addParameter("client_id", clientId);
261+
request.addParameter("redirect_uri", redirectUri);
262+
request.addParameter("response_type", responseType);
263+
if (Objects.nonNull(this.clientSecret)) {
264+
request.addParameter("client_secret", clientSecret);
265+
}
266+
if (params != null) {
267+
params.forEach(request::addParameter);
268+
}
269+
return request;
270+
}
271+
221272
/**
222273
* Creates an instance of the {@link LogoutUrlBuilder} with the given return-to url.
223274
* i.e.:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/**
9+
* Represents the response from a Pushed Authorization Request (PAR).
10+
*/
11+
@JsonIgnoreProperties(ignoreUnknown = true)
12+
@JsonInclude(JsonInclude.Include.NON_NULL)
13+
public class PushedAuthorizationResponse {
14+
15+
@JsonProperty("request_uri")
16+
private String requestURI;
17+
@JsonProperty("expires_in")
18+
private Integer expiresIn;
19+
20+
@JsonCreator
21+
public PushedAuthorizationResponse(@JsonProperty("request_uri") String requestURI, @JsonProperty("expires_in") Integer expiresIn) {
22+
this.requestURI = requestURI;
23+
this.expiresIn = expiresIn;
24+
}
25+
26+
public String getRequestURI() {
27+
return requestURI;
28+
}
29+
30+
public Integer getExpiresIn() {
31+
return expiresIn;
32+
}
33+
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public class Client {
9292
private String crossOriginLoc;
9393
@JsonProperty("client_authentication_methods")
9494
private ClientAuthenticationMethods clientAuthenticationMethods;
95+
@JsonProperty("require_pushed_authorization_requests")
96+
private Boolean requiresPushedAuthorizationRequests;
9597

9698
/**
9799
* Getter for the name of the tenant this client belongs to.
@@ -803,5 +805,20 @@ public void setClientAuthenticationMethods(ClientAuthenticationMethods clientAut
803805
public ClientAuthenticationMethods getClientAuthenticationMethods() {
804806
return clientAuthenticationMethods;
805807
}
808+
809+
/**
810+
* @return whether this client requires pushed authorization requests or not.
811+
*/
812+
public Boolean getRequiresPushedAuthorizationRequests() {
813+
return requiresPushedAuthorizationRequests;
814+
}
815+
816+
/**
817+
* Sets whether the client requires pushed authorization requests or not.
818+
* @param requiresPushedAuthorizationRequests true if the client should require pushed authorization requests, false if not.
819+
*/
820+
public void setRequiresPushedAuthorizationRequests(Boolean requiresPushedAuthorizationRequests) {
821+
this.requiresPushedAuthorizationRequests = requiresPushedAuthorizationRequests;
822+
}
806823
}
807824

src/main/java/com/auth0/net/BaseRequest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ protected T readResponseBody(Auth0HttpResponse response) throws IOException {
118118
return mapper.readValue(payload, tType);
119119
}
120120

121+
protected Map<String, Object> getParameters() {
122+
return this.parameters;
123+
}
121124
/**
122125
* Executes this request.
123126
*
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.auth0.net;
2+
3+
import com.auth0.client.mgmt.TokenProvider;
4+
import com.auth0.json.ObjectMapperProvider;
5+
import com.auth0.json.auth.PushedAuthorizationResponse;
6+
import com.auth0.net.client.*;
7+
import com.auth0.utils.Asserts;
8+
import com.fasterxml.jackson.core.type.TypeReference;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import okhttp3.FormBody;
11+
import okhttp3.RequestBody;
12+
13+
import java.io.IOException;
14+
15+
/**
16+
* Represents a form request.
17+
* @param <T> The type expected to be received as part of the response.
18+
*/
19+
public class FormBodyRequest<T> extends BaseRequest<T> {
20+
21+
private static final String CONTENT_TYPE_FORM_DATA = "application/x-www-form-urlencoded";
22+
23+
FormBodyRequest(Auth0HttpClient client, TokenProvider tokenProvider, String url, HttpMethod method, ObjectMapper mapper, TypeReference<T> tType) {
24+
super(client, tokenProvider, url, method, mapper, tType);
25+
}
26+
27+
public FormBodyRequest(Auth0HttpClient client, TokenProvider tokenProvider, String url, HttpMethod method, TypeReference<T> tType) {
28+
this(client, tokenProvider, url, method, ObjectMapperProvider.getMapper(), tType);
29+
}
30+
31+
@Override
32+
protected HttpRequestBody createRequestBody() throws IOException {
33+
return HttpRequestBody.create(CONTENT_TYPE_FORM_DATA, new Auth0FormRequestBody(super.getParameters()));
34+
}
35+
36+
@Override
37+
protected String getContentType() {
38+
return CONTENT_TYPE_FORM_DATA;
39+
}
40+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.auth0.net.client;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* Represents the body of a application/x-www-form-urlencoded request
7+
*/
8+
public class Auth0FormRequestBody {
9+
10+
private final Map<String, Object> params;
11+
12+
public Auth0FormRequestBody(Map<String, Object> params) {
13+
this.params = params;
14+
}
15+
16+
public Map<String, Object> getParams() {
17+
return params;
18+
}
19+
}

src/main/java/com/auth0/net/client/DefaultHttpClient.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,16 @@ private RequestBody addBody(Auth0HttpRequest request) {
161161
HttpRequestBody body = request.getBody();
162162
RequestBody okBody;
163163

164-
if (Objects.nonNull(body.getMultipartRequestBody())) {
164+
if (Objects.nonNull(body.getFormRequestBody())) {
165+
Auth0FormRequestBody formData = body.getFormRequestBody();
166+
FormBody.Builder builder = new FormBody.Builder();
167+
for (Map.Entry<String, Object> entry : formData.getParams().entrySet()) {
168+
Object val = entry.getValue();
169+
builder.add(entry.getKey(), val instanceof String ? (String) val : val.toString());
170+
}
171+
okBody = builder.build();
172+
}
173+
else if (Objects.nonNull(body.getMultipartRequestBody())) {
165174
Auth0MultipartRequestBody multipartRequestBody = body.getMultipartRequestBody();
166175
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()
167176
.setType(MultipartBody.FORM);

src/main/java/com/auth0/net/client/HttpRequestBody.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public class HttpRequestBody {
55
private byte[] content;
66
private String contentType;
77
private Auth0MultipartRequestBody multipartRequestBody;
8+
private Auth0FormRequestBody formRequestBody;
89

910
public static HttpRequestBody create(String contentType, byte[] content) {
1011
return new HttpRequestBody(contentType, content);
@@ -14,6 +15,10 @@ public static HttpRequestBody create(String contentType, Auth0MultipartRequestBo
1415
return new HttpRequestBody(contentType, multipartRequestBody);
1516
}
1617

18+
public static HttpRequestBody create(String contentType, Auth0FormRequestBody formRequestBody) {
19+
return new HttpRequestBody(contentType, formRequestBody);
20+
}
21+
1722
public byte[] getContent() {
1823
return this.content;
1924
}
@@ -22,6 +27,10 @@ public Auth0MultipartRequestBody getMultipartRequestBody() {
2227
return this.multipartRequestBody;
2328
}
2429

30+
public Auth0FormRequestBody getFormRequestBody() {
31+
return this.formRequestBody;
32+
}
33+
2534
public String getContentType() {
2635
return this.contentType;
2736
}
@@ -35,4 +44,9 @@ private HttpRequestBody(String contentType, Auth0MultipartRequestBody multipartR
3544
this.contentType = contentType;
3645
this.multipartRequestBody = multipartRequestBody;
3746
}
47+
48+
private HttpRequestBody(String contentType, Auth0FormRequestBody formRequestBody) {
49+
this.contentType = contentType;
50+
this.formRequestBody = formRequestBody;
51+
}
3852
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ public class MockServer {
108108
public static final String MULTIPART_SAMPLE = "src/test/resources/mgmt/multipart_sample.json";
109109
public static final String PASSWORDLESS_EMAIL_RESPONSE = "src/test/resources/auth/passwordless_email.json";
110110
public static final String PASSWORDLESS_SMS_RESPONSE = "src/test/resources/auth/passwordless_sms.json";
111+
public static final String PUSHED_AUTHORIZATION_RESPONSE = "src/test/resources/auth/pushed_authorization_response.json";
111112
public static final String AUTHENTICATOR_METHOD_BY_ID = "src/test/resources/mgmt/authenticator_method_by_id.json";
112113
public static final String AUTHENTICATOR_METHOD_CREATE = "src/test/resources/mgmt/authenticator_method_create.json";
113114
public static final String AUTHENTICATOR_METHOD_LIST = "src/test/resources/mgmt/authenticator_method_list.json";

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,113 @@ public void shouldNotAddAnyParamsIfNoSecretOrAssertion() throws Exception {
15801580
assertThat(response.getExpiresIn(), is(notNullValue()));
15811581
}
15821582

1583+
@Test
1584+
public void authorizeUrlWithPARShouldThrowWhenRequestUriNull() {
1585+
exception.expect(IllegalArgumentException.class);
1586+
exception.expectMessage("'request uri' cannot be null!");
1587+
api.authorizeUrlWithPAR(null);
1588+
}
1589+
1590+
@Test
1591+
public void shouldBuildAuthorizeUrlWithPAR() {
1592+
AuthAPI api = AuthAPI.newBuilder("domain.auth0.com", CLIENT_ID, CLIENT_SECRET).build();
1593+
String url = api.authorizeUrlWithPAR("urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2");
1594+
assertThat(url, is(notNullValue()));
1595+
assertThat(url, isUrl("https", "domain.auth0.com", "/authorize"));
1596+
1597+
assertThat(url, hasQueryParameter("request_uri", "urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2"));
1598+
assertThat(url, hasQueryParameter("client_id", CLIENT_ID));
1599+
}
1600+
1601+
@Test
1602+
public void pushedAuthorizationRequestShouldThrowWhenRedirectUriIsNull() {
1603+
exception.expect(IllegalArgumentException.class);
1604+
exception.expectMessage("'redirect uri' must be a valid URL!");
1605+
api.pushedAuthorizationRequest(null, "code", Collections.emptyMap());
1606+
}
1607+
1608+
@Test
1609+
public void pushedAuthorizationRequestShouldThrowWhenResponseTypeIsNull() {
1610+
exception.expect(IllegalArgumentException.class);
1611+
exception.expectMessage("'response type' cannot be null!");
1612+
api.pushedAuthorizationRequest("https://domain.com/callback", null, Collections.emptyMap());
1613+
}
1614+
1615+
@Test
1616+
public void shouldCreatePushedAuthorizationRequestWithNullAdditionalParams() throws Exception {
1617+
Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequest("https://domain.com/callback", "code", null);
1618+
assertThat(request, is(notNullValue()));
1619+
1620+
server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
1621+
PushedAuthorizationResponse response = request.execute().getBody();
1622+
RecordedRequest recordedRequest = server.takeRequest();
1623+
1624+
assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/par"));
1625+
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));
1626+
1627+
String body = readFromRequest(recordedRequest);
1628+
assertThat(body, containsString("client_id=" + CLIENT_ID));
1629+
assertThat(body, containsString("redirect_uri=" + "https%3A%2F%2Fdomain.com%2Fcallback"));
1630+
assertThat(body, containsString("response_type=" + "code"));
1631+
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));
1632+
1633+
assertThat(response, is(notNullValue()));
1634+
assertThat(response.getRequestURI(), not(emptyOrNullString()));
1635+
assertThat(response.getExpiresIn(), notNullValue());
1636+
}
1637+
1638+
@Test
1639+
public void shouldCreatePushedAuthorizationRequestWithAdditionalParams() throws Exception {
1640+
Map<String, String> additionalParams = new HashMap<>();
1641+
additionalParams.put("audience", "aud");
1642+
additionalParams.put("connection", "conn");
1643+
Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequest("https://domain.com/callback", "code", additionalParams);
1644+
assertThat(request, is(notNullValue()));
1645+
1646+
server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
1647+
PushedAuthorizationResponse response = request.execute().getBody();
1648+
RecordedRequest recordedRequest = server.takeRequest();
1649+
1650+
assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/par"));
1651+
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));
1652+
1653+
String body = readFromRequest(recordedRequest);
1654+
assertThat(body, containsString("client_id=" + CLIENT_ID));
1655+
assertThat(body, containsString("redirect_uri=" + "https%3A%2F%2Fdomain.com%2Fcallback"));
1656+
assertThat(body, containsString("response_type=" + "code"));
1657+
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));
1658+
assertThat(body, containsString("audience=" + "aud"));
1659+
assertThat(body, containsString("connection=" + "conn"));
1660+
1661+
assertThat(response, is(notNullValue()));
1662+
assertThat(response.getRequestURI(), not(emptyOrNullString()));
1663+
assertThat(response.getExpiresIn(), notNullValue());
1664+
}
1665+
1666+
@Test
1667+
public void shouldCreatePushedAuthorizationRequestWithoutSecret() throws Exception {
1668+
AuthAPI api = AuthAPI.newBuilder(server.getBaseUrl(), CLIENT_ID).build();
1669+
Request<PushedAuthorizationResponse> request = api.pushedAuthorizationRequest("https://domain.com/callback", "code", null);
1670+
assertThat(request, is(notNullValue()));
1671+
1672+
server.jsonResponse(PUSHED_AUTHORIZATION_RESPONSE, 200);
1673+
PushedAuthorizationResponse response = request.execute().getBody();
1674+
RecordedRequest recordedRequest = server.takeRequest();
1675+
1676+
assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/par"));
1677+
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));
1678+
1679+
String body = readFromRequest(recordedRequest);
1680+
assertThat(body, containsString("client_id=" + CLIENT_ID));
1681+
assertThat(body, containsString("redirect_uri=" + "https%3A%2F%2Fdomain.com%2Fcallback"));
1682+
assertThat(body, containsString("response_type=" + "code"));
1683+
assertThat(body, not(containsString("client_secret")));
1684+
1685+
assertThat(response, is(notNullValue()));
1686+
assertThat(response.getRequestURI(), not(emptyOrNullString()));
1687+
assertThat(response.getExpiresIn(), notNullValue());
1688+
}
1689+
15831690
static class TestAssertionSigner implements ClientAssertionSigner {
15841691

15851692
private final String token;

0 commit comments

Comments
 (0)