Skip to content

Commit b090498

Browse files
adds session-cookie boolean configuration (#1906)
* adds session-cookie boolean configuration By default, there was already an expiration date. This PR adds a configuration option to define. whether the cookie is a [session cookie](https://en.wikipedia.org/wiki/HTTP_cookie#Expires_and_Max-Age). A session cookie does not have an expiration date. When set to true, then no Expires atttribute is set for the cookie, making it an session cookie. Close: #339 * Update security-jwt/src/test/java/io/micronaut/security/token/jwt/cookie/JwtCookieSessionCookieTest.java * chore(deps): update plugin io.micronaut.build.shared.settings to v7.3.1 (#1905) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update softprops/action-gh-release action to v2.2.1 (#1907) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * extract access and refresh token Cookie methods (#1908) This pull request extracts two methods to ease TokenCookieLoginHandler bean replacement. see: #339 * change javadoc * don’t define default value --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1 parent 900c5ca commit b090498

File tree

12 files changed

+207
-7
lines changed

12 files changed

+207
-7
lines changed

security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,13 @@ public interface CsrfConfiguration extends CookieConfiguration, Toggleable {
6262
*/
6363
@NonNull
6464
String getFieldName();
65+
66+
/**
67+
*
68+
* @return whether the CSRF cookie is a session cookie. A session cookie does not have an expiration date. If set to true, then {@link CsrfConfiguration#getCookieMaxAge()} is ignored.
69+
* @since 4.12.0
70+
*/
71+
default boolean isSessionCookie() {
72+
return false;
73+
}
6574
}

security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ final class CsrfConfigurationProperties implements CsrfConfiguration {
4242
public static final String DEFAULT_FIELD_NAME = "csrfToken";
4343

4444
/**
45-
* The default cookie name..
45+
* The default cookie name.
4646
* @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#using-cookies-with-host-prefixes-to-identify-origins">Using Cookies with Host Prefixes to Identify Origins</a>
4747
*/
4848
@SuppressWarnings("WeakerAccess")
@@ -59,7 +59,6 @@ final class CsrfConfigurationProperties implements CsrfConfiguration {
5959
*/
6060
@SuppressWarnings("WeakerAccess")
6161
public static final SameSite DEFAULT_SAME_SITE = SameSite.Strict;
62-
6362
public static final int DEFAULT_RANDOM_VALUE_SIZE = 16;
6463

6564
public static final boolean DEFAULT_ENABLED = true;
@@ -87,6 +86,22 @@ final class CsrfConfigurationProperties implements CsrfConfiguration {
8786

8887
@Nullable
8988
private String signatureKey;
89+
private boolean sessionCookie;
90+
91+
@Override
92+
public boolean isSessionCookie() {
93+
return sessionCookie;
94+
}
95+
96+
/**
97+
* Whether the cookie is a session cookie. A session cookie does not have an expiration date. `cookie-max-age` is ignored if session cookie is set to true. Default value (false).
98+
*
99+
* @param sessionCookie Whether the cookie is a session cookie.
100+
* @since 4.12.0
101+
*/
102+
public void setSessionCookie(boolean sessionCookie) {
103+
this.sessionCookie = sessionCookie;
104+
}
90105

91106
@Override
92107
@Nullable
@@ -243,6 +258,9 @@ public void setCookieHttpOnly(Boolean cookieHttpOnly) {
243258

244259
@Override
245260
public Optional<TemporalAmount> getCookieMaxAge() {
261+
if (isSessionCookie()) {
262+
return Optional.empty();
263+
}
246264
return Optional.ofNullable(cookieMaxAge);
247265
}
248266

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.micronaut.security.csrf;
2+
3+
import io.micronaut.context.annotation.Property;
4+
import io.micronaut.core.util.StringUtils;
5+
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertFalse;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
10+
11+
@Property(name = "micronaut.security.csrf.session-cookie", value = StringUtils.TRUE)
12+
@MicronautTest(startApplication = false)
13+
class CsrfConfigurationSessionCookieTest {
14+
15+
@Test
16+
void whenSettingSessionCookieMaxAgeIgnored(CsrfConfiguration csrfConfiguration) {
17+
assertTrue(csrfConfiguration.isSessionCookie());
18+
assertFalse(csrfConfiguration.getCookieMaxAge().isPresent());
19+
}
20+
}

security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ void defaultHeaderName() {
1919
assertEquals("X-CSRF-TOKEN", csrfConfiguration.getHeaderName());
2020
}
2121

22+
@Test
23+
void defaultIsSessionCookie() {
24+
assertFalse(csrfConfiguration.isSessionCookie());
25+
}
26+
2227
@Test
2328
void defaultFieldName() {
2429
assertEquals("csrfToken", csrfConfiguration.getFieldName());

security-jwt/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies {
4040

4141
testImplementation(libs.system.stubs.core)
4242

43+
testAnnotationProcessor(mn.micronaut.inject.java)
4344
testImplementation(mnTest.micronaut.test.junit5)
4445
testImplementation(libs.junit.jupiter.params)
4546
testRuntimeOnly(libs.junit.jupiter.engine)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.micronaut.security.token.jwt.cookie;
2+
3+
import io.micronaut.context.annotation.Property;
4+
import io.micronaut.context.annotation.Requires;
5+
import io.micronaut.core.util.StringUtils;
6+
import io.micronaut.http.HttpRequest;
7+
import io.micronaut.http.HttpResponse;
8+
import io.micronaut.http.MediaType;
9+
import io.micronaut.http.client.BlockingHttpClient;
10+
import io.micronaut.http.client.HttpClient;
11+
import io.micronaut.http.client.annotation.Client;
12+
import io.micronaut.security.authentication.AuthenticationRequest;
13+
import io.micronaut.security.authentication.AuthenticationResponse;
14+
import io.micronaut.security.authentication.provider.HttpRequestExecutorAuthenticationProvider;
15+
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
16+
import jakarta.inject.Singleton;
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.util.Map;
20+
21+
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
23+
@Property(name = "micronaut.http.client.followRedirects", value = StringUtils.FALSE)
24+
@Property(name = "micronaut.security.authentication", value = "cookie")
25+
@Property(name = "micronaut.security.token.cookie.session-cookie", value = StringUtils.TRUE)
26+
@Property(name = "micronaut.security.redirect.login-failure", value = "/login/authFailed")
27+
@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "qrD6h8K6S9503Q06Y6Rfk21TErImPYqa")
28+
@Property(name = "spec.name", value = "JwtCookieSessionCookeTest")
29+
@MicronautTest
30+
class JwtCookieSessionCookieTest {
31+
32+
@Test
33+
void testMaxAgeIsSetFromJwtCookieSettings(@Client("/") HttpClient httpClient) {
34+
BlockingHttpClient client = httpClient.toBlocking();
35+
HttpRequest<?> loginRequest = HttpRequest.POST("/login", Map.of("username","sherlock","password","password"))
36+
.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
37+
38+
HttpResponse<?> loginRsp = client.exchange(loginRequest, String.class);
39+
40+
String cookie = loginRsp.getHeaders().get("Set-Cookie");
41+
assertFalse(cookie.contains("Max-Age="));
42+
assertFalse(cookie.contains("Expires="));
43+
}
44+
45+
@Requires(property = "spec.name", value = "JwtCookieSessionCookeTest")
46+
@Singleton
47+
static class AuthProvider<B> implements HttpRequestExecutorAuthenticationProvider<B> {
48+
49+
@Override
50+
public AuthenticationResponse authenticate(HttpRequest<B> requestContext, AuthenticationRequest<String, String> authRequest) {
51+
return AuthenticationResponse.success("sherlock");
52+
}
53+
}
54+
55+
}

security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/AbstractCookieConfiguration.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public abstract class AbstractCookieConfiguration implements CookieConfiguration
3232

3333
private static final boolean DEFAULT_HTTPONLY = true;
3434
private static final String DEFAULT_COOKIEPATH = "/";
35-
3635
private static final Duration DEFAULT_MAX_AGE = Duration.ofMinutes(5);
3736

3837
protected String cookieDomain;
@@ -42,6 +41,26 @@ public abstract class AbstractCookieConfiguration implements CookieConfiguration
4241
protected Boolean cookieHttpOnly = DEFAULT_HTTPONLY;
4342
protected Duration cookieMaxAge = DEFAULT_MAX_AGE;
4443
protected String cookieName = null;
44+
protected boolean sessionCookie;
45+
46+
/**
47+
* Whether the cookie is a session cookie. A session cookie does not have an expiration date. `cookie-max-age` is ignored if session cookie is set to true. Default value (false).
48+
* @return whether the cookie is a session cookie. A session cookie does not have an expiration date. If set to true, then {@link CookieConfiguration#getCookieMaxAge()} is ignored.
49+
* @since 4.12.0
50+
*/
51+
public boolean isSessionCookie() {
52+
return sessionCookie;
53+
}
54+
55+
/**
56+
* Whether the cookie is a session cookie. A session cookie does not have an expiration date. `cookie-max-age` is ignored if session cookie is set to true. Default value (false).
57+
*
58+
* @param sessionCookie Whether the cookie is a session cookie.
59+
* @since 4.12.0
60+
*/
61+
public void setSessionCookie(boolean sessionCookie) {
62+
this.sessionCookie = sessionCookie;
63+
}
4564

4665
@Override
4766
public Optional<String> getCookieDomain() {
@@ -121,6 +140,9 @@ public void setCookieHttpOnly(Boolean cookieHttpOnly) {
121140

122141
@Override
123142
public Optional<TemporalAmount> getCookieMaxAge() {
143+
if (isSessionCookie()) {
144+
return Optional.empty();
145+
}
124146
return Optional.ofNullable(cookieMaxAge);
125147
}
126148

security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ public List<Cookie> getCookies(Authentication authentication, HttpRequest<?> req
104104

105105
Cookie jwtCookie = Cookie.of(accessTokenCookieConfiguration.getCookieName(), accessToken);
106106
jwtCookie.configure(accessTokenCookieConfiguration, request.isSecure());
107-
jwtCookie.maxAge(cookieExpiration(authentication, request));
107+
if (!accessTokenCookieConfiguration.isSessionCookie()) {
108+
jwtCookie.maxAge(cookieExpiration(authentication, request));
109+
}
108110
cookies.add(jwtCookie);
109111
for (LoginCookieProvider<HttpRequest<?>> loginCookieProvider : loginCookieProviders) {
110112
cookies.add(loginCookieProvider.provideCookie(request));

security/src/main/java/io/micronaut/security/config/TokenCookieConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,12 @@
2525
* @since 2.0.0
2626
*/
2727
public interface TokenCookieConfiguration extends CookieConfiguration, Toggleable {
28+
/**
29+
*
30+
* @return whether the cookie is a session cookie. A session cookie does not have an expiration date. If set to true, then {@link CookieConfiguration#getCookieMaxAge()} is ignored.
31+
* @since 4.12.0
32+
*/
33+
default boolean isSessionCookie() {
34+
return false;
35+
}
2836
}

security/src/main/java/io/micronaut/security/token/cookie/AbstractAccessTokenCookieConfigurationProperties.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ public abstract class AbstractAccessTokenCookieConfigurationProperties implement
4848
protected Boolean cookieSecure;
4949
protected Duration cookieMaxAge;
5050
protected SameSite cookieSameSite = DEFAULT_COOKIESAMESITE;
51+
protected boolean sessionCookie;
52+
53+
@Override
54+
public boolean isSessionCookie() {
55+
return sessionCookie;
56+
}
57+
58+
/**
59+
* Whether the cookie is a session cookie. A session cookie does not have an expiration date. `cookie-max-age` is ignored if session cookie is set to true. Default value (false).
60+
*
61+
* @param sessionCookie Whether the cookie is a session cookie.
62+
* @since 4.12.0
63+
*/
64+
public void setSessionCookie(boolean sessionCookie) {
65+
this.sessionCookie = sessionCookie;
66+
}
5167

5268
/**
5369
*
@@ -80,6 +96,9 @@ public Optional<Boolean> isCookieSecure() {
8096
*/
8197
@Override
8298
public Optional<TemporalAmount> getCookieMaxAge() {
99+
if (isSessionCookie()) {
100+
return Optional.empty();
101+
}
83102
return Optional.ofNullable(cookieMaxAge);
84103
}
85104

security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,10 @@ protected List<Cookie> getCookies(AccessRefreshToken accessRefreshToken, HttpReq
146146
protected Cookie accessTokenCookie(@NonNull AccessRefreshToken accessRefreshToken, @NonNull HttpRequest<?> request) {
147147
Cookie jwtCookie = Cookie.of(accessTokenCookieConfiguration.getCookieName(), accessRefreshToken.getAccessToken());
148148
jwtCookie.configure(accessTokenCookieConfiguration, request.isSecure());
149-
TemporalAmount maxAge = accessTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofSeconds(accessTokenConfiguration.getExpiration()));
150-
jwtCookie.maxAge(maxAge);
149+
if (!accessTokenCookieConfiguration.isSessionCookie()) {
150+
TemporalAmount maxAge = accessTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofSeconds(accessTokenConfiguration.getExpiration()));
151+
jwtCookie.maxAge(maxAge);
152+
}
151153
return jwtCookie;
152154
}
153155

@@ -166,7 +168,9 @@ protected Optional<Cookie> refreshTokenCookie(@NonNull AccessRefreshToken access
166168
}
167169
Cookie refreshCookie = Cookie.of(refreshTokenCookieConfiguration.getCookieName(), refreshToken);
168170
refreshCookie.configure(refreshTokenCookieConfiguration, request.isSecure());
169-
refreshCookie.maxAge(refreshTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofDays(30)));
171+
if (!refreshTokenCookieConfiguration.isSessionCookie()) {
172+
refreshCookie.maxAge(refreshTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofDays(30)));
173+
}
170174
return Optional.of(refreshCookie);
171175
}
172176
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.micronaut.security.config;
2+
3+
import io.micronaut.context.ApplicationContext;
4+
import io.micronaut.core.util.StringUtils;
5+
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
6+
import org.junit.jupiter.api.Test;
7+
import java.util.Collections;
8+
import java.util.Map;
9+
10+
import static org.junit.jupiter.api.Assertions.assertFalse;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
@MicronautTest(startApplication = false)
14+
class TokenCookieConfigurationTest {
15+
16+
@Test
17+
void iSessionCookieDefaultsToFalse() {
18+
try (ApplicationContext ctx = ApplicationContext.run(Map.of("micronaut.security.authentication", "cookie",
19+
"micronaut.security.token.cookie.cookie-max-age", "5m"
20+
))) {
21+
TokenCookieConfiguration tokenCookieConfiguration = ctx.getBean(TokenCookieConfiguration.class);
22+
assertFalse(tokenCookieConfiguration.isSessionCookie());
23+
assertTrue(tokenCookieConfiguration.getCookieMaxAge().isPresent());
24+
}
25+
26+
try (ApplicationContext ctx = ApplicationContext.run(Map.of("micronaut.security.authentication", "cookie",
27+
"micronaut.security.token.cookie.session-cookie", StringUtils.TRUE,
28+
"micronaut.security.token.cookie.cookie-max-age", "5m"
29+
))) {
30+
TokenCookieConfiguration tokenCookieConfiguration = ctx.getBean(TokenCookieConfiguration.class);
31+
assertTrue(tokenCookieConfiguration.isSessionCookie());
32+
// by setting session-cookie to true, the cookie-max-age should be ignored
33+
assertFalse(tokenCookieConfiguration.getCookieMaxAge().isPresent());
34+
}
35+
36+
}
37+
}

0 commit comments

Comments
 (0)