Skip to content

Commit

Permalink
[SDK-4146] Support configurable cookie path (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmyjames committed May 10, 2023
1 parent a355498 commit 4a8a07c
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 41 deletions.
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pluginManagement {
gradlePluginPortal()
}
plugins {
id 'com.auth0.gradle.oss-library.java' version '0.17.2'
id 'com.auth0.gradle.oss-library.java' version '0.18.0'
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/auth0/AuthCookie.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class AuthCookie {
private final String value;
private boolean secure;
private SameSite sameSite;
private String cookiePath;

/**
* Create a new instance.
Expand All @@ -37,6 +38,10 @@ class AuthCookie {
this.value = value;
}

void setPath(String path) {
this.cookiePath = path;
}

/**
* Sets whether the Secure attribute should be set on the cookie. False by default.
*
Expand Down Expand Up @@ -64,6 +69,9 @@ void setSameSite(SameSite sameSite) {
*/
String buildHeaderString() {
String baseCookieString = String.format("%s=%s; HttpOnly; Max-Age=%d", encode(key), encode(value), MAX_AGE_SECONDS);
if (cookiePath != null) {
baseCookieString = baseCookieString.concat(String.format("; Path=%s", cookiePath));
}
if (sameSite != null) {
baseCookieString = baseCookieString.concat(String.format("; SameSite=%s", encode(sameSite.getValue())));
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/auth0/AuthenticationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public static class Builder {
private String organization;
private String invitation;
private HttpOptions httpOptions;
private String cookiePath;

Builder(String domain, String clientId, String clientSecret) {
Validate.notNull(domain);
Expand All @@ -87,6 +88,19 @@ public Builder withHttpOptions(HttpOptions httpOptions) {
return this;
}

/**
* Specify that transient authentication-based cookies such as state and nonce are created with the specified
* {@code Path} cookie attribute.
*
* @param cookiePath the path to set on the cookie.
* @return this builder instance.
*/
public Builder withCookiePath(String cookiePath) {
Validate.notNull(cookiePath);
this.cookiePath = cookiePath;
return this;
}

/**
* Change the response type to request in the Authorization step. Default value is 'code'.
*
Expand Down Expand Up @@ -208,6 +222,7 @@ public AuthenticationController build() throws UnsupportedOperationException {
.withLegacySameSiteCookie(useLegacySameSiteCookie)
.withOrganization(organization)
.withInvitation(invitation)
.withCookiePath(cookiePath)
.build();

return new AuthenticationController(processor);
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/com/auth0/AuthorizeUrl.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class AuthorizeUrl {
private String nonce;
private String state;
private final AuthAPI authAPI;
private String cookiePath;

private boolean used;
private Map<String, String> params;
private final String redirectUri;
Expand Down Expand Up @@ -130,6 +132,16 @@ public AuthorizeUrl withAudience(String audience) {
return this;
}

/**
* Sets the value of the Path cookie attribute
* @param cookiePath the cookie path to set
* @return
*/
AuthorizeUrl withCookiePath(String cookiePath) {
this.cookiePath = cookiePath;
return this;
}

/**
* Sets the state value.
*
Expand Down Expand Up @@ -234,8 +246,8 @@ private void storeTransient() {
if (response != null) {
SameSite sameSiteValue = containsFormPost() ? SameSite.NONE : SameSite.LAX;

TransientCookieStore.storeState(response, state, sameSiteValue, useLegacySameSiteCookie, setSecureCookie);
TransientCookieStore.storeNonce(response, nonce, sameSiteValue, useLegacySameSiteCookie, setSecureCookie);
TransientCookieStore.storeState(response, state, sameSiteValue, useLegacySameSiteCookie, setSecureCookie, cookiePath);
TransientCookieStore.storeNonce(response, nonce, sameSiteValue, useLegacySameSiteCookie, setSecureCookie, cookiePath);
}

// Also store in Session just in case developer uses deprecated
Expand Down
20 changes: 16 additions & 4 deletions src/main/java/com/auth0/RequestProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class RequestProcessor {
private final IdTokenVerifier tokenVerifier;
private final String organization;
private final String invitation;
private final String cookiePath;


static class Builder {
Expand All @@ -50,6 +51,7 @@ static class Builder {
private IdTokenVerifier tokenVerifier;
private String organization;
private String invitation;
private String cookiePath;

Builder(AuthAPI client, String responseType, IdTokenVerifier.Options verifyOptions) {
Validate.notNull(client);
Expand All @@ -60,6 +62,11 @@ static class Builder {
this.verifyOptions = verifyOptions;
}

Builder withCookiePath(String cookiePath) {
this.cookiePath = cookiePath;
return this;
}

Builder withLegacySameSiteCookie(boolean useLegacySameSiteCookie) {
this.useLegacySameSiteCookie = useLegacySameSiteCookie;
return this;
Expand All @@ -83,11 +90,11 @@ Builder withInvitation(String invitation) {
RequestProcessor build() {
return new RequestProcessor(client, responseType, verifyOptions,
this.tokenVerifier == null ? new IdTokenVerifier() : this.tokenVerifier,
useLegacySameSiteCookie, organization, invitation);
useLegacySameSiteCookie, organization, invitation, cookiePath);
}
}

private RequestProcessor(AuthAPI client, String responseType, IdTokenVerifier.Options verifyOptions, IdTokenVerifier tokenVerifier, boolean useLegacySameSiteCookie, String organization, String invitation) {
private RequestProcessor(AuthAPI client, String responseType, IdTokenVerifier.Options verifyOptions, IdTokenVerifier tokenVerifier, boolean useLegacySameSiteCookie, String organization, String invitation, String cookiePath) {
Validate.notNull(client);
Validate.notNull(responseType);
Validate.notNull(verifyOptions);
Expand All @@ -98,6 +105,7 @@ private RequestProcessor(AuthAPI client, String responseType, IdTokenVerifier.Op
this.useLegacySameSiteCookie = useLegacySameSiteCookie;
this.organization = organization;
this.invitation = invitation;
this.cookiePath = cookiePath;
}

/**
Expand Down Expand Up @@ -132,12 +140,16 @@ AuthorizeUrl buildAuthorizeUrl(HttpServletRequest request, HttpServletResponse r
if (this.invitation != null) {
creator.withInvitation(invitation);
}
if (this.cookiePath != null) {
creator.withCookiePath(this.cookiePath);
}

// null response means state and nonce will be stored in session, so legacy cookie flag does not apply
if (response != null) {
creator.withLegacySameSiteCookie(useLegacySameSiteCookie);
}


return getAuthorizeUrl(nonce, creator);
}

Expand Down Expand Up @@ -169,7 +181,7 @@ Tokens process(HttpServletRequest request, HttpServletResponse response) throws
String nonce;
if (response != null) {
// Nonce dynamically set and changes on every request.
nonce = TransientCookieStore.getNonce(request, response, useLegacySameSiteCookie);
nonce = TransientCookieStore.getNonce(request, response);

// Just in case the developer created the authorizeUrl that stores state/nonce in the session
if (nonce == null) {
Expand Down Expand Up @@ -289,7 +301,7 @@ private void assertValidState(HttpServletRequest request, HttpServletResponse re
return;
}

String cookieState = TransientCookieStore.getState(request, response, useLegacySameSiteCookie);
String cookieState = TransientCookieStore.getState(request, response);

// Just in case state was stored in Session by building auth URL with deprecated method, but then called the
// supported handle method with the request and response
Expand Down
25 changes: 13 additions & 12 deletions src/main/java/com/auth0/TransientCookieStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ private TransientCookieStore() {}
* @param useLegacySameSiteCookie whether to set a fallback cookie or not
* @param isSecureCookie whether to always set the Secure cookie attribute or not
*/
static void storeState(HttpServletResponse response, String state, SameSite sameSite, boolean useLegacySameSiteCookie, boolean isSecureCookie) {
store(response, StorageUtils.STATE_KEY, state, sameSite, useLegacySameSiteCookie, isSecureCookie);
static void storeState(HttpServletResponse response, String state, SameSite sameSite, boolean useLegacySameSiteCookie, boolean isSecureCookie, String cookiePath) {
store(response, StorageUtils.STATE_KEY, state, sameSite, useLegacySameSiteCookie, isSecureCookie, cookiePath);
}

/**
Expand All @@ -40,35 +40,33 @@ static void storeState(HttpServletResponse response, String state, SameSite same
* @param useLegacySameSiteCookie whether to set a fallback cookie or not
* @param isSecureCookie whether to always set the Secure cookie attribute or not
*/
static void storeNonce(HttpServletResponse response, String nonce, SameSite sameSite, boolean useLegacySameSiteCookie, boolean isSecureCookie) {
store(response, StorageUtils.NONCE_KEY, nonce, sameSite, useLegacySameSiteCookie, isSecureCookie);
static void storeNonce(HttpServletResponse response, String nonce, SameSite sameSite, boolean useLegacySameSiteCookie, boolean isSecureCookie, String cookiePath) {
store(response, StorageUtils.NONCE_KEY, nonce, sameSite, useLegacySameSiteCookie, isSecureCookie, cookiePath);
}

/**
* Gets the value associated with the state cookie and removes it.
*
* @param request the request object
* @param response the response object
* @param useLegacySameSiteCookie whether to use a fallback cookie or not
* @return the value of the state cookie, if it exists
*/
static String getState(HttpServletRequest request, HttpServletResponse response, boolean useLegacySameSiteCookie) {
return getOnce(StorageUtils.STATE_KEY, request, response, useLegacySameSiteCookie);
static String getState(HttpServletRequest request, HttpServletResponse response) {
return getOnce(StorageUtils.STATE_KEY, request, response);
}

/**
* Gets the value associated with the nonce cookie and removes it.
*
* @param request the request object
* @param response the response object
* @param useLegacySameSiteCookie whether to use a fallback cookie or not
* @return the value of the nonce cookie, if it exists
*/
static String getNonce(HttpServletRequest request, HttpServletResponse response, boolean useLegacySameSiteCookie) {
return getOnce(StorageUtils.NONCE_KEY, request, response, useLegacySameSiteCookie);
static String getNonce(HttpServletRequest request, HttpServletResponse response) {
return getOnce(StorageUtils.NONCE_KEY, request, response);
}

private static void store(HttpServletResponse response, String key, String value, SameSite sameSite, boolean useLegacySameSiteCookie, boolean isSecureCookie) {
private static void store(HttpServletResponse response, String key, String value, SameSite sameSite, boolean useLegacySameSiteCookie, boolean isSecureCookie, String cookiePath) {
Validate.notNull(response, "response must not be null");
Validate.notNull(key, "key must not be null");
Validate.notNull(sameSite, "sameSite must not be null");
Expand All @@ -82,6 +80,9 @@ private static void store(HttpServletResponse response, String key, String value
AuthCookie sameSiteCookie = new AuthCookie(key, value);
sameSiteCookie.setSameSite(sameSite);
sameSiteCookie.setSecure(isSameSiteNone || isSecureCookie);
if (cookiePath != null) {
sameSiteCookie.setPath(cookiePath);
}

// Servlet Cookie API does not yet support setting the SameSite attribute, so just set cookie on header
response.addHeader("Set-Cookie", sameSiteCookie.buildHeaderString());
Expand All @@ -95,7 +96,7 @@ private static void store(HttpServletResponse response, String key, String value

}

private static String getOnce(String cookieName, HttpServletRequest request, HttpServletResponse response, boolean useLegacySameSiteCookie) {
private static String getOnce(String cookieName, HttpServletRequest request, HttpServletResponse response) {
Cookie[] requestCookies = request.getCookies();
if (requestCookies == null) {
return null;
Expand Down
18 changes: 18 additions & 0 deletions src/test/java/com/auth0/AuthenticationControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -565,4 +565,22 @@ public void shouldThrowOnNullInvitationParameter() {
() -> AuthenticationController.newBuilder("DOMAIN", "CLIENT_ID", "SECRET")
.withInvitation(null));
}

@Test
public void shouldConfigureCookiePath() {
MockHttpServletResponse response = new MockHttpServletResponse();

AuthenticationController controller = AuthenticationController.newBuilder("domain", "clientId", "clientSecret")
.withCookiePath("/Path")
.build();

controller.buildAuthorizeUrl(new MockHttpServletRequest(), response, "https://redirect.uri/here")
.withState("state")
.build();

List<String> headers = response.getHeaders("Set-Cookie");

assertThat(headers.size(), is(1));
assertThat(headers, everyItem(is("com.auth0.state=state; HttpOnly; Max-Age=600; Path=/Path; SameSite=Lax")));
}
}
Loading

0 comments on commit 4a8a07c

Please sign in to comment.