Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds session-cookie boolean configuration #1906

Open
wants to merge 8 commits into
base: 4.12.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,6 @@ jobs:
- name: Upload assets
# Upload the artifacts to the existing release. Note that the SLSA provenance will
# attest to each artifact file and not the aggregated ZIP file.
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
files: artifacts.zip
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ public interface CsrfConfiguration extends CookieConfiguration, Toggleable {
*/
@NonNull
String getFieldName();

/**
*
* @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.
* @since 4.12.0
*/
default boolean isSessionCookie() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ final class CsrfConfigurationProperties implements CsrfConfiguration {
public static final String DEFAULT_FIELD_NAME = "csrfToken";

/**
* The default cookie name..
* The default cookie name.
* @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>
*/
@SuppressWarnings("WeakerAccess")
Expand All @@ -59,7 +59,6 @@ final class CsrfConfigurationProperties implements CsrfConfiguration {
*/
@SuppressWarnings("WeakerAccess")
public static final SameSite DEFAULT_SAME_SITE = SameSite.Strict;

public static final int DEFAULT_RANDOM_VALUE_SIZE = 16;

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

@Nullable
private String signatureKey;
private boolean sessionCookie;

@Override
public boolean isSessionCookie() {
return sessionCookie;
}

/**
* 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).
*
* @param sessionCookie Whether the cookie is a session cookie.
* @since 4.12.0
*/
public void setSessionCookie(boolean sessionCookie) {
this.sessionCookie = sessionCookie;
}

@Override
@Nullable
Expand Down Expand Up @@ -243,6 +258,9 @@ public void setCookieHttpOnly(Boolean cookieHttpOnly) {

@Override
public Optional<TemporalAmount> getCookieMaxAge() {
if (isSessionCookie()) {
return Optional.empty();
}
return Optional.ofNullable(cookieMaxAge);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.micronaut.security.csrf;

import io.micronaut.context.annotation.Property;
import io.micronaut.core.util.StringUtils;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Property(name = "micronaut.security.csrf.session-cookie", value = StringUtils.TRUE)
@MicronautTest(startApplication = false)
class CsrfConfigurationSessionCookieTest {

@Test
void whenSettingSessionCookieMaxAgeIgnored(CsrfConfiguration csrfConfiguration) {
assertTrue(csrfConfiguration.isSessionCookie());
assertFalse(csrfConfiguration.getCookieMaxAge().isPresent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ void defaultHeaderName() {
assertEquals("X-CSRF-TOKEN", csrfConfiguration.getHeaderName());
}

@Test
void defaultIsSessionCookie() {
assertFalse(csrfConfiguration.isSessionCookie());
}

@Test
void defaultFieldName() {
assertEquals("csrfToken", csrfConfiguration.getFieldName());
Expand Down
4 changes: 4 additions & 0 deletions security-jwt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ dependencies {
testImplementation(mnTestResources.testcontainers.core)

testImplementation(libs.system.stubs.core)

testAnnotationProcessor(mn.micronaut.inject.java)
testImplementation(mnTest.micronaut.test.junit5)
testRuntimeOnly(libs.junit.jupiter.engine)
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.micronaut.security.token.jwt.cookie;

import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.security.authentication.AuthenticationRequest;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.provider.HttpRequestExecutorAuthenticationProvider;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Singleton;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertFalse;

@Property(name = "micronaut.http.client.followRedirects", value = StringUtils.FALSE)
@Property(name = "micronaut.security.authentication", value = "cookie")
@Property(name = "micronaut.security.token.cookie.session-cookie", value = StringUtils.TRUE)
@Property(name = "micronaut.security.redirect.login-failure", value = "/login/authFailed")
@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "qrD6h8K6S9503Q06Y6Rfk21TErImPYqa")
@Property(name = "spec.name", value = "JwtCookieSessionCookeTest")
@MicronautTest
class JwtCookieSessionCookieTest {

@Test
void testMaxAgeIsSetFromJwtCookieSettings(@Client("/") HttpClient httpClient) {
BlockingHttpClient client = httpClient.toBlocking();
HttpRequest<?> loginRequest = HttpRequest.POST("/login", Map.of("username","sherlock","password","password"))
.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);

HttpResponse<?> loginRsp = client.exchange(loginRequest, String.class);

String cookie = loginRsp.getHeaders().get("Set-Cookie");
assertFalse(cookie.contains("Max-Age="));
assertFalse(cookie.contains("Expires="));
}

@Requires(property = "spec.name", value = "JwtCookieSessionCookeTest")
@Singleton
static class AuthProvider<B> implements HttpRequestExecutorAuthenticationProvider<B> {

@Override
public AuthenticationResponse authenticate(HttpRequest<B> requestContext, AuthenticationRequest<String, String> authRequest) {
return AuthenticationResponse.success("sherlock");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public abstract class AbstractCookieConfiguration implements CookieConfiguration

private static final boolean DEFAULT_HTTPONLY = true;
private static final String DEFAULT_COOKIEPATH = "/";

private static final Duration DEFAULT_MAX_AGE = Duration.ofMinutes(5);

protected String cookieDomain;
Expand All @@ -42,6 +41,26 @@ public abstract class AbstractCookieConfiguration implements CookieConfiguration
protected Boolean cookieHttpOnly = DEFAULT_HTTPONLY;
protected Duration cookieMaxAge = DEFAULT_MAX_AGE;
protected String cookieName = null;
protected boolean sessionCookie;

/**
* 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).
* @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.
* @since 4.12.0
*/
public boolean isSessionCookie() {
return sessionCookie;
}

/**
* 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).
*
* @param sessionCookie Whether the cookie is a session cookie.
* @since 4.12.0
*/
public void setSessionCookie(boolean sessionCookie) {
this.sessionCookie = sessionCookie;
}

@Override
public Optional<String> getCookieDomain() {
Expand Down Expand Up @@ -121,6 +140,9 @@ public void setCookieHttpOnly(Boolean cookieHttpOnly) {

@Override
public Optional<TemporalAmount> getCookieMaxAge() {
if (isSessionCookie()) {
return Optional.empty();
}
return Optional.ofNullable(cookieMaxAge);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ public List<Cookie> getCookies(Authentication authentication, HttpRequest<?> req

Cookie jwtCookie = Cookie.of(accessTokenCookieConfiguration.getCookieName(), accessToken);
jwtCookie.configure(accessTokenCookieConfiguration, request.isSecure());
jwtCookie.maxAge(cookieExpiration(authentication, request));
if (!accessTokenCookieConfiguration.isSessionCookie()) {
jwtCookie.maxAge(cookieExpiration(authentication, request));
}
cookies.add(jwtCookie);
for (LoginCookieProvider<HttpRequest<?>> loginCookieProvider : loginCookieProviders) {
cookies.add(loginCookieProvider.provideCookie(request));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@
* @since 2.0.0
*/
public interface TokenCookieConfiguration extends CookieConfiguration, Toggleable {
/**
*
* @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.
* @since 4.12.0
*/
default boolean isSessionCookie() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ public abstract class AbstractAccessTokenCookieConfigurationProperties implement
protected Boolean cookieSecure;
protected Duration cookieMaxAge;
protected SameSite cookieSameSite = DEFAULT_COOKIESAMESITE;
protected boolean sessionCookie;

@Override
public boolean isSessionCookie() {
return sessionCookie;
}

/**
* 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).
*
* @param sessionCookie Whether the cookie is a session cookie.
* @since 4.12.0
*/
public void setSessionCookie(boolean sessionCookie) {
this.sessionCookie = sessionCookie;
}

/**
*
Expand Down Expand Up @@ -80,6 +96,9 @@ public Optional<Boolean> isCookieSecure() {
*/
@Override
public Optional<TemporalAmount> getCookieMaxAge() {
if (isSessionCookie()) {
return Optional.empty();
}
return Optional.ofNullable(cookieMaxAge);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@ protected List<Cookie> getCookies(AccessRefreshToken accessRefreshToken, HttpReq
protected Cookie accessTokenCookie(@NonNull AccessRefreshToken accessRefreshToken, @NonNull HttpRequest<?> request) {
Cookie jwtCookie = Cookie.of(accessTokenCookieConfiguration.getCookieName(), accessRefreshToken.getAccessToken());
jwtCookie.configure(accessTokenCookieConfiguration, request.isSecure());
TemporalAmount maxAge = accessTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofSeconds(accessTokenConfiguration.getExpiration()));
jwtCookie.maxAge(maxAge);
if (!accessTokenCookieConfiguration.isSessionCookie()) {
TemporalAmount maxAge = accessTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofSeconds(accessTokenConfiguration.getExpiration()));
jwtCookie.maxAge(maxAge);
}
return jwtCookie;
}

Expand All @@ -166,7 +168,9 @@ protected Optional<Cookie> refreshTokenCookie(@NonNull AccessRefreshToken access
}
Cookie refreshCookie = Cookie.of(refreshTokenCookieConfiguration.getCookieName(), refreshToken);
refreshCookie.configure(refreshTokenCookieConfiguration, request.isSecure());
refreshCookie.maxAge(refreshTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofDays(30)));
if (!refreshTokenCookieConfiguration.isSessionCookie()) {
refreshCookie.maxAge(refreshTokenCookieConfiguration.getCookieMaxAge().orElseGet(() -> Duration.ofDays(30)));
}
return Optional.of(refreshCookie);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.micronaut.security.config;

import io.micronaut.context.ApplicationContext;
import io.micronaut.core.util.StringUtils;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest(startApplication = false)
class TokenCookieConfigurationTest {

@Test
void iSessionCookieDefaultsToFalse() {
try (ApplicationContext ctx = ApplicationContext.run(Map.of("micronaut.security.authentication", "cookie",
"micronaut.security.token.cookie.cookie-max-age", "5m"
))) {
TokenCookieConfiguration tokenCookieConfiguration = ctx.getBean(TokenCookieConfiguration.class);
assertFalse(tokenCookieConfiguration.isSessionCookie());
assertTrue(tokenCookieConfiguration.getCookieMaxAge().isPresent());
}

try (ApplicationContext ctx = ApplicationContext.run(Map.of("micronaut.security.authentication", "cookie",
"micronaut.security.token.cookie.session-cookie", StringUtils.TRUE,
"micronaut.security.token.cookie.cookie-max-age", "5m"
))) {
TokenCookieConfiguration tokenCookieConfiguration = ctx.getBean(TokenCookieConfiguration.class);
assertTrue(tokenCookieConfiguration.isSessionCookie());
// by setting session-cookie to true, the cookie-max-age should be ignored
assertFalse(tokenCookieConfiguration.getCookieMaxAge().isPresent());
}

}
}
Loading