From f64c3147cc0cd7a98d5f4b0c9c22fa01c3225383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Ch=C3=A9dru?= Date: Wed, 2 Nov 2022 12:34:26 +0100 Subject: [PATCH] Make cookie name configurable (#68) Fixes #67 --- .../authentication/JwtCookieAuthBundle.java | 77 ++++++++++++------- .../JwtCookieAuthConfiguration.java | 13 ++++ .../JwtCookieAuthenticationTest.java | 27 +++---- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthBundle.java b/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthBundle.java index d38dc5c..567abb0 100644 --- a/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthBundle.java +++ b/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthBundle.java @@ -1,12 +1,12 @@ /** * Copyright 2020 Dhatim - * + *

* 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 - * + *

* http://www.apache.org/licenses/LICENSE-2.0 - * + *

* 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 @@ -30,6 +30,11 @@ 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; @@ -37,33 +42,31 @@ 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 Your application configuration class * @param

the class of the principal that will be serialized in / deserialized from JWT cookies */ -public class JwtCookieAuthBundle implements ConfiguredBundle{ +public class JwtCookieAuthBundle implements ConfiguredBundle { + 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

principalType; - private final Function serializer; + private final Function serializer; private final Function deserializer; private Function configurationSupplier; private BiFunction keySuppplier; /** * Get a bundle instance that will use DefaultJwtCookiePrincipal + * * @param Your application configuration class * @return a bundle instance that will use DefaultJwtCookiePrincipal */ - public static JwtCookieAuthBundle getDefault(){ + public static JwtCookieAuthBundle getDefault() { return new JwtCookieAuthBundle<>( DefaultJwtCookiePrincipal.class, DefaultJwtCookiePrincipal::getClaims, @@ -72,11 +75,12 @@ public static JwtCookieAuthBundle principalType, Function serializer, Function deserializer) { + public JwtCookieAuthBundle(Class

principalType, Function serializer, Function deserializer) { this.principalType = principalType; this.serializer = serializer; this.deserializer = deserializer; @@ -85,16 +89,18 @@ public JwtCookieAuthBundle(Class

principalType, Function 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 withKeyProvider(BiFunction keySupplier){ + public JwtCookieAuthBundle withKeyProvider(BiFunction 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 */ @@ -121,7 +127,7 @@ 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)); @@ -129,22 +135,36 @@ public void run(C configuration, Environment environment) throws Exception { } /** - * 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 getAuthRequestFilter(Key key){ + public AuthFilter 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::isInRole)) + .setAuthorizer((Authorizer

) (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 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 */ @@ -152,7 +172,7 @@ public ContainerResponseFilter getAuthResponseFilter(Key key, JwtCookieAuthConfi return new JwtCookieAuthResponseFilter<>( principalType, serializer, - DEFAULT_COOKIE_NAME, + configuration.getCookieName(), configuration.isSecure(), configuration.isHttpOnly(), configuration.getDomain(), @@ -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) { @@ -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); } } diff --git a/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthConfiguration.java b/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthConfiguration.java index 16d640c..dd80df0 100644 --- a/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthConfiguration.java +++ b/src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthConfiguration.java @@ -18,6 +18,8 @@ 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 */ @@ -25,6 +27,8 @@ public class JwtCookieAuthConfiguration { private String secretSeed; + private String cookieName = JWT_COOKIE_DEFAULT_NAME; + private boolean secure = false; private boolean httpOnly = true; @@ -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 here. * diff --git a/src/test/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthenticationTest.java b/src/test/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthenticationTest.java index 355dae4..d1eb784 100644 --- a/src/test/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthenticationTest.java +++ b/src/test/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthenticationTest.java @@ -42,6 +42,7 @@ public class JwtCookieAuthenticationTest { private static final DropwizardAppExtension EXT = new DropwizardAppExtension(TestApplication.class); + private static final String COOKIE_NAME = "sessionToken"; private WebTarget getTarget() { return EXT.client().target("http://localhost:" + EXT.getLocalPort() + "/application").path("principal"); @@ -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()); @@ -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()); @@ -84,24 +85,24 @@ 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 @@ -109,14 +110,14 @@ 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()); @@ -132,7 +133,7 @@ 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()); @@ -140,7 +141,7 @@ public void testRoles() { //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()); @@ -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()); @@ -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();