Skip to content
Draft
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: 2 additions & 0 deletions .agents/skills/coding/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Should not trigger:
- Preserve existing nullability intent when editing older code. Use JSpecify for new or modified contracts, but do not rewrite deliberate compatibility annotations such as `io.micronaut.core.annotation.Nullable` or `jakarta.annotation.Nullable` unless the task is specifically a nullability migration and compatibility impact has been checked.
- Avoid reflection-oriented implementations in framework code paths; prefer Micronaut compile-time/introspection mechanisms.
- Use `jakarta.inject` APIs for DI, not `javax.inject`.
- Do not introduce wildcard imports such as `import io.micronaut.http.*`; import referenced types explicitly.
- Prefer constructor injection and immutable state over field injection.
- For configuration models, prefer `@ConfigurationProperties` over scattered `@Value` usage.

Expand Down Expand Up @@ -111,6 +112,7 @@ If Spotless fails, run `./gradlew -q spotlessApply` and re-run `spotlessCheck`.
## Guardrails

- Do not introduce `javax.inject` usage.
- Do not introduce wildcard imports (`*`) in Java or Kotlin source.
- Do not introduce legacy or third-party nullability annotations for new or modified Micronaut Java contracts when JSpecify is available, except for deliberate compatibility annotations whose impact has been checked.
- Do not hard-code dependency versions in module build files.
- Do not break public APIs without explicit major-version intent.
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
managed-nimbus-jose-jwt = "10.9"
managed-jjwt = "0.13.0"
micronaut = "5.0.0"
micronaut-platform = "5.0.0-RC1"
micronaut-platform = "5.0.0"
awaitility = "4.3.0"
geb = "8.0.1"
selenium = "4.41.0"
Expand Down
1 change: 1 addition & 0 deletions security-csrf/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
id("io.micronaut.build.internal.security-module")
}

