Skip to content

Commit

Permalink
Make cookie name configurable (#68)
Browse files Browse the repository at this point in the history
Fixes #67
  • Loading branch information
Olivier Chédru authored Nov 2, 2022
1 parent 9cda449 commit f64c314
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Copyright 2020 Dhatim
*
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
*
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
Expand All @@ -30,40 +30,43 @@
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClaims;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;

import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.container.ContainerResponseFilter;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.container.ContainerResponseFilter;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;

/**
* Dopwizard bundle
*
* @param <C> Your application configuration class
* @param <P> the class of the principal that will be serialized in / deserialized from JWT cookies
*/
public class JwtCookieAuthBundle<C extends Configuration, P extends JwtCookiePrincipal> implements ConfiguredBundle<C>{
public class JwtCookieAuthBundle<C extends Configuration, P extends JwtCookiePrincipal> implements ConfiguredBundle<C> {

public static final String JWT_COOKIE_DEFAULT_NAME = "sessionToken";
private static final String JWT_COOKIE_PREFIX = "jwtCookie";
private static final String DEFAULT_COOKIE_NAME = "sessionToken";

private final Class<P> principalType;
private final Function<P,Claims> serializer;
private final Function<P, Claims> serializer;
private final Function<Claims, P> deserializer;
private Function<C, JwtCookieAuthConfiguration> configurationSupplier;
private BiFunction<C, Environment, Key> keySuppplier;

/**
* Get a bundle instance that will use DefaultJwtCookiePrincipal
*
* @param <C> Your application configuration class
* @return a bundle instance that will use DefaultJwtCookiePrincipal
*/
public static <C extends Configuration> JwtCookieAuthBundle<C, DefaultJwtCookiePrincipal> getDefault(){
public static <C extends Configuration> JwtCookieAuthBundle<C, DefaultJwtCookiePrincipal> getDefault() {
return new JwtCookieAuthBundle<>(
DefaultJwtCookiePrincipal.class,
DefaultJwtCookiePrincipal::getClaims,
Expand All @@ -72,11 +75,12 @@ public static <C extends Configuration> JwtCookieAuthBundle<C, DefaultJwtCookieP

/**
* Build a new instance of JwtCookieAuthBundle
*
* @param principalType the class of the principal that will be serialized in / deserialized from JWT cookies
* @param serializer a function to serialize principals into JWT claims
* @param deserializer a function to deserialize JWT claims into principals
* @param serializer a function to serialize principals into JWT claims
* @param deserializer a function to deserialize JWT claims into principals
*/
public JwtCookieAuthBundle(Class<P> principalType, Function<P,Claims> serializer, Function<Claims, P> deserializer) {
public JwtCookieAuthBundle(Class<P> principalType, Function<P, Claims> serializer, Function<Claims, P> deserializer) {
this.principalType = principalType;
this.serializer = serializer;
this.deserializer = deserializer;
Expand All @@ -85,16 +89,18 @@ public JwtCookieAuthBundle(Class<P> principalType, Function<P,Claims> serializer

/**
* If you want to sign the JWT with your own key, specify it here
*
* @param keySupplier a bi-function which will return the signing key from the configuration and environment
* @return this
*/
public JwtCookieAuthBundle<C, P> withKeyProvider(BiFunction<C, Environment, Key> keySupplier){
public JwtCookieAuthBundle<C, P> withKeyProvider(BiFunction<C, Environment, Key> keySupplier) {
this.keySuppplier = keySupplier;
return this;
}

/**
* If you need to configure the bundle, specify it here
*
* @param configurationSupplier a bi-function which will return the bundle configuration from the application configuration
* @return this
*/
Expand All @@ -121,38 +127,52 @@ public void run(C configuration, Environment environment) throws Exception {
.orElseGet(() -> generateKey(conf.getSecretSeed()));

JerseyEnvironment jerseyEnvironment = environment.jersey();
jerseyEnvironment.register(new AuthDynamicFeature(getAuthRequestFilter(key)));
jerseyEnvironment.register(new AuthDynamicFeature(getAuthRequestFilter(key, conf.getCookieName())));
jerseyEnvironment.register(new AuthValueFactoryProvider.Binder<>(principalType));
jerseyEnvironment.register(RolesAllowedDynamicFeature.class);
jerseyEnvironment.register(getAuthResponseFilter(key, conf));
jerseyEnvironment.register(DontRefreshSessionFilter.class);
}

/**
* Get a filter that will desezialize the principal from JWT cookies found in HTTP requests
* @param key the key used to validate the JWT
* Get a filter that will deserialize the principal from JWT cookies found in HTTP requests
*
* @param key the key used to validate the JWT
* @param cookieName the name of the cookie holding the JWT
* @return the request filter
*/
public AuthFilter<String, P> getAuthRequestFilter(Key key){
public AuthFilter<String, P> getAuthRequestFilter(Key key, String cookieName) {
return new JwtCookieAuthRequestFilter.Builder()
.setCookieName(DEFAULT_COOKIE_NAME)
.setCookieName(cookieName)
.setAuthenticator(new JwtCookiePrincipalAuthenticator(key, deserializer))
.setPrefix(JWT_COOKIE_PREFIX)
.setAuthorizer((Authorizer<P>)(P::isInRole))
.setAuthorizer((Authorizer<P>) (P::isInRole))
.buildAuthFilter();
}

/**
* Get a filter that will deserialize the principal from JWT cookies found in HTTP requests,
* using the default cookie name.
*
* @param key the key used to validate the JWT
* @return the request filter
*/
public AuthFilter<String, P> getAuthRequestFilter(Key key) {
return getAuthRequestFilter(key, JWT_COOKIE_DEFAULT_NAME);
}

/**
* Get a filter that will serialize principals into JWTs and add them to HTTP response cookies
* @param key the key used to sign the JWT
*
* @param key the key used to sign the JWT
* @param configuration cookie configuration (secure, httpOnly, expiration...)
* @return the response filter
*/
public ContainerResponseFilter getAuthResponseFilter(Key key, JwtCookieAuthConfiguration configuration) {
return new JwtCookieAuthResponseFilter<>(
principalType,
serializer,
DEFAULT_COOKIE_NAME,
configuration.getCookieName(),
configuration.isSecure(),
configuration.isHttpOnly(),
configuration.getDomain(),
Expand All @@ -164,9 +184,10 @@ public ContainerResponseFilter getAuthResponseFilter(Key key, JwtCookieAuthConfi

/**
* Generate a HMAC SHA256 Key that can be used to sign JWTs
*
* @param secretSeed a seed from which the key will be generated.
* Identical seeds will generate identical keys.
* If null, a random key is returned.
* Identical seeds will generate identical keys.
* If null, a random key is returned.
* @return a HMAC SHA256 Key
*/
public static Key generateKey(String secretSeed) {
Expand All @@ -178,10 +199,10 @@ public static Key generateKey(String secretSeed) {
.orElseGet(getHmacSha256KeyGenerator()::generateKey);
}

private static KeyGenerator getHmacSha256KeyGenerator(){
try{
private static KeyGenerator getHmacSha256KeyGenerator() {
try {
return KeyGenerator.getInstance(SignatureAlgorithm.HS256.getJcaName());
} catch(NoSuchAlgorithmException e){
} catch (NoSuchAlgorithmException e) {
throw new SecurityException(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
import javax.annotation.Nullable;
import javax.validation.constraints.NotEmpty;

import static org.dhatim.dropwizard.jwt.cookie.authentication.JwtCookieAuthBundle.JWT_COOKIE_DEFAULT_NAME;

/**
* Bundle configuration class
*/
public class JwtCookieAuthConfiguration {

private String secretSeed;

private String cookieName = JWT_COOKIE_DEFAULT_NAME;

private boolean secure = false;

private boolean httpOnly = true;
Expand All @@ -51,6 +55,15 @@ public String getSecretSeed() {
return secretSeed;
}

/**
* The name of the cookie holding the JWT. Its default value is "sessionToken".
*
* @return the cookie name
*/
public String getCookieName() {
return cookieName;
}

/**
* Check if the {@code Secure} cookie attribute is set, as described <a href="https://tools.ietf.org/html/rfc6265#section-5.2.5">here</a>.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
public class JwtCookieAuthenticationTest {

private static final DropwizardAppExtension<Configuration> EXT = new DropwizardAppExtension<Configuration>(TestApplication.class);
private static final String COOKIE_NAME = "sessionToken";

private WebTarget getTarget() {
return EXT.client().target("http://localhost:" + EXT.getLocalPort() + "/application").path("principal");
Expand All @@ -63,7 +64,7 @@ public void testCookieSetting() throws IOException {
Assert.assertEquals(principalName, principal.getName());

//check that a session cookie has been set
NewCookie cookie1 = response.getCookies().get("sessionToken");
NewCookie cookie1 = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie1);
Assert.assertTrue(Strings.hasText(cookie1.getValue()));
Assert.assertTrue(cookie1.isHttpOnly());
Expand All @@ -73,7 +74,7 @@ public void testCookieSetting() throws IOException {
Assert.assertEquals(200, response.getStatus());
principal = getPrincipal(response);
Assert.assertEquals(principalName, principal.getName());
NewCookie cookie2 = response.getCookies().get("sessionToken");
NewCookie cookie2 = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie2);
Assert.assertTrue(Strings.hasText(cookie1.getValue()));
Assert.assertNotSame(cookie1.getValue(), cookie2.getValue());
Expand All @@ -84,39 +85,39 @@ public void testDontRefreshSession() throws IOException {
//requests made to methods annotated with @DontRefreshSession should not modify the cookie
String principalName = UUID.randomUUID().toString();
Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(principalName)));
NewCookie cookie = response.getCookies().get("sessionToken");
NewCookie cookie = response.getCookies().get(COOKIE_NAME);

response = getTarget().path("idempotent").request(MediaType.APPLICATION_JSON).cookie(cookie).get();
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(principalName, getPrincipal(response).getName());
Assert.assertNull(response.getCookies().get("sessionToken"));
Assert.assertNull(response.getCookies().get(COOKIE_NAME));
}

@Test
public void testPublicEndpoint() {
//public endpoints (i.e. not with @Auth, @RolesAllowed etc.) should not modify the cookie
Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(UUID.randomUUID().toString())));
NewCookie cookie = response.getCookies().get("sessionToken");
NewCookie cookie = response.getCookies().get(COOKIE_NAME);

//request made to public methods should not refresh the cookie
response = getTarget().path("public").request(MediaType.APPLICATION_JSON).cookie(cookie).get();
Assert.assertEquals(200, response.getStatus());
Assert.assertNull(response.getCookies().get("sessionToken"));
Assert.assertNull(response.getCookies().get(COOKIE_NAME));
}

@Test
public void testRememberMe() {
//a volatile principal should set a volatile cookie
DefaultJwtCookiePrincipal principal = new DefaultJwtCookiePrincipal(UUID.randomUUID().toString());
Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal));
NewCookie cookie = response.getCookies().get("sessionToken");
NewCookie cookie = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie);
Assert.assertEquals(-1, cookie.getMaxAge());

//a long term principal should set a persistent cookie
principal.setPersistent(true);
response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal));
cookie = response.getCookies().get("sessionToken");
cookie = response.getCookies().get(COOKIE_NAME);
//default maxAge is 604800s (7 days)
Assert.assertNotNull(cookie);
Assert.assertEquals(604800, cookie.getMaxAge());
Expand All @@ -132,15 +133,15 @@ public void testRoles() {
//set a principal without the admin role (-> 403 FORBIDDEN)
DefaultJwtCookiePrincipal principal = new DefaultJwtCookiePrincipal(UUID.randomUUID().toString());
response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal));
NewCookie cookie = response.getCookies().get("sessionToken");
NewCookie cookie = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie);
response = restrictedTarget.request().cookie(cookie).get();
Assert.assertEquals(403, response.getStatus());

//set a principal with the admin role (-> 200 OK)
principal.setRoles(Collections.singleton("admin"));
response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal));
cookie = response.getCookies().get("sessionToken");
cookie = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie);
response = restrictedTarget.request().cookie(cookie).get();
Assert.assertEquals(200, response.getStatus());
Expand All @@ -149,13 +150,13 @@ public void testRoles() {
@Test
public void testDeleteCookie() {
Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(UUID.randomUUID().toString())));
NewCookie cookie = response.getCookies().get("sessionToken");
NewCookie cookie = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie);

//removing the principal should produce a cookie with empty contenant and a past expiration date
response = getTarget().path("unset").request().cookie(cookie).get();
Assert.assertEquals(204, response.getStatus());
cookie = response.getCookies().get("sessionToken");
cookie = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie);
Assert.assertEquals("", cookie.getValue());
Assert.assertEquals(Date.from(Instant.EPOCH), cookie.getExpiry());
Expand All @@ -166,7 +167,7 @@ public void testGetCurrentPrincipal() throws IOException {
//test to get principal from CurrentPrincipal.get() instead of @Auth
String principalName = UUID.randomUUID().toString();
Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(principalName)));
NewCookie cookie = response.getCookies().get("sessionToken");
NewCookie cookie = response.getCookies().get(COOKIE_NAME);
Assert.assertNotNull(cookie);

response = getTarget().path("current").request(MediaType.APPLICATION_JSON).cookie(cookie).get();
Expand Down

0 comments on commit f64c314

Please sign in to comment.