Skip to content

Commit

Permalink
[SDK-4133] Support Pushed Authorization Requests (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmyjames committed May 2, 2023
1 parent 864dcf6 commit a355498
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 22 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ dependencies {
implementation 'com.google.guava:guava-annotations:r03'
implementation 'commons-codec:commons-codec:1.15'

api 'com.auth0:auth0:1.44.2'
api 'com.auth0:auth0:1.45.0'
api 'com.auth0:java-jwt:3.19.4'
api 'com.auth0:jwks-rsa:0.21.3'

Expand Down
74 changes: 55 additions & 19 deletions src/main/java/com/auth0/AuthorizeUrl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import com.auth0.client.auth.AuthAPI;
import com.auth0.client.auth.AuthorizeUrlBuilder;
import com.auth0.exception.Auth0Exception;
import com.auth0.json.auth.PushedAuthorizationResponse;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;

import static com.auth0.IdentityVerificationException.API_ERROR;

/**
* Class to create and customize an Auth0 Authorize URL.
Expand All @@ -20,14 +22,15 @@ public class AuthorizeUrl {

private HttpServletResponse response;
private HttpServletRequest request;
private final AuthorizeUrlBuilder builder;
private final String responseType;
private boolean useLegacySameSiteCookie = true;
private boolean setSecureCookie = false;
private String nonce;
private String state;

private final AuthAPI authAPI;
private boolean used;
private Map<String, String> params;
private final String redirectUri;

/**
* Creates a new instance that can be used to build an Auth0 Authorization URL.
Expand All @@ -40,16 +43,17 @@ public class AuthorizeUrl {
* @param client the Auth0 Authentication API client
* @parem request the HTTP request. Used to store state and nonce as a fallback if cookies not set.
* @param response the response where the state and nonce will be stored as cookies
* @param redirectUrl the url to redirect to after authentication
* @param redirectUri the url to redirect to after authentication
* @param responseType the response type to use
*/
AuthorizeUrl(AuthAPI client, HttpServletRequest request, HttpServletResponse response, String redirectUrl, String responseType) {
AuthorizeUrl(AuthAPI client, HttpServletRequest request, HttpServletResponse response, String redirectUri, String responseType) {
this.request = request;
this.response = response;
this.responseType = responseType;
this.builder = client.authorizeUrl(redirectUrl)
.withResponseType(responseType)
.withScope(SCOPE_OPENID);
this.authAPI = client;
this.redirectUri = redirectUri;
this.params = new HashMap<>();
this.params.put("scope", SCOPE_OPENID);
}

/**
Expand All @@ -59,7 +63,7 @@ public class AuthorizeUrl {
* @return the builder instance.
*/
public AuthorizeUrl withOrganization(String organization) {
builder.withOrganization(organization);
params.put("organization", organization);
return this;
}

Expand All @@ -71,7 +75,7 @@ public AuthorizeUrl withOrganization(String organization) {
* @return the builder instance.
*/
public AuthorizeUrl withInvitation(String invitation) {
builder.withInvitation(invitation);
params.put("invitation", invitation);
return this;
}

Expand All @@ -82,7 +86,7 @@ public AuthorizeUrl withInvitation(String invitation) {
* @return the builder instance
*/
public AuthorizeUrl withConnection(String connection) {
builder.withConnection(connection);
params.put("connection", connection);
return this;
}

Expand Down Expand Up @@ -122,7 +126,7 @@ AuthorizeUrl withLegacySameSiteCookie(boolean useLegacySameSiteCookie) {
* @return the builder instance
*/
public AuthorizeUrl withAudience(String audience) {
builder.withAudience(audience);
params.put("audience", audience);
return this;
}

Expand All @@ -134,7 +138,7 @@ public AuthorizeUrl withAudience(String audience) {
*/
public AuthorizeUrl withState(String state) {
this.state = state;
builder.withState(state);
params.put("state", state);
return this;
}

Expand All @@ -146,7 +150,7 @@ public AuthorizeUrl withState(String state) {
*/
public AuthorizeUrl withNonce(String nonce) {
this.nonce = nonce;
builder.withParameter("nonce", nonce);
params.put("nonce", nonce);
return this;
}

Expand All @@ -157,7 +161,7 @@ public AuthorizeUrl withNonce(String nonce) {
* @return the builder instance
*/
public AuthorizeUrl withScope(String scope) {
builder.withScope(scope);
params.put("scope", scope);
return this;
}

Expand All @@ -178,7 +182,7 @@ public AuthorizeUrl withParameter(String name, String value) {
if ("redirect_uri".equals(name)) {
throw new IllegalArgumentException("Redirect URI cannot be changed once set.");
}
builder.withParameter(name, value);
params.put(name, value);
return this;
}

Expand All @@ -190,6 +194,39 @@ public AuthorizeUrl withParameter(String name, String value) {
* @throws IllegalStateException if it's called more than once
*/
public String build() throws IllegalStateException {
storeTransient();
AuthorizeUrlBuilder builder = authAPI.authorizeUrl(redirectUri).withResponseType(responseType);
params.forEach(builder::withParameter);
return builder.build();
}

/**
* Executes a Pushed Authorization Request (PAR) and uses the {@code request_uri} to
* construct the authorize URL.
*
* @return the authorize URL as a string.
* @throws InvalidRequestException if there is an error when making the request.
* @see <a href="https://www.rfc-editor.org/rfc/rfc9126.html">RFC 9126</a>
*/
public String fromPushedAuthorizationRequest() throws InvalidRequestException {
storeTransient();

try {
PushedAuthorizationResponse pushedAuthResponse = authAPI.pushedAuthorizationRequest(redirectUri, responseType, params).execute();
String requestUri = pushedAuthResponse.getRequestURI();
if (requestUri == null || requestUri.isEmpty()) {
throw new InvalidRequestException(API_ERROR, "The PAR request returned a missing or empty request_uri value");
}
if (pushedAuthResponse.getExpiresIn() == null) {
throw new InvalidRequestException(API_ERROR, "The PAR request returned a missing expires_in value");
}
return authAPI.authorizeUrlWithPAR(pushedAuthResponse.getRequestURI());
} catch (Auth0Exception e) {
throw new InvalidRequestException(API_ERROR, e.getMessage(), e);
}
}

private void storeTransient() {
if (used) {
throw new IllegalStateException("The AuthorizeUrl instance must not be reused.");
}
Expand All @@ -207,7 +244,6 @@ public String build() throws IllegalStateException {
RandomStorage.setSessionNonce(request, nonce);

used = true;
return builder.build();
}

private boolean containsFormPost() {
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/auth0/InvalidRequestException.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ public class InvalidRequestException extends IdentityVerificationException {
static final String DEFAULT_DESCRIPTION = "The request contains an error";

InvalidRequestException(String code, String description) {
super(code, description != null ? description : DEFAULT_DESCRIPTION, null);
this(code, description, null);
}

InvalidRequestException(String code, String description, Throwable cause) {
super(code, description != null ? description : DEFAULT_DESCRIPTION, cause);
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/main/java/com/auth0/RequestProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.auth0.client.auth.AuthAPI;
import com.auth0.exception.Auth0Exception;
import com.auth0.json.auth.TokenHolder;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.Validate;

import javax.servlet.http.HttpServletRequest;
Expand Down
107 changes: 107 additions & 0 deletions src/test/java/com/auth0/AuthorizeUrlTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.auth0;

import com.auth0.client.HttpOptions;
import com.auth0.client.auth.AuthAPI;
import com.auth0.exception.Auth0Exception;
import com.auth0.json.auth.PushedAuthorizationResponse;
import com.auth0.net.Request;
import okhttp3.HttpUrl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -10,11 +14,15 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Map;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class AuthorizeUrlTest {

Expand Down Expand Up @@ -235,4 +243,103 @@ public void shouldThrowWhenChangingTheNonceUsingCustomParameterSetter() {
.withParameter("nonce", "new_value"));
assertEquals("Please, use the dedicated methods for setting the 'nonce' and 'state' parameters.", e.getMessage());
}

@Test
public void shouldGetAuthorizeUrlFromPAR() throws Exception {
AuthAPIStub authAPIStub = new AuthAPIStub("https://domain.com", "clientId", "clientSecret");
Request requestMock = mock(Request.class);

when(requestMock.execute()).thenReturn(new PushedAuthorizationResponse("urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2", 90));

authAPIStub.pushedAuthorizationResponseRequest = requestMock;
String url = new AuthorizeUrl(authAPIStub, request, response, "https://domain.com/callback", "code")
.fromPushedAuthorizationRequest();

assertThat(url, is("https://domain.com/authorize?client_id=clientId&request_uri=urn%3Aexample%3Abwc4JK-ESC0w8acc191e-Y1LTC2"));
}

@Test
public void fromPushedAuthorizationRequestThrowsWhenRequestUriIsNull() throws Exception {
AuthAPIStub authAPIStub = new AuthAPIStub("https://domain.com", "clientId", "clientSecret");
Request requestMock = mock(Request.class);
when(requestMock.execute()).thenReturn(new PushedAuthorizationResponse(null, 90));

authAPIStub.pushedAuthorizationResponseRequest = requestMock;

InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
new AuthorizeUrl(authAPIStub, request, response, "https://domain.com/callback", "code")
.fromPushedAuthorizationRequest();
});

assertThat(exception.getMessage(), is("The PAR request returned a missing or empty request_uri value"));
}

@Test
public void fromPushedAuthorizationRequestThrowsWhenRequestUriIsEmpty() throws Exception {
AuthAPIStub authAPIStub = new AuthAPIStub("https://domain.com", "clientId", "clientSecret");
Request requestMock = mock(Request.class);
when(requestMock.execute()).thenReturn(new PushedAuthorizationResponse("urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2", null));

authAPIStub.pushedAuthorizationResponseRequest = requestMock;

InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
new AuthorizeUrl(authAPIStub, request, response, "https://domain.com/callback", "code")
.fromPushedAuthorizationRequest();
});

assertThat(exception.getMessage(), is("The PAR request returned a missing expires_in value"));
}

@Test
public void fromPushedAuthorizationRequestThrowsWhenExpiresInIsNull() throws Exception {
AuthAPIStub authAPIStub = new AuthAPIStub("https://domain.com", "clientId", "clientSecret");
Request requestMock = mock(Request.class);
when(requestMock.execute()).thenReturn(new PushedAuthorizationResponse(null, 90));

authAPIStub.pushedAuthorizationResponseRequest = requestMock;

InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
new AuthorizeUrl(authAPIStub, request, response, "https://domain.com/callback", "code")
.fromPushedAuthorizationRequest();
});

assertThat(exception.getMessage(), is("The PAR request returned a missing or empty request_uri value"));
}

@Test
public void fromPushedAuthorizationRequestThrowsWhenRequestThrows() throws Exception {
AuthAPI authAPIMock = mock(AuthAPI.class);
Request requestMock = mock(Request.class);

when(requestMock.execute())
.thenThrow(new Auth0Exception("error"));
when(authAPIMock.pushedAuthorizationRequest(eq("https://domain.com/callback"), eq("code"), anyMap()))
.thenReturn(requestMock);

InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
new AuthorizeUrl(authAPIMock, request, response, "https://domain.com/callback", "code")
.fromPushedAuthorizationRequest();
});

assertThat(exception.getMessage(), is("error"));
assertThat(exception.getCause(), instanceOf(Auth0Exception.class));
}

static class AuthAPIStub extends AuthAPI {

Request<PushedAuthorizationResponse> pushedAuthorizationResponseRequest;

public AuthAPIStub(String domain, String clientId, String clientSecret, HttpOptions options) {
super(domain, clientId, clientSecret, options);
}

public AuthAPIStub(String domain, String clientId, String clientSecret) {
super(domain, clientId, clientSecret);
}

@Override
public Request<PushedAuthorizationResponse> pushedAuthorizationRequest(String redirectUri, String responseType, Map<String, String> params) {
return pushedAuthorizationResponseRequest;
}
}
}

0 comments on commit a355498

Please sign in to comment.