dependencies {

Check warning on line 5 in security-csrf/build.gradle.kts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Group dependencies by their destination.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-security&issues=AZ5J5eUy6942M8tMnWlj&open=AZ5J5eUy6942M8tMnWlj&pullRequest=2189
api(projects.micronautSecurity)
compileOnly(mn.micronaut.http.server)
compileOnly(projects.micronautSecuritySession)
Expand All @@ -14,6 +14,7 @@
testImplementation(mn.micronaut.http.client)
testAnnotationProcessor(mnSerde.micronaut.serde.processor)
testImplementation(mnSerde.micronaut.serde.jackson)
testImplementation(projects.testSuiteUtils)
testImplementation(projects.testSuiteUtilsSecurity)
testImplementation(projects.micronautSecurityJwt)
testImplementation(projects.micronautSecuritySession)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.bind.annotation.Bindable;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import io.micronaut.http.cookie.SameSite;
Expand Down Expand Up @@ -62,6 +63,7 @@ final class CsrfConfigurationProperties implements CsrfConfiguration {
public static final int DEFAULT_RANDOM_VALUE_SIZE = 16;

public static final boolean DEFAULT_ENABLED = true;
public static final boolean DEFAULT_SESSION_COOKIE = false;

private static final boolean DEFAULT_HTTPONLY = true;
private static final String DEFAULT_COOKIEPATH = "/";
Expand Down Expand Up @@ -94,11 +96,12 @@ public boolean isSessionCookie() {
}

/**
* 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).
* 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: {@value #DEFAULT_SESSION_COOKIE}.
*
* @param sessionCookie Whether the cookie is a session cookie.
* @since 4.12.0
*/
@Bindable(defaultValue = "" + DEFAULT_SESSION_COOKIE)
public void setSessionCookie(boolean sessionCookie) {
this.sessionCookie = sessionCookie;
}
Expand Down Expand Up @@ -127,6 +130,7 @@ public String getHttpSessionName() {
* Key to look for the CSRF token in an HTTP Session. Default Value: {@value #DEFAULT_HTTP_SESSION_NAME}.
* @param httpSessionName Key to look for the CSRF token in an HTTP Session.
*/
@Bindable(defaultValue = DEFAULT_HTTP_SESSION_NAME)
public void setHttpSessionName(@NonNull String httpSessionName) {
this.httpSessionName = httpSessionName;
}
Expand All @@ -140,6 +144,7 @@ public int getRandomValueSize() {
* Random value's size in bytes. The random value used is used to build a CSRF Token. Default Value: {@value #DEFAULT_RANDOM_VALUE_SIZE}.
* @param randomValueSize Random CSRF Token size in bytes.
*/
@Bindable(defaultValue = "" + DEFAULT_RANDOM_VALUE_SIZE)
public void setRandomValueSize(int randomValueSize) {
this.randomValueSize = randomValueSize;
}
Expand All @@ -154,6 +159,7 @@ public String getHeaderName() {
* HTTP Header name to look for the CSRF token. Default Value: {@value #DEFAULT_HTTP_HEADER_NAME}.
* @param headerName HTTP Header name to look for the CSRF token.
*/
@Bindable(defaultValue = DEFAULT_HTTP_HEADER_NAME)
public void setHeaderName(@NonNull String headerName) {
this.headerName = headerName;
}
Expand All @@ -168,6 +174,7 @@ public String getFieldName() {
* Field name in a form url encoded submission to look for the CSRF token. Default Value: {@value #DEFAULT_FIELD_NAME}.
* @param fieldName Field name in a form url encoded submission to look for the CSRF token.
*/
@Bindable(defaultValue = DEFAULT_FIELD_NAME)
public void setFieldName(@NonNull String fieldName) {
this.fieldName = fieldName;
}
Expand All @@ -181,6 +188,7 @@ public boolean isEnabled() {
* Whether the CSRF integration is enabled. Default value {@value #DEFAULT_ENABLED}.
* @param enabled Whether the CSRF integration is enabled
*/
@Bindable(defaultValue = "" + DEFAULT_ENABLED)
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
Expand Down Expand Up @@ -209,6 +217,7 @@ public Optional<Boolean> isCookieSecure() {
*
* @param cookieSecure True if the cookie is secure
*/
@Bindable(defaultValue = "" + true)
public void setCookieSecure(Boolean cookieSecure) {
this.cookieSecure = cookieSecure;
}
Expand All @@ -220,10 +229,11 @@ public String getCookieName() {
}

/**
* Cookie Name.
* Cookie Name. Default value ({@value #DEFAULT_COOKIE_NAME}).
*
* @param cookieName Cookie name
*/
@Bindable(defaultValue = DEFAULT_COOKIE_NAME)
public void setCookieName(@NonNull String cookieName) {
this.cookieName = cookieName;
}
Expand All @@ -238,6 +248,7 @@ public Optional<String> getCookiePath() {
*
* @param cookiePath The path of the cookie.
*/
@Bindable(defaultValue = DEFAULT_COOKIEPATH)
public void setCookiePath(@Nullable String cookiePath) {
this.cookiePath = cookiePath;
}
Expand All @@ -252,6 +263,7 @@ public Optional<Boolean> isCookieHttpOnly() {
*
* @param cookieHttpOnly Whether the Cookie can only be accessed via HTTP
*/
@Bindable(defaultValue = "" + DEFAULT_HTTPONLY)
public void setCookieHttpOnly(Boolean cookieHttpOnly) {
this.cookieHttpOnly = cookieHttpOnly;
}
Expand All @@ -269,6 +281,7 @@ public Optional<TemporalAmount> getCookieMaxAge() {
*
* @param cookieMaxAge The maximum age of the cookie
*/
@Bindable(defaultValue = "" + AccessTokenConfigurationProperties.DEFAULT_EXPIRATION)
public void setCookieMaxAge(Duration cookieMaxAge) {
this.cookieMaxAge = cookieMaxAge;
}
Expand All @@ -282,6 +295,7 @@ public Optional<SameSite> getCookieSameSite() {
* Cookie Same Site Configuration. It defaults to Strict.
* @param sameSite Same Site Configuration
*/
@Bindable(defaultValue = "Strict")
public void setCookieSameSite(SameSite sameSite) {
this.sameSite = sameSite;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.bind.annotation.Bindable;
import org.jspecify.annotations.NonNull;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.MediaType;
Expand Down Expand Up @@ -69,6 +70,7 @@ public Set<HttpMethod> getMethods() {
* Filter will only process requests whose method matches any of these methods. Default Value is POST, PUT, DELETE, PATCH.
* @param methods HTTP methods.
*/
@Bindable(defaultValue = "POST,PUT,DELETE,PATCH")
public void setMethods(@NonNull Set<HttpMethod> methods) {
this.methods = methods;
}
Expand All @@ -83,6 +85,7 @@ public Set<MediaType> getContentTypes() {
* Filter will only process requests whose content type matches any of these content types. Default Value is application/x-www-form-urlencoded, multipart/form-data.
* @param contentTypes Content Types
*/
@Bindable(defaultValue = "application/x-www-form-urlencoded,multipart/form-data")
public void setContentTypes(@NonNull Set<MediaType> contentTypes) {
this.contentTypes = contentTypes;
}
Expand All @@ -96,6 +99,7 @@ public boolean isEnabled() {
* Whether the filter is enabled. Default value {@value #DEFAULT_ENABLED}.
* @param enabled Whether the filter is enabled.
*/
@Bindable(defaultValue = "" + DEFAULT_ENABLED)
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
Expand All @@ -109,6 +113,7 @@ public String getRegexPattern() {
* CSRF filter processes only request paths matching this regular expression. Default Value: {@value #DEFAULT_REGEX_PATTERN}
* @param regexPattern Regular expression pattern for the filter.
*/
@Bindable(defaultValue = DEFAULT_REGEX_PATTERN)
public void setRegexPattern(String regexPattern) {
this.regexPattern = regexPattern;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.micronaut.security.csrf;

import io.micronaut.security.token.generator.AccessTokenConfigurationProperties;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import static io.micronaut.security.testutils.ConfigurationSchemaUtils.assertDefaults;
import static io.micronaut.security.testutils.ConfigurationSchemaUtils.assertProperty;
import static io.micronaut.security.testutils.ConfigurationSchemaUtils.assertSchemaMetadata;
import static io.micronaut.security.testutils.ConfigurationSchemaUtils.map;
import static io.micronaut.security.testutils.ConfigurationSchemaUtils.properties;
import static io.micronaut.security.testutils.ConfigurationSchemaUtils.schema;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

class CsrfConfigurationPropertiesSchemaTest {
private static final String CONFIGURATION_PROPERTIES_TYPE = "io.micronaut.security.csrf.CsrfConfigurationProperties";
private static final String FILTER_CONFIGURATION_PROPERTIES_TYPE =
"io.micronaut.security.csrf.filter.CsrfFilterConfigurationProperties";

@Test
void generatedConfigurationSchemaContainsDefaultsFromBindableSetters() throws IOException {
Map<String, Object> schema = schema(CONFIGURATION_PROPERTIES_TYPE);

assertSchemaMetadata(schema, CONFIGURATION_PROPERTIES_TYPE, CsrfConfiguration.PREFIX, "configuration-properties");

Map<String, Object> properties = map(schema, "properties");
assertCsrfProperty(properties, "session-cookie", "boolean", "boolean", CsrfConfigurationProperties.DEFAULT_SESSION_COOKIE);
assertCsrfProperty(properties, "http-session-name", "string", "java.lang.String",
CsrfConfigurationProperties.DEFAULT_HTTP_SESSION_NAME);
assertCsrfProperty(properties, "random-value-size", "integer", "int", CsrfConfigurationProperties.DEFAULT_RANDOM_VALUE_SIZE);
assertCsrfProperty(properties, "header-name", "string", "java.lang.String", CsrfConfigurationProperties.DEFAULT_HTTP_HEADER_NAME);
assertCsrfProperty(properties, "field-name", "string", "java.lang.String", CsrfConfigurationProperties.DEFAULT_FIELD_NAME);
assertCsrfProperty(properties, "enabled", "boolean", "boolean", CsrfConfigurationProperties.DEFAULT_ENABLED);
assertCsrfProperty(properties, "cookie-secure", "boolean", "java.lang.Boolean", true);
assertCsrfProperty(properties, "cookie-name", "string", "java.lang.String", CsrfConfigurationProperties.DEFAULT_COOKIE_NAME);
assertCsrfProperty(properties, "cookie-path", "string", "java.lang.String", "/");
assertCsrfProperty(properties, "cookie-http-only", "boolean", "java.lang.Boolean", true);
assertCsrfProperty(properties, "cookie-max-age", "string", "java.time.Duration",
String.valueOf(AccessTokenConfigurationProperties.DEFAULT_EXPIRATION));
assertCsrfProperty(properties, "cookie-same-site", "string", "io.micronaut.http.cookie.SameSite", "Strict");

Map<String, Object> cookieMaxAge = map(properties, "cookie-max-age");
assertEquals("duration", cookieMaxAge.get("format"));

Map<String, Object> cookieSameSite = map(properties, "cookie-same-site");
assertEquals(List.of("Lax", "Strict", "None"), cookieSameSite.get("enum"));

assertFalse(map(properties, "signature-key").containsKey("default"));
assertFalse(map(properties, "cookie-domain").containsKey("default"));
}

@Test
void generatedFilterConfigurationSchemaContainsDefaultsFromBindableSetters() throws IOException {
assertDefaults(FILTER_CONFIGURATION_PROPERTIES_TYPE, Map.of(
"methods", "POST,PUT,DELETE,PATCH",
"content-types", "application/x-www-form-urlencoded,multipart/form-data",
"enabled", true,
"regex-pattern", "^.*$"
));

Map<String, Object> properties = properties(FILTER_CONFIGURATION_PROPERTIES_TYPE);
Map<String, Object> methods = map(properties, "methods");
assertEquals("array", methods.get("type"));
assertEquals("java.util.Set", methods.get("x-micronaut-javaType"));
assertEquals(List.of("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "PATCH", "CUSTOM"),
map(methods, "items").get("enum"));

Map<String, Object> contentTypes = map(properties, "content-types");
assertEquals("array", contentTypes.get("type"));
assertEquals("java.util.Set", contentTypes.get("x-micronaut-javaType"));
assertEquals("string", map(contentTypes, "items").get("type"));
}

private static void assertCsrfProperty(Map<String, Object> properties,
String name,
String type,
String javaType,
Object defaultValue) {
assertProperty(properties, name, type, javaType, defaultValue, CONFIGURATION_PROPERTIES_TYPE, CsrfConfiguration.PREFIX);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.util.StringUtils;
import io.micronaut.security.token.config.TokenConfigurationProperties;

Expand Down Expand Up @@ -49,6 +50,7 @@ public boolean isEnabled() {
* Sets whether JWT security is enabled. Default value ({@value #DEFAULT_ENABLED}).
* @param enabled True if it is
*/
@Bindable(defaultValue = "" + DEFAULT_ENABLED)
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.nimbusds.jose.JWEAlgorithm;
import io.micronaut.context.annotation.EachProperty;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.security.token.jwt.config.JwtConfigurationProperties;

/**
Expand Down Expand Up @@ -108,6 +109,7 @@ public boolean isBase64() {
*
* @param base64 boolean flag indicating whether the supplied secret is base64 encoded
*/
@Bindable(defaultValue = "" + DEFAULT_BASE64)
public void setBase64(boolean base64) {
this.base64 = base64;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import org.jspecify.annotations.NonNull;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.util.StringUtils;
import io.micronaut.security.config.SecurityConfigurationProperties;
import org.jspecify.annotations.NonNull;

/**
* Configures the {@link io.micronaut.security.token.jwt.endpoints.KeysController}.
Expand Down Expand Up @@ -66,6 +67,7 @@ public String getPath() {
* Enables {@link io.micronaut.security.token.jwt.endpoints.KeysController}. Default value {@value #DEFAULT_ENABLED}.
* @param enabled True if it is enabled
*/
@Bindable(defaultValue = "" + DEFAULT_ENABLED)
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
Expand All @@ -74,6 +76,7 @@ public void setEnabled(boolean enabled) {
* Path to the {@link io.micronaut.security.token.jwt.endpoints.KeysController}. Default value {@value #DEFAULT_PATH}.
* @param path The path
*/
@Bindable(defaultValue = DEFAULT_PATH)
public void setPath(String path) {
if (StringUtils.isNotEmpty(path)) {
this.path = path;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Introspected;
import org.jspecify.annotations.NonNull;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.util.StringUtils;
import io.micronaut.security.token.jwt.config.JwtConfigurationProperties;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.jspecify.annotations.NonNull;

/**
* {@link ConfigurationProperties} implementation of {@link RefreshTokenConfiguration} to configure {@link SignedRefreshTokenGenerator}.
Expand Down Expand Up @@ -75,6 +76,7 @@ public class RefreshTokenConfigurationProperties implements RefreshTokenConfigur
*
* @param enabled True if it is enabled
*/
@Bindable(defaultValue = "" + DEFAULT_ENABLED)
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
Expand All @@ -84,6 +86,7 @@ public void setEnabled(boolean enabled) {
*
* @param jwsAlgorithm JWS Algorithm
*/
@Bindable(defaultValue = "HS256")
public void setJwsAlgorithm(@NonNull JWSAlgorithm jwsAlgorithm) {
this.jwsAlgorithm = jwsAlgorithm;
}
Expand All @@ -100,6 +103,7 @@ public void setSecret(@NonNull String secret) {
*
* @param base64 boolean flag indicating whether the supplied secret is base64 encoded
*/
@Bindable(defaultValue = "" + DEFAULT_BASE64)
public void setBase64(boolean base64) {
this.base64 = base64;
}
Expand Down
Loading
Loading