From 3cf842a0669af78aee97b0dccdb6193632cd8463 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 15:32:05 +0200 Subject: [PATCH 001/108] add security-csrf module --- security-csrf/build.gradle.kts | 0 settings.gradle | 1 + 2 files changed, 1 insertion(+) create mode 100644 security-csrf/build.gradle.kts diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/settings.gradle b/settings.gradle index 4f66455fdd..3d3d422353 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,7 @@ include "security-bom" include "security" include "security-aot" include "security-annotations" +include "security-csrf" include "security-jwt" include "security-session" include "security-ldap" From 656913bcb003e89080342604e8ec0bd68bebadb3 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 15:33:38 +0200 Subject: [PATCH 002/108] build: apply security module --- security-csrf/build.gradle.kts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index e69de29bb2..8ee62f5207 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("io.micronaut.build.internal.security-module") +} + +dependencies { +} + +micronautBuild { + binaryCompatibility.enabled = false +} \ No newline at end of file From e42005a0e69b05021c99cf3734996d77faa708c4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 15:34:44 +0200 Subject: [PATCH 003/108] add logback.xml to src/test/resources --- security-csrf/src/test/resources/logback.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 security-csrf/src/test/resources/logback.xml diff --git a/security-csrf/src/test/resources/logback.xml b/security-csrf/src/test/resources/logback.xml new file mode 100644 index 0000000000..432f5aa24e --- /dev/null +++ b/security-csrf/src/test/resources/logback.xml @@ -0,0 +1,10 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + \ No newline at end of file From 865b2c616a9e2d24195df5a8d6c81cae480f0648 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 15:53:22 +0200 Subject: [PATCH 004/108] csrf configuration --- security-csrf/build.gradle.kts | 10 +++ .../security/csrf/CsrfConfiguration.java | 41 ++++++++++ .../csrf/CsrfConfigurationProperties.java | 79 +++++++++++++++++++ .../micronaut/security/csrf/package-info.java | 28 +++++++ .../security/csrf/CsrfConfigurationTest.java | 29 +++++++ src/main/docs/guide/csrf.adoc | 4 + src/main/docs/guide/toc.yml | 2 + 7 files changed, 193 insertions(+) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java create mode 100644 src/main/docs/guide/csrf.adoc diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index 8ee62f5207..2d43df16b9 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -3,6 +3,16 @@ plugins { } dependencies { + api(projects.micronautSecurity) + + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(mnTest.micronaut.test.junit5) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(mnLogging.logback.classic) +} + +tasks.withType { + useJUnitPlatform() } micronautBuild { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java new file mode 100644 index 0000000000..d19ae20a78 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.Toggleable; + +/** + * CSRF Configuration. + * @author Sergio del Amo + * @since 4.11.0 + */ +public interface CsrfConfiguration extends Toggleable { + + /** + * + * @return HTTP Header name to look for the CSRF token. + */ + @NonNull + String getHeaderName(); + + /** + * + * @return Field name in a form url encoded submission to look for the CSRF token. + */ + @NonNull + String getFieldName(); +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java new file mode 100644 index 0000000000..fe6192c9c0 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -0,0 +1,79 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.security.config.SecurityConfigurationProperties; + +@Internal +@ConfigurationProperties(CsrfConfigurationProperties.PREFIX) +class CsrfConfigurationProperties implements CsrfConfiguration { + public static final String PREFIX = SecurityConfigurationProperties.PREFIX + ".csrf"; + + public static final String DEFAULT_HTTP_HEADER_NAME = "X-CSRF-TOKEN"; + + public static final String DEFAULT_FIELD_NAME = "csrfToken"; + + public static final boolean DEFAULT_ENABLED = true; + + private boolean enabled = DEFAULT_ENABLED; + + private String headerName = DEFAULT_HTTP_HEADER_NAME; + + private String fieldName = DEFAULT_FIELD_NAME; + + @Override + @NonNull + public String getHeaderName() { + return headerName; + } + + /** + * 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. + */ + public void setHeaderName(@NonNull String headerName) { + this.headerName = headerName; + } + + @Override + public String getFieldName() { + return fieldName; + } + + /** + * 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. + */ + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + /** + * Whether the CSRF integration is enabled. Default value {@value #DEFAULT_ENABLED}. + * @param enabled Whether the CSRF integration is enabled + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java new file mode 100644 index 0000000000..30d39af669 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes related to Cross Site Request Forgery (CSRF). + * @see Cross Site Request Forgery (CSRF) + * @author Sergio del Amo + * @since 4.11.0 + */ +@Requires(property = CsrfConfigurationProperties.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Configuration +package io.micronaut.security.csrf; + +import io.micronaut.context.annotation.Configuration; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java new file mode 100644 index 0000000000..5e0c773a2f --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java @@ -0,0 +1,29 @@ +package io.micronaut.security.csrf; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@MicronautTest(startApplication = false) +class CsrfConfigurationTest { + + @Inject + CsrfConfiguration csrfConfiguration; + + @Test + void defaultHeaderName() { + assertEquals("X-CSRF-TOKEN", csrfConfiguration.getHeaderName()); + } + + @Test + void defaultFieldName() { + assertEquals("csrfToken", csrfConfiguration.getFieldName()); + } + + @Test + void defaultEnabled() { + assertTrue(csrfConfiguration.isEnabled()); + } +} \ No newline at end of file diff --git a/src/main/docs/guide/csrf.adoc b/src/main/docs/guide/csrf.adoc new file mode 100644 index 0000000000..aed0bfcec5 --- /dev/null +++ b/src/main/docs/guide/csrf.adoc @@ -0,0 +1,4 @@ +The following configuration options are available for CSRF: + +include::{includedir}configurationProperties/io.micronaut.security.csrf.CsrfConfigurationProperties.adoc[] + diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index e7a724eafd..209ae136e9 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -83,6 +83,8 @@ authenticationStrategies: x509: X.509 Certificate Authentication custom: Custom Authorization Strategies rejection: Rejection Handling +csrf: + title: Cross-Site Request Forgery (CSRF) tokenPropagation: Token Propagation tokenendpoints: title: Built-In Security Token Controllers From 37cf4f8b4a156c2ec0e029c7cc281f22cdf8cc34 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 16:10:16 +0200 Subject: [PATCH 005/108] CSRF Filter configuration --- security-csrf/build.gradle.kts | 1 + .../csrf/filter/CsrfFilterConfiguration.java | 51 ++++++++ .../CsrfFilterConfigurationProperties.java | 114 ++++++++++++++++++ .../security/csrf/filter/package-info.java | 21 ++++ .../micronaut/security/csrf/package-info.java | 2 +- src/main/docs/guide/csrf/csrfFilter.adoc | 4 + src/main/docs/guide/toc.yml | 1 + 7 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/filter/package-info.java create mode 100644 src/main/docs/guide/csrf/csrfFilter.adoc diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index 2d43df16b9..d478ba8446 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { api(projects.micronautSecurity) + compileOnly(mn.micronaut.http) testAnnotationProcessor(mn.micronaut.inject.java) testImplementation(mnTest.micronaut.test.junit5) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java new file mode 100644 index 0000000000..afb85008cb --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.filter; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.Toggleable; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.MediaType; + +import java.util.Set; + +/** + * @author Sergio del Amo + * @since 4.11.0 + */ +public interface CsrfFilterConfiguration extends Toggleable { + + /** + * + * @return Regular expression pattern. Filter will only process requests whose path matches this pattern. + */ + @NonNull + String getRegexPattern(); + + /** + * + * @return HTTP methods. Filter will only process requests whose method matches any of these methods. + */ + @NonNull + Set getMethods(); + + /** + * + * @return HTTP methods. Filter will only process requests whose content type matches any of these content types. + */ + @NonNull + Set getContentTypes(); +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java new file mode 100644 index 0000000000..90749544f2 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.filter; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.MediaType; +import io.micronaut.security.config.SecurityConfigurationProperties; +import java.util.Set; + +@Requires(classes = { HttpMethod.class, MediaType.class}) +@Internal +@ConfigurationProperties(CsrfFilterConfigurationProperties.PREFIX) +final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration { + public static final String PREFIX = SecurityConfigurationProperties.PREFIX + "csrf.filter"; + + /** + * The default enable value. + */ + @SuppressWarnings("WeakerAccess") + public static final boolean DEFAULT_ENABLED = true; + + /** + * The default regex pattern. + */ + @SuppressWarnings("WeakerAccess") + public static final String DEFAULT_REGEX_PATTERN = "^.*$"; + + private static final Set DEFAULT_METHODS = Set.of( + HttpMethod.POST, + HttpMethod.PUT, + HttpMethod.DELETE, + HttpMethod.PATCH + ); + private static final Set DEFAULT_CONTENT_TYPES = Set.of( + MediaType.APPLICATION_FORM_URLENCODED_TYPE, + MediaType.MULTIPART_FORM_DATA_TYPE + ); + + private boolean enabled = DEFAULT_ENABLED; + private String regexPattern = DEFAULT_REGEX_PATTERN; + private Set methods = DEFAULT_METHODS; + private Set contentTypes = DEFAULT_CONTENT_TYPES; + + @Override + @NonNull + public Set getMethods() { + return methods; + } + + /** + * Filter will only process requests whose method matches any of these methods. Default Value is POST, PUT, DELETE, PATCH. + * @param methods HTTP methods. + */ + public void setMethods(@NonNull Set methods) { + this.methods = methods; + } + + @Override + @NonNull + public Set getContentTypes() { + return contentTypes; + } + + /** + * 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 + */ + public void setContentTypes(@NonNull Set contentTypes) { + this.contentTypes = contentTypes; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + /** + * Whether the filter is enabled. Default value {@value #DEFAULT_ENABLED}. + * @param enabled Whether the filter is enabled. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getRegexPattern() { + return regexPattern; + } + + /** + * Filter will only process requests whose path matches this pattern. Default Value {@value #DEFAULT_REGEX_PATTERN}. + * @param regexPattern Regular expression pattern for the filter. + */ + public void setRegexPattern(String regexPattern) { + this.regexPattern = regexPattern; + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/package-info.java new file mode 100644 index 0000000000..d414412654 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes related to the CSRF Filter. + * @author Sergio del Amo + * @since 4.11.0 + */ +package io.micronaut.security.csrf.filter; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java index 30d39af669..5ab9264a0e 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java @@ -25,4 +25,4 @@ import io.micronaut.context.annotation.Configuration; import io.micronaut.context.annotation.Requires; -import io.micronaut.core.util.StringUtils; \ No newline at end of file +import io.micronaut.core.util.StringUtils; diff --git a/src/main/docs/guide/csrf/csrfFilter.adoc b/src/main/docs/guide/csrf/csrfFilter.adoc new file mode 100644 index 0000000000..42b3e5de99 --- /dev/null +++ b/src/main/docs/guide/csrf/csrfFilter.adoc @@ -0,0 +1,4 @@ +The following configuration options are available for CSRF: + +include::{includedir}configurationProperties/io.micronaut.security.csrf.filter.CsrfFilterConfigurationProperties.adoc[] + diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 209ae136e9..5ef1eeb705 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -85,6 +85,7 @@ authenticationStrategies: rejection: Rejection Handling csrf: title: Cross-Site Request Forgery (CSRF) + csrfFilter: CSRF Filter tokenPropagation: Token Propagation tokenendpoints: title: Built-In Security Token Controllers From 6f0878ecd2f2ad7b120e658ff7a9cbf7811a8ea9 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 16:13:01 +0200 Subject: [PATCH 006/108] add test --- security-csrf/build.gradle.kts | 1 + .../CsrfFilterConfigurationProperties.java | 2 +- .../filter/CsrfFilterConfigurationTest.java | 34 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index d478ba8446..ca149645bb 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { testImplementation(mnTest.micronaut.test.junit5) testRuntimeOnly(libs.junit.jupiter.engine) testRuntimeOnly(mnLogging.logback.classic) + testImplementation(mn.micronaut.http.server.netty) } tasks.withType { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java index 90749544f2..821b0e3a5d 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java @@ -24,7 +24,7 @@ import io.micronaut.security.config.SecurityConfigurationProperties; import java.util.Set; -@Requires(classes = { HttpMethod.class, MediaType.class}) +@Requires(classes = { HttpMethod.class, MediaType.class }) @Internal @ConfigurationProperties(CsrfFilterConfigurationProperties.PREFIX) final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration { diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java new file mode 100644 index 0000000000..9e784a4f20 --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java @@ -0,0 +1,34 @@ +package io.micronaut.security.csrf.filter; + +import io.micronaut.http.HttpMethod; +import io.micronaut.http.MediaType; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest(startApplication = false) +class CsrfFilterConfigurationTest { + + @Inject + CsrfFilterConfiguration csrfFilterConfiguration; + + @Test + void defaultMethods() { + assertEquals(Set.of(HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PUT, HttpMethod.PATCH), csrfFilterConfiguration.getMethods()); + } + + @Test + void defaultContentType() { + assertEquals(Set.of(MediaType.APPLICATION_FORM_URLENCODED_TYPE, MediaType.MULTIPART_FORM_DATA_TYPE), csrfFilterConfiguration.getContentTypes()); + } + + @Test + void defaultEnabled() { + assertTrue(csrfFilterConfiguration.isEnabled()); + } +} \ No newline at end of file From 97e3d6e21f3f34e5f1141032f713998d880d8662 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 16:27:46 +0200 Subject: [PATCH 007/108] Http Header Csrf Token Resolver --- security-csrf/build.gradle.kts | 3 + .../csrf/resolver/CsrfTokenResolver.java | 39 +++++++++++ .../resolver/HttpHeaderCsrfTokenResolver.java | 54 +++++++++++++++ ...tpHeaderCsrfTokenResolverDisabledTest.java | 24 +++++++ .../HttpHeaderCsrfTokenResolverTest.java | 68 +++++++++++++++++++ .../docs/guide/csrf/csrfTokenResolvers.adoc | 6 ++ src/main/docs/guide/toc.yml | 1 + 7 files changed, 195 insertions(+) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverDisabledTest.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverTest.java create mode 100644 src/main/docs/guide/csrf/csrfTokenResolvers.adoc diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index ca149645bb..f4ded239df 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -11,6 +11,9 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) testRuntimeOnly(mnLogging.logback.classic) testImplementation(mn.micronaut.http.server.netty) + testImplementation(mn.micronaut.http.client) + testAnnotationProcessor(mnSerde.micronaut.serde.processor) + testImplementation(mnSerde.micronaut.serde.jackson) } tasks.withType { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java new file mode 100644 index 0000000000..65c967b313 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.order.Ordered; + +import java.util.Optional; + +/** + * Attempts to resolve a CSRF token from the provided request. + * + * @author Sergio del Amo + * @since 1.1.0 + * @param request + */ +public interface CsrfTokenResolver extends Ordered { + + /** + * + * @param request The Request. Maybe an HTTP Request. + * @return A CSRF token or an empty Optional if the token cannot be resolved. + */ + @NonNull + Optional resolveToken(T request); +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java new file mode 100644 index 0000000000..48d9bdd892 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.security.csrf.CsrfConfiguration; +import jakarta.inject.Singleton; + +import java.util.Optional; + +/** + * Resolves a CSRF token from a request HTTP Header named {@link CsrfConfiguration#getHeaderName()}. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Requires(property = "micronaut.security.csrf.token-resolvers.http-header.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Singleton +@Internal +final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver> { + private final CsrfConfiguration csrfConfiguration; + + HttpHeaderCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { + this.csrfConfiguration = csrfConfiguration; + } + + @Override + public Optional resolveToken(HttpRequest request) { + String csrfToken = request.getHeaders().get(csrfConfiguration.getHeaderName()); + if (csrfToken != null) { + return Optional.of(csrfToken); + } + csrfToken = request.getHeaders().get(csrfConfiguration.getHeaderName().toLowerCase()); + if (csrfToken != null) { + return Optional.of(csrfToken); + } + return Optional.empty(); + } +} diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverDisabledTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverDisabledTest.java new file mode 100644 index 0000000000..1bf5bfa70b --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverDisabledTest.java @@ -0,0 +1,24 @@ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "micronaut.security.csrf.token-resolvers.http-header.enabled", value = StringUtils.FALSE) +@MicronautTest(startApplication = false) +class HttpHeaderCsrfTokenResolverDisabledTest { + + @Inject + BeanContext beanContext; + + @Test + void testHttpHeaderCsrfTokenResolverDisabled() { + assertFalse(beanContext.containsBean(HttpHeaderCsrfTokenResolver.class)); + } + +} \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverTest.java new file mode 100644 index 0000000000..d6fc3fa8a8 --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolverTest.java @@ -0,0 +1,68 @@ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "spec.name", value = "HttpHeaderCsrfTokenResolverTest") +@MicronautTest +class HttpHeaderCsrfTokenResolverTest { + + @Inject + @Client("/") + HttpClient httpClient; + + @Test + void csrfTokenCanBeResolvedInAnHttpHeader() { + BlockingHttpClient client = httpClient.toBlocking(); + String expected = "abcde"; + // uppercase header name + HttpRequest request = HttpRequest.GET("/csrf/echo").header("X-CSRF-TOKEN", expected); + String token = assertDoesNotThrow(() -> client.retrieve(request)); + assertEquals(expected, token); + + // lowercase header name + HttpRequest lowerCaseRequest = HttpRequest.GET("/csrf/echo").header("X-CSRF-TOKEN", expected); + token = assertDoesNotThrow(() -> client.retrieve(lowerCaseRequest)); + assertEquals(expected, token); + + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(HttpRequest.GET("/csrf/echo"))); + assertEquals(HttpStatus.NOT_FOUND, ex.getStatus()); + } + + @Requires(property = "spec.name", value = "HttpHeaderCsrfTokenResolverTest") + @Controller("/csrf") + static class CsrfTokenEchoController { + + private final HttpHeaderCsrfTokenResolver httpHeaderCsrfTokenResolver; + + CsrfTokenEchoController(HttpHeaderCsrfTokenResolver httpHeaderCsrfTokenResolver) { + this.httpHeaderCsrfTokenResolver = httpHeaderCsrfTokenResolver; + } + + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_PLAIN) + @Get("/echo") + Optional echo(HttpRequest request) { + return httpHeaderCsrfTokenResolver.resolveToken(request); + } + } +} \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfTokenResolvers.adoc b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc new file mode 100644 index 0000000000..cc5a80e54f --- /dev/null +++ b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc @@ -0,0 +1,6 @@ +|=== +|Resolver | Enabled by Default | Disable with +|api:security.csrf.resolver:HttpHeaderCsrfTokenResolver[] +| Yes +| micronaut.security.csrf.token-resolvers.http-header.enabled=false +|=== diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 5ef1eeb705..b65a943d2c 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -86,6 +86,7 @@ rejection: Rejection Handling csrf: title: Cross-Site Request Forgery (CSRF) csrfFilter: CSRF Filter + csrfTokenResolvers: CSRF Token Resolvers tokenPropagation: Token Propagation tokenendpoints: title: Built-In Security Token Controllers From dece74be93cbeee86666265515e799ca168be085 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 16:39:15 +0200 Subject: [PATCH 008/108] CsrfTokenFilter --- security-csrf/build.gradle.kts | 2 +- .../security/csrf/filter/CsrfTokenFilter.java | 141 ++++++++++++++++++ .../csrf/resolver/FieldCsrfTokenResolver.java | 88 +++++++++++ .../csrf/validator/CsrfTokenValidator.java | 36 +++++ .../security/csrf/validator/package-info.java | 21 +++ .../filter/CsrfFilterConfigurationTest.java | 35 +++++ .../FieldCsrfTokenResolverDisabledTest.java | 24 +++ .../resolver/FieldCsrfTokenResolverTest.java | 65 ++++++++ .../docs/guide/csrf/csrfTokenResolvers.adoc | 8 +- 9 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfTokenFilter.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverDisabledTest.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index f4ded239df..c1e91e0c0c 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { api(projects.micronautSecurity) - compileOnly(mn.micronaut.http) + compileOnly(mn.micronaut.http.server) testAnnotationProcessor(mn.micronaut.inject.java) testImplementation(mnTest.micronaut.test.junit5) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfTokenFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfTokenFilter.java new file mode 100644 index 0000000000..ee89b5a228 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfTokenFilter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.filter; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.*; +import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ServerFilter; +import io.micronaut.http.filter.FilterPatternStyle; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.authentication.AuthorizationException; +import io.micronaut.security.csrf.resolver.CsrfTokenResolver; +import io.micronaut.security.csrf.validator.CsrfTokenValidator; +import io.micronaut.security.filters.SecurityFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; + +@Internal +@Requires(property = CsrfFilterConfigurationProperties.PREFIX +".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Requires(classes = { ExceptionHandler.class, HttpRequest.class }) +@Requires(beans = { CsrfTokenValidator.class }) +@ServerFilter(patternStyle = FilterPatternStyle.REGEX, value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:^.*$}") +final class CsrfTokenFilter { + private static final Logger LOG = LoggerFactory.getLogger(CsrfTokenFilter.class); + private final List>> csrfTokenResolvers; + private final CsrfTokenValidator> csrfTokenValidator; + private final ExceptionHandler> exceptionHandler; + private final CsrfFilterConfiguration csrfFilterConfiguration; + + CsrfTokenFilter(CsrfFilterConfiguration csrfFilterConfiguration, + List>> csrfTokenResolvers, + CsrfTokenValidator> csrfTokenValidator, + ExceptionHandler> exceptionHandler) { + this.csrfTokenResolvers = csrfTokenResolvers; + this.csrfTokenValidator = csrfTokenValidator; + this.exceptionHandler = exceptionHandler; + this.csrfFilterConfiguration = csrfFilterConfiguration; + } + + @ExecuteOn(TaskExecutors.BLOCKING) + @RequestFilter + @Nullable + public HttpResponse csrfFilter(@NonNull HttpRequest request) { + if (!shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(request)) { + return null; + } + if (!shouldTheFilterProcessTheRequestAccordingToTheContentType(request)) { + return null; + } + if (!validateCsrfToken(request)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Request rejected by the {} because the CSRF Token validation failed", this.getClass().getSimpleName()); + } + return unauthorized(request); + } + return null; + } + + private boolean shouldTheFilterProcessTheRequestAccordingToTheContentType(@NonNull HttpRequest request) { + if (request.getContentType().isPresent() && csrfFilterConfiguration.getContentTypes().stream().noneMatch(method -> method.equals(request.getContentType().get()))) { + if (LOG.isTraceEnabled()) { + LOG.trace("Request {} {} with content type {} is not processed by the CSRF filter. CSRF filter only processes Content Types: {}", + request.getMethod(), + request.getPath(), + request.getContentType().get(), + csrfFilterConfiguration.getContentTypes().stream().map(MediaType::toString).toList()); + } + return false; + } + return true; + } + + private boolean shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(@NonNull HttpRequest request) { + if (csrfFilterConfiguration.getMethods().stream().noneMatch(method -> method.equals(request.getMethod()))) { + if (LOG.isTraceEnabled()) { + LOG.trace("Request {} {} not processed by the CSRF filter. CSRF filter only processes HTTP Methods: {}", + request.getMethod(), + request.getPath(), + csrfFilterConfiguration.getMethods().stream().map(HttpMethod::name).toList()); + } + return false; + } + return true; + } + + @Nullable + private String resolveCsrfToken(@NonNull HttpRequest request) { + String csrfToken = null; + for (CsrfTokenResolver> tokenResolver : csrfTokenResolvers) { + Optional tokenOptional = tokenResolver.resolveToken(request); + if (tokenOptional.isPresent()) { + if (LOG.isTraceEnabled()) { + LOG.trace("CSRF token resolved via {}", tokenResolver.getClass().getSimpleName()); + } + csrfToken = tokenOptional.get(); + break; + } + } + return csrfToken; + } + + private boolean validateCsrfToken(@NonNull HttpRequest request) { + String csrfToken = resolveCsrfToken(request); + if (csrfToken == null) { + LOG.trace("No CSRF token found in request"); + return false; + } + return csrfTokenValidator.validateCsrfToken(request, csrfToken); + } + + @NonNull + private HttpResponse unauthorized(@NonNull HttpRequest request) { + Authentication authentication = request.getAttribute(SecurityFilter.AUTHENTICATION, Authentication.class) + .orElse(null); + return exceptionHandler.handle(request, + new AuthorizationException(authentication)); + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java new file mode 100644 index 0000000000..0919ba975e --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.ServerHttpRequest; +import io.micronaut.http.body.ByteBody; +import io.micronaut.http.body.CloseableByteBody; +import io.micronaut.security.csrf.CsrfConfiguration; +import jakarta.inject.Singleton; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * Resolves a CSRF token from a form-urlencoded body using the {@link ServerHttpRequest#byteBody()} API.. + * + * @since 2.0.0 + */ +@Requires(property = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Singleton +public class FieldCsrfTokenResolver implements CsrfTokenResolver> { + private final CsrfConfiguration csrfConfiguration; + + public FieldCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { + this.csrfConfiguration = csrfConfiguration; + } + + @Override + public Optional resolveToken(HttpRequest request) { + if (request instanceof ServerHttpRequest serverHttpRequest) { + return resolveToken(serverHttpRequest); + } + return Optional.empty(); + } + + public Optional resolveToken(ServerHttpRequest request) { + try (CloseableByteBody ourCopy = + request.byteBody() + .split(ByteBody.SplitBackpressureMode.SLOWEST) + .allowDiscard()) { + try(InputStream inputStream = ourCopy.toInputStream()) { + String str = ofInputStream(inputStream); + return extractCsrfTokenFromAFormUrlEncodedString(str); + } catch (IOException e) { + return Optional.empty(); + } + } + } + + private String ofInputStream(InputStream inputStream) throws IOException { + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int length; (length = inputStream.read(buffer)) != -1; ) { + result.write(buffer, 0, length); + } + return result.toString(StandardCharsets.UTF_8); + } + + private Optional extractCsrfTokenFromAFormUrlEncodedString(String body) { + final String[] arr = body.split("&"); + final String prefix = csrfConfiguration.getFieldName() + "="; + for (String s : arr) { + if (s.startsWith(prefix)) { + return Optional.of(s.substring(prefix.length())); + } + } + return Optional.empty(); + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java new file mode 100644 index 0000000000..adf5f8fa99 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.validator; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +/** + * CsrfToken Validator + * @author Sergio del Amo + * @since 4.11.0 + * @param request + */ +@FunctionalInterface +public interface CsrfTokenValidator { + /** + * + * @param request Request + * @param token CSRF Token + * @return Whether the CSRF token is valid + */ + boolean validateCsrfToken(@Nullable T request, @NonNull String token); +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java new file mode 100644 index 0000000000..6b74e0ca3e --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes related to CSRF token validation. + * @author Sergio del Amo + * @since 4.11.0 + */ +package io.micronaut.security.csrf.validator; \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java new file mode 100644 index 0000000000..df0c2ebf3a --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java @@ -0,0 +1,35 @@ +package io.micronaut.security.csrf.filter.filter; + +import io.micronaut.http.HttpMethod; +import io.micronaut.http.MediaType; +import io.micronaut.security.csrf.filter.CsrfFilterConfiguration; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest(startApplication = false) +class CsrfFilterConfigurationTest { + + @Inject + CsrfFilterConfiguration csrfFilterConfiguration; + + @Test + void defaultMethods() { + assertEquals(Set.of(HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PUT, HttpMethod.PATCH), csrfFilterConfiguration.getMethods()); + } + + @Test + void defaultContentType() { + assertEquals(Set.of(MediaType.APPLICATION_FORM_URLENCODED_TYPE, MediaType.MULTIPART_FORM_DATA_TYPE), csrfFilterConfiguration.getContentTypes()); + } + + @Test + void defaultEnabled() { + assertTrue(csrfFilterConfiguration.isEnabled()); + } +} \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverDisabledTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverDisabledTest.java new file mode 100644 index 0000000000..42571e06ef --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverDisabledTest.java @@ -0,0 +1,24 @@ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Property(name = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.FALSE) +@MicronautTest(startApplication = false) +class FieldCsrfTokenResolverDisabledTest { + + @Inject + BeanContext beanContext; + + @Test + void testFieldCsrfTokenResolverDisabled() { + assertFalse(beanContext.containsBean(FieldCsrfTokenResolver.class)); + } + +} \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java new file mode 100644 index 0000000000..a19edeb41a --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java @@ -0,0 +1,65 @@ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.*; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.csrf.validator.CsrfTokenValidator; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.serde.annotation.Serdeable; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Property(name = "spec.name", value = "FieldCsrfTokenResolverTest") +@MicronautTest +class FieldCsrfTokenResolverTest { + + @Test + void fieldTokenResolver(@Client("/") HttpClient httpClient) { + BlockingHttpClient client = httpClient.toBlocking(); + HttpRequest request = HttpRequest.POST("/password/change", "username=sherlock&csrfToken=abcde&password=elementary") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE) + .accept(MediaType.TEXT_HTML); + String result = assertDoesNotThrow(() -> client.retrieve(request)); + assertEquals("sherlock", result); + } + + @Requires(property = "spec.name", value = "FieldCsrfTokenResolverTest") + @Singleton + @Replaces(CsrfTokenValidator.class) + static class CsrfTokenValidatorReplacement implements CsrfTokenValidator> { + @Override + public boolean validateCsrfToken(HttpRequest request, String token) { + return token.equals("abcde"); + } + } + + @Requires(property = "spec.name", value = "FieldCsrfTokenResolverTest") + @Controller + static class PasswordChangeController { + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_HTML) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Post("/password/change") + String changePassword(@Body PasswordChangeForm passwordChangeForm) { + return passwordChangeForm.username; + } + } + + @Serdeable + record PasswordChangeForm( + String username, + String password, + String csrfToken) { + } +} \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfTokenResolvers.adoc b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc index cc5a80e54f..beaab92d97 100644 --- a/src/main/docs/guide/csrf/csrfTokenResolvers.adoc +++ b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc @@ -1,6 +1,12 @@ |=== |Resolver | Enabled by Default | Disable with + |api:security.csrf.resolver:HttpHeaderCsrfTokenResolver[] | Yes -| micronaut.security.csrf.token-resolvers.http-header.enabled=false +| `micronaut.security.csrf.token-resolvers.http-header.enabled=false` + +|api:security.csrf.resolver:FieldCsrfTokenResolver[] +| Yes +| `micronaut.security.csrf.token-resolvers.field.enabled=false` + |=== From 91a81dfb531a4b8340fa9ae6d3234bd0b493109c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 16:49:54 +0200 Subject: [PATCH 009/108] add disabled test --- .../{CsrfTokenFilter.java => CsrfFilter.java} | 12 +++---- .../csrf/filter/CsrfFilterDisabledTest.java | 24 +++++++++++++ .../filter/CsrfFilterConfigurationTest.java | 35 ------------------- 3 files changed, 30 insertions(+), 41 deletions(-) rename security-csrf/src/main/java/io/micronaut/security/csrf/filter/{CsrfTokenFilter.java => CsrfFilter.java} (94%) create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java delete mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfTokenFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java similarity index 94% rename from security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfTokenFilter.java rename to security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index ee89b5a228..146be6cab1 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfTokenFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -43,17 +43,17 @@ @Requires(classes = { ExceptionHandler.class, HttpRequest.class }) @Requires(beans = { CsrfTokenValidator.class }) @ServerFilter(patternStyle = FilterPatternStyle.REGEX, value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:^.*$}") -final class CsrfTokenFilter { - private static final Logger LOG = LoggerFactory.getLogger(CsrfTokenFilter.class); +final class CsrfFilter { + private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); private final List>> csrfTokenResolvers; private final CsrfTokenValidator> csrfTokenValidator; private final ExceptionHandler> exceptionHandler; private final CsrfFilterConfiguration csrfFilterConfiguration; - CsrfTokenFilter(CsrfFilterConfiguration csrfFilterConfiguration, - List>> csrfTokenResolvers, - CsrfTokenValidator> csrfTokenValidator, - ExceptionHandler> exceptionHandler) { + CsrfFilter(CsrfFilterConfiguration csrfFilterConfiguration, + List>> csrfTokenResolvers, + CsrfTokenValidator> csrfTokenValidator, + ExceptionHandler> exceptionHandler) { this.csrfTokenResolvers = csrfTokenResolvers; this.csrfTokenValidator = csrfTokenValidator; this.exceptionHandler = exceptionHandler; diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java new file mode 100644 index 0000000000..456fbc8e4a --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java @@ -0,0 +1,24 @@ +package io.micronaut.security.csrf.filter; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Property(name = "micronaut.security.csrf.filter.enabled", value = StringUtils.FALSE) +@MicronautTest(startApplication = false) +class CsrfFilterDisabledTest { + + @Inject + BeanContext beanContext; + + @Test + void testFieldCsrfTokenResolverDisabled() { + assertFalse(beanContext.containsBean(CsrfFilter.class)); + } + +} \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java deleted file mode 100644 index df0c2ebf3a..0000000000 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/filter/CsrfFilterConfigurationTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.micronaut.security.csrf.filter.filter; - -import io.micronaut.http.HttpMethod; -import io.micronaut.http.MediaType; -import io.micronaut.security.csrf.filter.CsrfFilterConfiguration; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@MicronautTest(startApplication = false) -class CsrfFilterConfigurationTest { - - @Inject - CsrfFilterConfiguration csrfFilterConfiguration; - - @Test - void defaultMethods() { - assertEquals(Set.of(HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PUT, HttpMethod.PATCH), csrfFilterConfiguration.getMethods()); - } - - @Test - void defaultContentType() { - assertEquals(Set.of(MediaType.APPLICATION_FORM_URLENCODED_TYPE, MediaType.MULTIPART_FORM_DATA_TYPE), csrfFilterConfiguration.getContentTypes()); - } - - @Test - void defaultEnabled() { - assertTrue(csrfFilterConfiguration.isEnabled()); - } -} \ No newline at end of file From 6fbc3a55e5485a0fa42d0e5219a516f515c87102 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 17 Oct 2024 17:07:48 +0200 Subject: [PATCH 010/108] start csrf token generation implementation --- .../security/csrf/CsrfConfiguration.java | 6 +++ .../csrf/CsrfConfigurationProperties.java | 17 +++++++ .../security/csrf/filter/CsrfFilter.java | 14 ++++-- .../csrf/generator/CsrfTokenGenerator.java | 34 ++++++++++++++ .../generator/DefaultCsrfTokenGenerator.java | 47 +++++++++++++++++++ .../security/csrf/generator/package-info.java | 21 +++++++++ .../csrf/resolver/FieldCsrfTokenResolver.java | 4 +- .../security/csrf/resolver/package-info.java | 21 +++++++++ .../csrf/validator/CsrfTokenValidator.java | 2 +- .../security/csrf/validator/package-info.java | 2 +- src/main/docs/guide/csrf.adoc | 2 + 11 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/generator/package-info.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/resolver/package-info.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index d19ae20a78..658ad03d1a 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -25,6 +25,12 @@ */ public interface CsrfConfiguration extends Toggleable { + /** + * + * @return Random CSRF Token size in bytes. + */ + int getTokenSize(); + /** * * @return HTTP Header name to look for the CSRF token. diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index fe6192c9c0..8336de9334 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -29,6 +29,8 @@ class CsrfConfigurationProperties implements CsrfConfiguration { public static final String DEFAULT_FIELD_NAME = "csrfToken"; + public static final int DEFAULT_TOKEN_SIZE = 16; + public static final boolean DEFAULT_ENABLED = true; private boolean enabled = DEFAULT_ENABLED; @@ -37,6 +39,21 @@ class CsrfConfigurationProperties implements CsrfConfiguration { private String fieldName = DEFAULT_FIELD_NAME; + private int tokenSize = DEFAULT_TOKEN_SIZE; + + @Override + public int getTokenSize() { + return tokenSize; + } + + /** + * Random CSRF Token size in bytes. Default Value: {@value #DEFAULT_TOKEN_SIZE}. + * @param tokenSize Random CSRF Token size in bytes. + */ + public void setTokenSize(int tokenSize) { + this.tokenSize = tokenSize; + } + @Override @NonNull public String getHeaderName() { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 146be6cab1..213b08572f 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -38,8 +38,14 @@ import java.util.List; import java.util.Optional; +/** + * {@link RequestFilter} which validates CSRF tokens and returns a 401 Unauthorized if the token is invalid. + * Which requests are intercepted can be controlled via {@link io.micronaut.security.csrf.CsrfConfiguration}. + * @author Sergio del Amo + * @since 4.11.0 + */ @Internal -@Requires(property = CsrfFilterConfigurationProperties.PREFIX +".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Requires(property = CsrfFilterConfigurationProperties.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Requires(classes = { ExceptionHandler.class, HttpRequest.class }) @Requires(beans = { CsrfTokenValidator.class }) @ServerFilter(patternStyle = FilterPatternStyle.REGEX, value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:^.*$}") @@ -65,10 +71,10 @@ final class CsrfFilter { @Nullable public HttpResponse csrfFilter(@NonNull HttpRequest request) { if (!shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(request)) { - return null; + return null; // continue normally } if (!shouldTheFilterProcessTheRequestAccordingToTheContentType(request)) { - return null; + return null; // continue normally } if (!validateCsrfToken(request)) { if (LOG.isDebugEnabled()) { @@ -76,7 +82,7 @@ public HttpResponse csrfFilter(@NonNull HttpRequest request) { } return unauthorized(request); } - return null; + return null; // continue normally } private boolean shouldTheFilterProcessTheRequestAccordingToTheContentType(@NonNull HttpRequest request) { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java new file mode 100644 index 0000000000..234309dabf --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.generator; + +import io.micronaut.context.annotation.DefaultImplementation; + +/** + * CSRF token Generation. + * @author Sergio del Amo + * @since 4.11.0 + */ +@DefaultImplementation(DefaultCsrfTokenGenerator.class) +@FunctionalInterface +public interface CsrfTokenGenerator { + + /** + * + * @return A CSRF Token. + */ + String generate(); +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java new file mode 100644 index 0000000000..895db669e9 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.generator; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.security.csrf.CsrfConfiguration; +import jakarta.inject.Singleton; + +import java.security.SecureRandom; +import java.util.Base64; + +/** + * Default implementation of {@link CsrfTokenGenerator} which generates a random base 64 encoded string using an instance of {@link SecureRandom} and random byte array of size {@link CsrfConfiguration#getTokenSize()}. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Singleton +@Internal +final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator { + + private final SecureRandom secureRandom = new SecureRandom(); + private final CsrfConfiguration csrfConfiguration; + + DefaultCsrfTokenGenerator(CsrfConfiguration csrfConfiguration) { + this.csrfConfiguration = csrfConfiguration; + } + + @Override + public String generate() { + byte[] tokenBytes = new byte[csrfConfiguration.getTokenSize()]; + secureRandom.nextBytes(tokenBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes); + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/package-info.java new file mode 100644 index 0000000000..7079368d66 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes related to CSRF Token Generation. + * @author Sergio del Amo + * @since 4.11.0 + */ +package io.micronaut.security.csrf.generator; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 0919ba975e..497c04a15e 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -52,12 +52,12 @@ public Optional resolveToken(HttpRequest request) { return Optional.empty(); } - public Optional resolveToken(ServerHttpRequest request) { + private Optional resolveToken(ServerHttpRequest request) { try (CloseableByteBody ourCopy = request.byteBody() .split(ByteBody.SplitBackpressureMode.SLOWEST) .allowDiscard()) { - try(InputStream inputStream = ourCopy.toInputStream()) { + try (InputStream inputStream = ourCopy.toInputStream()) { String str = ofInputStream(inputStream); return extractCsrfTokenFromAFormUrlEncodedString(str); } catch (IOException e) { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/package-info.java new file mode 100644 index 0000000000..f912fa67c9 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes related to CSRF Token Resolution. + * @author Sergio del Amo + * @since 4.11.0 + */ +package io.micronaut.security.csrf.resolver; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java index adf5f8fa99..af8b4778ee 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.Nullable; /** - * CsrfToken Validator + * CSRF Token Validation. * @author Sergio del Amo * @since 4.11.0 * @param request diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java index 6b74e0ca3e..1b3968512c 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/package-info.java @@ -18,4 +18,4 @@ * @author Sergio del Amo * @since 4.11.0 */ -package io.micronaut.security.csrf.validator; \ No newline at end of file +package io.micronaut.security.csrf.validator; diff --git a/src/main/docs/guide/csrf.adoc b/src/main/docs/guide/csrf.adoc index aed0bfcec5..ed1c2231f1 100644 --- a/src/main/docs/guide/csrf.adoc +++ b/src/main/docs/guide/csrf.adoc @@ -1,3 +1,5 @@ +https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)]. + The following configuration options are available for CSRF: include::{includedir}configurationProperties/io.micronaut.security.csrf.CsrfConfigurationProperties.adoc[] From bb94d279ca5baec08dc3d7111414e184655e74eb Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 18 Oct 2024 08:52:12 +0200 Subject: [PATCH 011/108] add csrf module in testImplementation --- security-session/build.gradle.kts | 9 +++++++++ .../session/SessionAuthenticationNoRedirectSpec.groovy | 3 +++ ...ginFailedHttpRequestAuthenticationProviderSpec.groovy | 1 + ...dHttpRequestExecutorAuthenticationProviderSpec.groovy | 1 + ...dHttpRequestReactiveAuthenticationProviderSpec.groovy | 1 + .../handlers/RedirectRejectionHandlerSpec.groovy | 8 ++++++-- .../io/micronaut/security/session/ContextPathSpec.groovy | 2 ++ .../session/RejectionHandlerResolutionSpec.groovy | 9 +++++++++ .../SecuritySessionBeansWithSecurityDisabledSpec.groovy | 2 ++ ...itySessionBeansWithSecuritySessionDisabledSpec.groovy | 2 ++ .../session/SessionLoginHandlerContextPathSpec.groovy | 2 ++ .../session/SessionLogoutHandlerContextPathSpec.groovy | 2 ++ .../micronaut/security/session/SessionReUseSpec.groovy | 2 ++ .../security/session/UnauthorizedTargetUrlSpec.groovy | 2 ++ 14 files changed, 44 insertions(+), 2 deletions(-) diff --git a/security-session/build.gradle.kts b/security-session/build.gradle.kts index 49bb4890c9..151fd1132b 100644 --- a/security-session/build.gradle.kts +++ b/security-session/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { api(mnSession.micronaut.session) api(projects.micronautSecurity) implementation(mnReactor.micronaut.reactor) + compileOnly(projects.micronautSecurityCsrf) testAnnotationProcessor(mnSerde.micronaut.serde.processor) testImplementation(mnSerde.micronaut.serde.jackson) @@ -16,4 +17,12 @@ dependencies { testImplementation(mn.micronaut.http.server.netty) testImplementation(projects.testSuiteUtils) testImplementation(projects.testSuiteUtilsSecurity) + + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(mnTest.micronaut.test.junit5) + testImplementation(projects.micronautSecurityCsrf) + + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(mnLogging.logback.classic) + } diff --git a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy index 270d8c7455..8b1384a8cd 100644 --- a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.docs.security.session import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.Nullable +import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.MediaType @@ -13,6 +14,7 @@ import io.micronaut.security.annotation.Secured import io.micronaut.security.testutils.EmbeddedServerSpecification import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario +import io.netty.util.internal.StringUtil import jakarta.inject.Singleton import java.security.Principal @@ -22,6 +24,7 @@ class SessionAuthenticationNoRedirectSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.authentication': 'session', 'micronaut.security.redirect.enabled': false, 'spec.name': 'SessionAuthenticationNoRedirectSpec', diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy index 5ba4d0865d..a0294e3189 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy @@ -32,6 +32,7 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +@Property(name = "micronaut.security.csrf.enabled", value=StringUtils.FALSE) @Property(name = "micronaut.security.authentication", value="session") @Property(name = "micronaut.http.client.follow-redirects", value = StringUtils.FALSE) @Property(name = "micronaut.security.redirect.login-failure", value="/security/login") diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy index 7ed848a746..0b96488a84 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy @@ -35,6 +35,7 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +@Property(name = "micronaut.security.csrf.enabled", value=StringUtils.FALSE) @Property(name = "micronaut.security.authentication", value="session") @Property(name = "micronaut.http.client.follow-redirects", value = StringUtils.FALSE) @Property(name = "micronaut.security.redirect.login-failure", value="/security/login") diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy index 6043997aea..a000816513 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy @@ -35,6 +35,7 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +@Property(name = "micronaut.security.csrf.enabled", value=StringUtils.FALSE) @Property(name = "micronaut.security.authentication", value="session") @Property(name = "micronaut.http.client.follow-redirects", value = StringUtils.FALSE) @Property(name = "micronaut.security.redirect.login-failure", value="/security/login") diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy index be5074a3e1..ef59790dc0 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.security.handlers 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.HttpStatus @@ -27,8 +28,11 @@ class RedirectRejectionHandlerSpec extends EmbeddedServerSpecification { String accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" Map getConfiguration() { - super.configuration + ['micronaut.security.redirect.unauthorized.url': '/login', - 'micronaut.security.redirect.forbidden.url': '/forbidden'] + super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, + 'micronaut.security.redirect.unauthorized.url': '/login', + 'micronaut.security.redirect.forbidden.url': '/forbidden' + ] } void "UnauthorizedRejectionUriProvider is used for 401"() { diff --git a/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy index 7141e0c43c..94f870a243 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.security.session import io.micronaut.context.annotation.Requires +import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType @@ -27,6 +28,7 @@ class ContextPathSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.server.context-path': 'foo', 'micronaut.security.authentication': 'session' ] diff --git a/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy index bbbac53026..282fd884e7 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy @@ -4,6 +4,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Replaces import io.micronaut.context.annotation.Requires import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.MutableHttpResponse import io.micronaut.http.server.exceptions.ExceptionHandler @@ -11,6 +12,7 @@ import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.security.authentication.AuthorizationException import io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler import io.micronaut.security.testutils.ApplicationContextSpecification +import io.micronaut.security.testutils.ConfigurationFixture import jakarta.inject.Singleton class RejectionHandlerResolutionSpec extends ApplicationContextSpecification { @@ -20,6 +22,13 @@ class RejectionHandlerResolutionSpec extends ApplicationContextSpecification { 'RejectionHandlerResolutionSpec' } + @Override + Map getConfiguration() { + return super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE + ] + } + void "RedirectRejectionHandler is the default rejection handler resolved"() { given: ApplicationContext ctx = ApplicationContext.run([:]) diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy index af953f420f..8e7764f2a5 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.security.session import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.ApplicationContextSpecification import spock.lang.Unroll @@ -9,6 +10,7 @@ class SecuritySessionBeansWithSecurityDisabledSpec extends ApplicationContextSpe Map getConfiguration() { super.configuration + [ 'micronaut.security.enabled': false, + 'micronaut.security.csrf.enabled': StringUtils.FALSE ] } diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy index 9cc7c6886e..09f8a10bb4 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.security.session import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.ApplicationContextSpecification import spock.lang.Unroll @@ -9,6 +10,7 @@ class SecuritySessionBeansWithSecuritySessionDisabledSpec extends ApplicationCon @Override Map getConfiguration() { super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.session.enabled': false, ] } diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy index ab74db0e5b..d5745df1af 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy @@ -1,11 +1,13 @@ package io.micronaut.security.session +import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.EmbeddedServerSpecification class SessionLoginHandlerContextPathSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.server.context-path': 'foo', 'micronaut.security.authentication': 'session' ] diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy index 2485e97284..453ddae2e7 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy @@ -1,11 +1,13 @@ package io.micronaut.security.session +import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.EmbeddedServerSpecification class SessionLogoutHandlerContextPathSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.server.context-path': 'foo', 'micronaut.security.authentication': 'session' ] diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy index a986732c38..3960410e88 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.security.session 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 @@ -22,6 +23,7 @@ class SessionReUseSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.authentication': 'session', ] } diff --git a/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy index 2053c805d7..c97bd2a8d9 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.security.session import io.micronaut.context.annotation.Requires +import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get @@ -17,6 +18,7 @@ class UnauthorizedTargetUrlSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ + 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.redirect.unauthorized.url': '/login/auth', 'micronaut.security.intercept-url-map': [ [pattern: '/login/auth', httpMethod: 'GET', access: ['isAnonymous()']] From b140a3c02dcf39ad48d5c55e7d261cd46174b12c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 18 Oct 2024 08:52:47 +0200 Subject: [PATCH 012/108] add getHttpSessionName in CSRF Configuration --- .../security/csrf/CsrfConfiguration.java | 7 +++++ .../csrf/CsrfConfigurationProperties.java | 29 +++++++++++++++++++ .../security/csrf/CsrfConfigurationTest.java | 10 +++++++ 3 files changed, 46 insertions(+) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index 658ad03d1a..e8899d3a85 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -38,6 +38,13 @@ public interface CsrfConfiguration extends Toggleable { @NonNull String getHeaderName(); + /** + * + * @return Key to look for the CSRF token in an HTTP Session. + */ + @NonNull + String getHttpSessionName(); + /** * * @return Field name in a form url encoded submission to look for the CSRF token. diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index 8336de9334..d12ab1750e 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -25,10 +25,24 @@ class CsrfConfigurationProperties implements CsrfConfiguration { public static final String PREFIX = SecurityConfigurationProperties.PREFIX + ".csrf"; + /** + * The default HTTP Header name. + */ + @SuppressWarnings("WeakerAccess") public static final String DEFAULT_HTTP_HEADER_NAME = "X-CSRF-TOKEN"; + /** + * The default fieldName. + */ + @SuppressWarnings("WeakerAccess") public static final String DEFAULT_FIELD_NAME = "csrfToken"; + /** + * The default HTTP Session name. + */ + @SuppressWarnings("WeakerAccess") + public static final String DEFAULT_HTTP_SESSION_NAME = "csrfToken"; + public static final int DEFAULT_TOKEN_SIZE = 16; public static final boolean DEFAULT_ENABLED = true; @@ -41,6 +55,21 @@ class CsrfConfigurationProperties implements CsrfConfiguration { private int tokenSize = DEFAULT_TOKEN_SIZE; + private String httpSessionName = DEFAULT_HTTP_SESSION_NAME; + + @Override + public String getHttpSessionName() { + return httpSessionName; + } + + /** + * 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. + */ + public void setHttpSessionName(String httpSessionName) { + this.httpSessionName = httpSessionName; + } + @Override public int getTokenSize() { return tokenSize; diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java index 5e0c773a2f..27d16b3dee 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java @@ -26,4 +26,14 @@ void defaultFieldName() { void defaultEnabled() { assertTrue(csrfConfiguration.isEnabled()); } + + @Test + void defaultHttpSessionName() { + assertEquals("csrfToken", csrfConfiguration.getHttpSessionName()); + } + + @Test + void defaultTokenSize() { + assertEquals(16, csrfConfiguration.getTokenSize()); + } } \ No newline at end of file From f994c7eefec08d7993fb299d17f7f0e9e73399b2 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 18 Oct 2024 08:53:24 +0200 Subject: [PATCH 013/108] integration of csrf in session --- .../csrf/repository/CsrfTokenRepository.java | 28 +++++++ .../RepositoryCsrfTokenValidator.java | 44 +++++++++++ .../security/session/SessionLoginHandler.java | 9 ++- .../csrf/CsrfSessionLogingHandler.java | 75 +++++++++++++++++++ .../csrf/SessionCsrfTokenRepository.java | 45 +++++++++++ .../security/session/csrf/package-info.java | 27 +++++++ 6 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java create mode 100644 security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java create mode 100644 security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java create mode 100644 security-session/src/main/java/io/micronaut/security/session/csrf/package-info.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java new file mode 100644 index 0000000000..cf196ccc71 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.repository; + +import java.util.Optional; + +/** + * Repository API for CSRF Tokens. + * @param Request + */ +@FunctionalInterface +public interface CsrfTokenRepository { + + Optional findCsrfToken(T request); +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java new file mode 100644 index 0000000000..5ac1ab789b --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.validator; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.security.csrf.repository.CsrfTokenRepository; +import jakarta.inject.Singleton; + +/** + * {@link CsrfTokenValidator} implementation that uses a {@link CsrfTokenRepository}. + * First attempts to retrieve a token from a {@link CsrfTokenRepository} and if found validates it against the supplied token. + * @param Request + * @since 4.11.0 + * @author Sergio del Amo + */ +@Requires(bean = CsrfTokenRepository.class) +@Singleton +public class RepositoryCsrfTokenValidator implements CsrfTokenValidator { + private final CsrfTokenRepository csrfTokenRepository; + + public RepositoryCsrfTokenValidator(CsrfTokenRepository csrfTokenRepository) { + this.csrfTokenRepository = csrfTokenRepository; + } + + @Override + public boolean validateCsrfToken(T request, String token) { + return csrfTokenRepository.findCsrfToken(request) + .map(storedToken -> storedToken.equals(token)) + .orElse(false); + } +} diff --git a/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java b/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java index 7a8bacf46f..3a745cc6c6 100644 --- a/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java +++ b/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java @@ -130,8 +130,15 @@ private ThrowingSupplier loginSuccessUriSupplier(@NonNu return uriSupplier; } - private void saveAuthenticationInSession(Authentication authentication, HttpRequest request) { + /** + * Saves the authentication in the session. + * @param authentication Authentication + * @param request HTTP Request + * @return The session found or created where the authentication was saved. + */ + protected Session saveAuthenticationInSession(Authentication authentication, HttpRequest request) { Session session = SessionForRequest.find(request).orElseGet(() -> SessionForRequest.create(sessionStore, request)); session.put(SecurityFilter.AUTHENTICATION, authentication); + return session; } } diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java new file mode 100644 index 0000000000..f62c9c169a --- /dev/null +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.session.csrf; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.config.RedirectConfiguration; +import io.micronaut.security.config.RedirectService; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.generator.CsrfTokenGenerator; +import io.micronaut.security.errors.PriorToLoginPersistence; +import io.micronaut.security.session.SessionAuthenticationModeCondition; +import io.micronaut.security.session.SessionLoginHandler; +import io.micronaut.session.Session; +import io.micronaut.session.SessionStore; +import jakarta.inject.Singleton; + +/** + * Replacement of {@link SessionLoginHandler} that extends it and saves a CSRF token in the session. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Requires(condition = SessionAuthenticationModeCondition.class) +@Requires(beans = { CsrfConfiguration.class, CsrfTokenGenerator.class }) +@Replaces(SessionLoginHandler.class) +@Singleton +public class CsrfSessionLogingHandler extends SessionLoginHandler { + + private final CsrfConfiguration csrfConfiguration; + private final CsrfTokenGenerator csrfTokenGenerator; + + /** + * Constructor. + * + * @param redirectConfiguration Redirect configuration + * @param sessionStore The session store + * @param priorToLoginPersistence The persistence to store the original url + * @param redirectService Redirection Service + * @param csrfConfiguration CSRF Configuration + * @param csrfTokenGenerator CSRF Token Generator + */ + public CsrfSessionLogingHandler(RedirectConfiguration redirectConfiguration, + SessionStore sessionStore, + PriorToLoginPersistence, + MutableHttpResponse> priorToLoginPersistence, + RedirectService redirectService, + CsrfConfiguration csrfConfiguration, CsrfTokenGenerator csrfTokenGenerator) { + super(redirectConfiguration, sessionStore, priorToLoginPersistence, redirectService); + this.csrfConfiguration = csrfConfiguration; + this.csrfTokenGenerator = csrfTokenGenerator; + } + + @Override + protected Session saveAuthenticationInSession(Authentication authentication, HttpRequest request) { + Session session = super.saveAuthenticationInSession(authentication, request); + session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generate()); + return session; + } +} diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java b/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java new file mode 100644 index 0000000000..07b90ec16c --- /dev/null +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.session.csrf; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.repository.CsrfTokenRepository; +import io.micronaut.session.http.SessionForRequest; + +import java.util.Optional; + +/** + * Implementation of {@link CsrfTokenRepository} that retrieves the CSRF token from an HTTP session using the key defined in {@link CsrfConfiguration#getHttpSessionName()}. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Requires(classes = HttpRequest.class) +@Requires(beans = CsrfConfiguration.class) +public class SessionCsrfTokenRepository implements CsrfTokenRepository> { + private final CsrfConfiguration csrfConfiguration; + + public SessionCsrfTokenRepository(CsrfConfiguration csrfConfiguration) { + this.csrfConfiguration = csrfConfiguration; + } + + @Override + public Optional findCsrfToken(HttpRequest request) { + return SessionForRequest.find(request) + .flatMap(session -> session.get(csrfConfiguration.getHttpSessionName(), String.class)); + } +} diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/package-info.java b/security-session/src/main/java/io/micronaut/security/session/csrf/package-info.java new file mode 100644 index 0000000000..62198cd79a --- /dev/null +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes related to Cross Site Request Forgery (CSRF) and HTTP Session. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Requires(classes = CsrfTokenRepository.class) +@Configuration +package io.micronaut.security.session.csrf; + +import io.micronaut.context.annotation.Configuration; +import io.micronaut.context.annotation.Requires; +import io.micronaut.security.csrf.repository.CsrfTokenRepository; From a448b190d83905fe4d49f281d3c428f843d8e616 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 18 Oct 2024 09:27:50 +0200 Subject: [PATCH 014/108] csrf session test --- .../security/csrf/filter/CsrfFilter.java | 3 +- .../CsrfFilterConfigurationProperties.java | 5 +- .../filter/CsrfFilterConfigurationTest.java | 13 +- .../generator/CsrfTokenGeneratorTest.java | 24 ++++ .../resolver/FieldCsrfTokenResolverTest.java | 12 +- .../csrf/CsrfSessionLogingHandler.java | 15 ++- .../csrf/SessionCsrfTokenRepository.java | 2 + .../csrf/CsrfSessionLogingHandlerTest.java | 127 ++++++++++++++++++ 8 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java create mode 100644 security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 213b08572f..bec149d114 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -48,7 +48,8 @@ @Requires(property = CsrfFilterConfigurationProperties.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Requires(classes = { ExceptionHandler.class, HttpRequest.class }) @Requires(beans = { CsrfTokenValidator.class }) -@ServerFilter(patternStyle = FilterPatternStyle.REGEX, value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:^.*$}") +@ServerFilter(patternStyle = FilterPatternStyle.REGEX, + value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") final class CsrfFilter { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); private final List>> csrfTokenResolvers; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java index 821b0e3a5d..c634c46d46 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java @@ -22,6 +22,8 @@ import io.micronaut.http.HttpMethod; import io.micronaut.http.MediaType; import io.micronaut.security.config.SecurityConfigurationProperties; +import io.micronaut.security.endpoints.LoginControllerConfigurationProperties; + import java.util.Set; @Requires(classes = { HttpMethod.class, MediaType.class }) @@ -40,7 +42,8 @@ final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration * The default regex pattern. */ @SuppressWarnings("WeakerAccess") - public static final String DEFAULT_REGEX_PATTERN = "^.*$"; + public static final String DEFAULT_REGEX_PATTERN = "^(?!\\/(login|logout)).*$"; + private static final Set DEFAULT_METHODS = Set.of( HttpMethod.POST, diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java index 9e784a4f20..1af23d70b1 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java @@ -1,5 +1,6 @@ package io.micronaut.security.csrf.filter; +import io.micronaut.core.util.PathMatcher; import io.micronaut.http.HttpMethod; import io.micronaut.http.MediaType; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; @@ -8,8 +9,7 @@ import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; @MicronautTest(startApplication = false) class CsrfFilterConfigurationTest { @@ -27,6 +27,15 @@ void defaultContentType() { assertEquals(Set.of(MediaType.APPLICATION_FORM_URLENCODED_TYPE, MediaType.MULTIPART_FORM_DATA_TYPE), csrfFilterConfiguration.getContentTypes()); } + @Test + void defaultRegexPattern() { + String regexPattern = csrfFilterConfiguration.getRegexPattern(); + assertFalse(PathMatcher.REGEX.matches(csrfFilterConfiguration.getRegexPattern(), "/login")); + assertFalse(PathMatcher.REGEX.matches(csrfFilterConfiguration.getRegexPattern(), "/logout")); + assertTrue(PathMatcher.REGEX.matches(csrfFilterConfiguration.getRegexPattern(), "/todo/list")); + assertEquals("^(?!\\/(login|logout)).*$", regexPattern); + } + @Test void defaultEnabled() { assertTrue(csrfFilterConfiguration.isEnabled()); diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java new file mode 100644 index 0000000000..f419317633 --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java @@ -0,0 +1,24 @@ +package io.micronaut.security.csrf.generator; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +@MicronautTest(startApplication = false) +class CsrfTokenGeneratorTest { + + @Test + void generatedCsrfTokensAreUnique(CsrfTokenGenerator csrfTokenGenerator) { + int attempts = 100; + Set results = new HashSet<>(); + for (int i = 0; i < attempts; i++) { + results.add(csrfTokenGenerator.generate()); + } + assertEquals(attempts, results.size()); + } + +} \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java index a19edeb41a..528e012282 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java @@ -10,13 +10,15 @@ import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.security.annotation.Secured; -import io.micronaut.security.csrf.validator.CsrfTokenValidator; +import io.micronaut.security.csrf.repository.CsrfTokenRepository; import io.micronaut.security.rules.SecurityRule; import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,11 +38,11 @@ void fieldTokenResolver(@Client("/") HttpClient httpClient) { @Requires(property = "spec.name", value = "FieldCsrfTokenResolverTest") @Singleton - @Replaces(CsrfTokenValidator.class) - static class CsrfTokenValidatorReplacement implements CsrfTokenValidator> { + @Replaces(CsrfTokenRepository.class) + static class CsrfTokenRepositoryReplacement implements CsrfTokenRepository> { @Override - public boolean validateCsrfToken(HttpRequest request, String token) { - return token.equals("abcde"); + public Optional findCsrfToken(HttpRequest request) { + return Optional.of("abcde"); } } diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java index f62c9c169a..df1cbc3b06 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java @@ -17,6 +17,7 @@ import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; import io.micronaut.security.authentication.Authentication; @@ -55,12 +56,14 @@ public class CsrfSessionLogingHandler extends SessionLoginHandler { * @param csrfConfiguration CSRF Configuration * @param csrfTokenGenerator CSRF Token Generator */ - public CsrfSessionLogingHandler(RedirectConfiguration redirectConfiguration, - SessionStore sessionStore, - PriorToLoginPersistence, - MutableHttpResponse> priorToLoginPersistence, - RedirectService redirectService, - CsrfConfiguration csrfConfiguration, CsrfTokenGenerator csrfTokenGenerator) { + public CsrfSessionLogingHandler( + RedirectConfiguration redirectConfiguration, + SessionStore sessionStore, + @Nullable PriorToLoginPersistence, + MutableHttpResponse> priorToLoginPersistence, + RedirectService redirectService, + CsrfConfiguration csrfConfiguration, + CsrfTokenGenerator csrfTokenGenerator) { super(redirectConfiguration, sessionStore, priorToLoginPersistence, redirectService); this.csrfConfiguration = csrfConfiguration; this.csrfTokenGenerator = csrfTokenGenerator; diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java b/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java index 07b90ec16c..ede3b88ef9 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java @@ -20,6 +20,7 @@ import io.micronaut.security.csrf.CsrfConfiguration; import io.micronaut.security.csrf.repository.CsrfTokenRepository; import io.micronaut.session.http.SessionForRequest; +import jakarta.inject.Singleton; import java.util.Optional; @@ -30,6 +31,7 @@ */ @Requires(classes = HttpRequest.class) @Requires(beans = CsrfConfiguration.class) +@Singleton public class SessionCsrfTokenRepository implements CsrfTokenRepository> { private final CsrfConfiguration csrfConfiguration; diff --git a/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java b/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java new file mode 100644 index 0000000000..a38fcd9a09 --- /dev/null +++ b/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java @@ -0,0 +1,127 @@ +package io.micronaut.security.session.csrf; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.*; +import io.micronaut.http.annotation.*; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.csrf.repository.CsrfTokenRepository; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider; +import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario; +import io.micronaut.serde.annotation.Serdeable; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@Property(name = "micronaut.security.authentication", value = "session") +@Property(name = "micronaut.security.redirect.enabled", value = StringUtils.FALSE) +@Property(name = "spec.name", value = "CsrfSessionLogingHandlerTest") +@MicronautTest +class CsrfSessionLogingHandlerTest { + + @Test + void loginSavesACsrfTokenInSession(@Client("/") HttpClient httpClient) { + BlockingHttpClient client = httpClient.toBlocking(); + HttpRequest csrfEcho = HttpRequest.GET("/csrf/echo"); + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(csrfEcho)); + assertEquals(HttpStatus.NOT_FOUND, ex.getStatus()); + + HttpRequest loginRequest = HttpRequest.POST("/login",Map.of("username", "sherlock", "password", "password")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + + HttpResponse loginRsp = assertDoesNotThrow(() -> client.exchange(loginRequest)); + assertEquals(HttpStatus.OK, loginRsp.getStatus()); + String cookie = loginRsp.getHeaders().get(HttpHeaders.SET_COOKIE); + assertNotNull(cookie); + assertTrue(cookie.contains("SESSION=")); + assertTrue(cookie.contains("; HTTPOnly")); + String sessionId = cookie.split(";")[0].split("=")[1]; + assertNotNull(sessionId); + HttpRequest csrfEchoRequestWithSession = HttpRequest.GET("/csrf/echo").cookie(Cookie.of("SESSION", sessionId)); + String csrfToken = assertDoesNotThrow(() -> client.retrieve(csrfEchoRequestWithSession)); + assertNotNull(csrfToken); + + PasswordChange form = new PasswordChange("sherlock", "evil"); + HttpRequest passwordChangeRequestNoSessionCookie = HttpRequest.POST("/password/change", form) + .accept(MediaType.TEXT_HTML) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(passwordChangeRequestNoSessionCookie)); + assertEquals(HttpStatus.UNAUTHORIZED, ex.getStatus()); + + PasswordChangeForm formWithCsrfToken = new PasswordChangeForm("sherlock", "evil", csrfToken); + HttpRequest passwordChangeRequestWithSessionCookie = HttpRequest.POST("/password/change", formWithCsrfToken) + .cookie(Cookie.of("SESSION", sessionId)) + .accept(MediaType.TEXT_HTML) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + HttpResponse passwordChangeRequestWithSessionCookieResponse = assertDoesNotThrow(() -> client.exchange(passwordChangeRequestWithSessionCookie, String.class)); + assertEquals(HttpStatus.OK, passwordChangeRequestWithSessionCookieResponse.getStatus()); + Optional htmlOptional = passwordChangeRequestWithSessionCookieResponse.getBody(); + assertTrue(htmlOptional.isPresent()); + assertEquals("sherlock", htmlOptional.get()); + } + + @Requires(property = "spec.name", value = "CsrfSessionLogingHandlerTest") + @Singleton + static class AuthenticationProviderUserPassword extends MockAuthenticationProvider { + AuthenticationProviderUserPassword() { + super(List.of(new SuccessAuthenticationScenario("sherlock"))); + } + } + + @Requires(property = "spec.name", value = "CsrfSessionLogingHandlerTest") + @Controller + static class PasswordChangeController { + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_HTML) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Post("/password/change") + String changePassword(@Body PasswordChange passwordChangeForm) { + return passwordChangeForm.username; + } + } + + @Serdeable + record PasswordChange( + String username, + String password) { + } + + @Serdeable + record PasswordChangeForm( + String username, + String password, + String csrfToken) { + } + + @Requires(property = "spec.name", value = "CsrfSessionLogingHandlerTest") + @Controller("/csrf") + static class CsrfTokenEchoController { + + private final CsrfTokenRepository> csrfTokenRepository; + + CsrfTokenEchoController(CsrfTokenRepository> csrfTokenRepository) { + this.csrfTokenRepository = csrfTokenRepository; + } + + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_PLAIN) + @Get("/echo") + Optional echo(HttpRequest request) { + return csrfTokenRepository.findCsrfToken(request); + } + } +} \ No newline at end of file From 7ea61ffe7da813cb2c5b8f6f2fac298954f30604 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 18 Oct 2024 13:20:17 +0200 Subject: [PATCH 015/108] CRSF more docs --- .../CsrfFilterConfigurationProperties.java | 5 +--- .../micronaut/security/csrf/package-info.java | 1 + .../csrf/repository/CsrfTokenRepository.java | 6 +++- .../csrf/resolver/CsrfTokenResolver.java | 1 + .../csrf/resolver/FieldCsrfTokenResolver.java | 4 +-- .../resolver/HttpHeaderCsrfTokenResolver.java | 6 ++++ .../csrf/resolver/CsrfTokenResolverTest.java | 28 +++++++++++++++++++ .../src/test/resources/logback.xml | 1 + src/main/docs/guide/csrf.adoc | 5 ++-- .../docs/guide/csrf/csrfConfiguration.adoc | 3 ++ src/main/docs/guide/csrf/csrfDependency.adoc | 3 ++ src/main/docs/guide/csrf/csrfMitigations.adoc | 2 ++ .../doubleSubmitCookiePattern.adoc | 1 + .../syncronizerTokenPattern.adoc | 1 + .../docs/guide/csrf/csrfTokenResolvers.adoc | 4 +-- src/main/docs/guide/toc.yml | 6 ++++ 16 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java create mode 100644 src/main/docs/guide/csrf/csrfConfiguration.adoc create mode 100644 src/main/docs/guide/csrf/csrfDependency.adoc create mode 100644 src/main/docs/guide/csrf/csrfMitigations.adoc create mode 100644 src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc create mode 100644 src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java index c634c46d46..f63665ffe1 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java @@ -22,8 +22,6 @@ import io.micronaut.http.HttpMethod; import io.micronaut.http.MediaType; import io.micronaut.security.config.SecurityConfigurationProperties; -import io.micronaut.security.endpoints.LoginControllerConfigurationProperties; - import java.util.Set; @Requires(classes = { HttpMethod.class, MediaType.class }) @@ -44,7 +42,6 @@ final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration @SuppressWarnings("WeakerAccess") public static final String DEFAULT_REGEX_PATTERN = "^(?!\\/(login|logout)).*$"; - private static final Set DEFAULT_METHODS = Set.of( HttpMethod.POST, HttpMethod.PUT, @@ -108,7 +105,7 @@ public String getRegexPattern() { } /** - * Filter will only process requests whose path matches this pattern. Default Value {@value #DEFAULT_REGEX_PATTERN}. + * CSRF filter processes only request paths matching this regular expression. Default Value {@value #DEFAULT_REGEX_PATTERN}. * @param regexPattern Regular expression pattern for the filter. */ public void setRegexPattern(String regexPattern) { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java index 5ab9264a0e..28844d4a6c 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java @@ -16,6 +16,7 @@ /** * Classes related to Cross Site Request Forgery (CSRF). * @see Cross Site Request Forgery (CSRF) + * @see CSRF Prevention Cheat Sheet * @author Sergio del Amo * @since 4.11.0 */ diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java index cf196ccc71..c8c4be85b4 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java @@ -23,6 +23,10 @@ */ @FunctionalInterface public interface CsrfTokenRepository { - + /** + * + * @param request Request + * @return A CSRF token or an empty optional. + */ Optional findCsrfToken(T request); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java index 65c967b313..9f7652a4b6 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java @@ -22,6 +22,7 @@ /** * Attempts to resolve a CSRF token from the provided request. + * {@link CsrfTokenResolver} is an {@link Ordered} api. Override the {@link #getOrder()} method to provide a custom order. * * @author Sergio del Amo * @since 1.1.0 diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 497c04a15e..49b481082c 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -37,10 +37,10 @@ */ @Requires(property = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton -public class FieldCsrfTokenResolver implements CsrfTokenResolver> { +class FieldCsrfTokenResolver implements CsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; - public FieldCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { + FieldCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { this.csrfConfiguration = csrfConfiguration; } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index 48d9bdd892..a4214eef5b 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -34,6 +34,7 @@ @Internal final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; + private final int ORDER = -100; HttpHeaderCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { this.csrfConfiguration = csrfConfiguration; @@ -51,4 +52,9 @@ public Optional resolveToken(HttpRequest request) { } return Optional.empty(); } + + @Override + public int getOrder() { + return ORDER; + } } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java new file mode 100644 index 0000000000..2e9ed16e8e --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java @@ -0,0 +1,28 @@ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.context.BeanContext; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@MicronautTest(startApplication = false) +class CsrfTokenResolverTest { + + @Inject + BeanContext beanContext; + @Test + void csrfTokenResolversOrder() { + Collection csrfTokenResolverCollection = beanContext.getBeansOfType(CsrfTokenResolver.class); + List csrfTokenResolverList = new ArrayList<>(csrfTokenResolverCollection); + assertEquals(2, csrfTokenResolverList.size()); + assertInstanceOf(HttpHeaderCsrfTokenResolver.class, csrfTokenResolverList.get(0)); + assertInstanceOf(FieldCsrfTokenResolver.class, csrfTokenResolverList.get(1)); + + } +} \ No newline at end of file diff --git a/security-session/src/test/resources/logback.xml b/security-session/src/test/resources/logback.xml index 432f5aa24e..67787a909e 100644 --- a/security-session/src/test/resources/logback.xml +++ b/security-session/src/test/resources/logback.xml @@ -7,4 +7,5 @@ + \ No newline at end of file diff --git a/src/main/docs/guide/csrf.adoc b/src/main/docs/guide/csrf.adoc index ed1c2231f1..ca01cf5e26 100644 --- a/src/main/docs/guide/csrf.adoc +++ b/src/main/docs/guide/csrf.adoc @@ -1,6 +1,7 @@ https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)]. -The following configuration options are available for CSRF: +____ +Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated +____ -include::{includedir}configurationProperties/io.micronaut.security.csrf.CsrfConfigurationProperties.adoc[] diff --git a/src/main/docs/guide/csrf/csrfConfiguration.adoc b/src/main/docs/guide/csrf/csrfConfiguration.adoc new file mode 100644 index 0000000000..2e57da285c --- /dev/null +++ b/src/main/docs/guide/csrf/csrfConfiguration.adoc @@ -0,0 +1,3 @@ +The following configuration options are available for CSRF: + +include::{includedir}configurationProperties/io.micronaut.security.csrf.CsrfConfigurationProperties.adoc[] diff --git a/src/main/docs/guide/csrf/csrfDependency.adoc b/src/main/docs/guide/csrf/csrfDependency.adoc new file mode 100644 index 0000000000..cd75033f3b --- /dev/null +++ b/src/main/docs/guide/csrf/csrfDependency.adoc @@ -0,0 +1,3 @@ +Add the Micronaut Security CSRF dependency to protect against CSRF: + +dependency:micronaut-security-csrf[groupId=io.micronaut.security] diff --git a/src/main/docs/guide/csrf/csrfMitigations.adoc b/src/main/docs/guide/csrf/csrfMitigations.adoc new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/src/main/docs/guide/csrf/csrfMitigations.adoc @@ -0,0 +1,2 @@ + + diff --git a/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc b/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc new file mode 100644 index 0000000000..09e04b7d19 --- /dev/null +++ b/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc @@ -0,0 +1 @@ +https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern[Double Submit Cookie Pattern]. \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc new file mode 100644 index 0000000000..07a0bd23e6 --- /dev/null +++ b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc @@ -0,0 +1 @@ +https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern[Syncronizer Token Pattern]. \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfTokenResolvers.adoc b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc index beaab92d97..16126ce13d 100644 --- a/src/main/docs/guide/csrf/csrfTokenResolvers.adoc +++ b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc @@ -1,11 +1,11 @@ |=== |Resolver | Enabled by Default | Disable with -|api:security.csrf.resolver:HttpHeaderCsrfTokenResolver[] +|`io.micronaut.security.csrf.resolver.HttpHeaderCsrfTokenResolver` | Yes | `micronaut.security.csrf.token-resolvers.http-header.enabled=false` -|api:security.csrf.resolver:FieldCsrfTokenResolver[] +| `io.micronaut.security.csrf.resolver.FieldCsrfTokenResolver` | Yes | `micronaut.security.csrf.token-resolvers.field.enabled=false` diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index b65a943d2c..53ec560208 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -85,6 +85,12 @@ authenticationStrategies: rejection: Rejection Handling csrf: title: Cross-Site Request Forgery (CSRF) + csrfDependency: CSRF Dependency + csrfMitigations: + title: CSRF Mitigations + syncronizerTokenPattern: Syncronizer Token Pattern + doubleSubmitCookiePattern: Double Submit Cookie Pattern + csrfConfiguration: CSRF Configuration csrfFilter: CSRF Filter csrfTokenResolvers: CSRF Token Resolvers tokenPropagation: Token Propagation From 06b3ec84b1690bde9974fac62297aa3463cc87a1 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 09:38:31 +0200 Subject: [PATCH 016/108] double cookie pattern --- security-csrf/build.gradle.kts | 2 + .../security/csrf/CsrfConfiguration.java | 4 +- .../csrf/CsrfConfigurationProperties.java | 111 ++++++++++++++- .../csrf/filter/CsrfFilterConfiguration.java | 1 - .../CsrfFilterConfigurationProperties.java | 4 +- .../CrsrfTokenCookieLoginHandler.java | 107 ++++++++++++++ .../repository/CsrfCookieTokenRepository.java | 45 ++++++ .../CsrfDoubleSubmitCookiePatternTest.java | 132 ++++++++++++++++++ .../csrf/CsrfSessionLogingHandlerTest.java | 1 - 9 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index c1e91e0c0c..57b5c8b274 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { testImplementation(mn.micronaut.http.client) testAnnotationProcessor(mnSerde.micronaut.serde.processor) testImplementation(mnSerde.micronaut.serde.jackson) + testImplementation(projects.testSuiteUtilsSecurity) + testImplementation(projects.micronautSecurityJwt) } tasks.withType { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index e8899d3a85..2a2609cf56 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -17,14 +17,14 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.Toggleable; +import io.micronaut.http.cookie.CookieConfiguration; /** * CSRF Configuration. * @author Sergio del Amo * @since 4.11.0 */ -public interface CsrfConfiguration extends Toggleable { - +public interface CsrfConfiguration extends CookieConfiguration, Toggleable { /** * * @return Random CSRF Token size in bytes. diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index d12ab1750e..4842204550 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -18,7 +18,13 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.security.config.SecurityConfigurationProperties; +import io.micronaut.security.token.generator.AccessTokenConfigurationProperties; + +import java.time.Duration; +import java.time.temporal.TemporalAmount; +import java.util.Optional; @Internal @ConfigurationProperties(CsrfConfigurationProperties.PREFIX) @@ -37,6 +43,12 @@ class CsrfConfigurationProperties implements CsrfConfiguration { @SuppressWarnings("WeakerAccess") public static final String DEFAULT_FIELD_NAME = "csrfToken"; + /** + * The default cookie name.. + */ + @SuppressWarnings("WeakerAccess") + public static final String DEFAULT_COOKIE_NAME = "csrfToken"; + /** * The default HTTP Session name. */ @@ -47,15 +59,21 @@ class CsrfConfigurationProperties implements CsrfConfiguration { public static final boolean DEFAULT_ENABLED = true; - private boolean enabled = DEFAULT_ENABLED; + private static final boolean DEFAULT_HTTPONLY = true; + private static final String DEFAULT_COOKIEPATH = "/"; + private static final Duration DEFAULT_MAX_AGE = Duration.ofSeconds(AccessTokenConfigurationProperties.DEFAULT_EXPIRATION); + private boolean enabled = DEFAULT_ENABLED; private String headerName = DEFAULT_HTTP_HEADER_NAME; - private String fieldName = DEFAULT_FIELD_NAME; - private int tokenSize = DEFAULT_TOKEN_SIZE; - private String httpSessionName = DEFAULT_HTTP_SESSION_NAME; + private String cookieDomain; + private Boolean cookieSecure; + private String cookiePath = DEFAULT_COOKIEPATH; + private Boolean cookieHttpOnly = DEFAULT_HTTPONLY; + private Duration cookieMaxAge = DEFAULT_MAX_AGE; + private String cookieName = DEFAULT_COOKIE_NAME; @Override public String getHttpSessionName() { @@ -122,4 +140,89 @@ public boolean isEnabled() { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + @Override + public Optional getCookieDomain() { + return Optional.ofNullable(cookieDomain); + } + + /** + * Sets the domain name of this Cookie. Default value (null). + * + * @param cookieDomain the domain name of this Cookie + */ + public void setCookieDomain(@Nullable String cookieDomain) { + this.cookieDomain = cookieDomain; + } + + @Override + public Optional isCookieSecure() { + return Optional.ofNullable(cookieSecure); + } + + /** + * Sets whether the cookie is secured. Defaults to the secure status of the request. + * + * @param cookieSecure True if the cookie is secure + */ + public void setCookieSecure(Boolean cookieSecure) { + this.cookieSecure = cookieSecure; + } + + @NonNull + @Override + public String getCookieName() { + return this.cookieName; + } + + /** + * Cookie Name. + * + * @param cookieName Cookie name + */ + public void setCookieName(@NonNull String cookieName) { + this.cookieName = cookieName; + } + + @Override + public Optional getCookiePath() { + return Optional.ofNullable(cookiePath); + } + + /** + * Sets the path of the cookie. Default value ({@value #DEFAULT_COOKIEPATH}). + * + * @param cookiePath The path of the cookie. + */ + public void setCookiePath(@Nullable String cookiePath) { + this.cookiePath = cookiePath; + } + + @Override + public Optional isCookieHttpOnly() { + return Optional.ofNullable(cookieHttpOnly); + } + + /** + * Whether the Cookie can only be accessed via HTTP. Default value ({@value #DEFAULT_HTTPONLY}). + * + * @param cookieHttpOnly Whether the Cookie can only be accessed via HTTP + */ + public void setCookieHttpOnly(Boolean cookieHttpOnly) { + this.cookieHttpOnly = cookieHttpOnly; + } + + @Override + public Optional getCookieMaxAge() { + return Optional.ofNullable(cookieMaxAge); + } + + /** + * Sets the maximum age of the cookie. Default value ({@value AccessTokenConfigurationProperties#DEFAULT_EXPIRATION} seconds). + * + * @param cookieMaxAge The maximum age of the cookie + */ + public void setCookieMaxAge(Duration cookieMaxAge) { + this.cookieMaxAge = cookieMaxAge; + } } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java index afb85008cb..3fd3c06415 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfiguration.java @@ -19,7 +19,6 @@ import io.micronaut.core.util.Toggleable; import io.micronaut.http.HttpMethod; import io.micronaut.http.MediaType; - import java.util.Set; /** diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java index f63665ffe1..cdc86535a3 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java @@ -28,7 +28,7 @@ @Internal @ConfigurationProperties(CsrfFilterConfigurationProperties.PREFIX) final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration { - public static final String PREFIX = SecurityConfigurationProperties.PREFIX + "csrf.filter"; + public static final String PREFIX = SecurityConfigurationProperties.PREFIX + ".csrf.filter"; /** * The default enable value. @@ -48,11 +48,11 @@ final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration HttpMethod.DELETE, HttpMethod.PATCH ); + private static final Set DEFAULT_CONTENT_TYPES = Set.of( MediaType.APPLICATION_FORM_URLENCODED_TYPE, MediaType.MULTIPART_FORM_DATA_TYPE ); - private boolean enabled = DEFAULT_ENABLED; private String regexPattern = DEFAULT_REGEX_PATTERN; private Set methods = DEFAULT_METHODS; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java new file mode 100644 index 0000000000..ad85a6f3b6 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.repository; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.config.RedirectConfiguration; +import io.micronaut.security.config.RedirectService; +import io.micronaut.security.config.SecurityConfigurationProperties; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.generator.CsrfTokenGenerator; +import io.micronaut.security.errors.PriorToLoginPersistence; +import io.micronaut.security.token.cookie.AccessTokenCookieConfiguration; +import io.micronaut.security.token.cookie.RefreshTokenCookieConfiguration; +import io.micronaut.security.token.cookie.TokenCookieLoginHandler; +import io.micronaut.security.token.generator.AccessRefreshTokenGenerator; +import io.micronaut.security.token.generator.AccessTokenConfiguration; +import jakarta.inject.Singleton; + +import java.util.List; + +/** + * Replaces {@link TokenCookieLoginHandler} to add an extra CSRF Cookie to the response. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Internal +@Requires(classes = { HttpRequest.class }) +@Requires(property = SecurityConfigurationProperties.PREFIX + ".authentication", value = "cookie") +@Replaces(TokenCookieLoginHandler.class) +@Singleton +public class CrsrfTokenCookieLoginHandler extends TokenCookieLoginHandler { + private final CsrfConfiguration csrfConfiguration; + private final CsrfTokenGenerator csrfTokenGenerator; + + /** + * @param redirectService Redirection Service + * @param redirectConfiguration Redirect configuration + * @param accessTokenCookieConfiguration JWT Access Token Cookie Configuration + * @param refreshTokenCookieConfiguration Refresh Token Cookie Configuration + * @param accessTokenConfiguration JWT Generator Configuration + * @param accessRefreshTokenGenerator Access Refresh Token Generator + * @param priorToLoginPersistence Prior To Login Persistence Mechanism + * @param csrfConfiguration CSRF Configuration + * @param csrfTokenGenerator CSRF Token Generator + */ + public CrsrfTokenCookieLoginHandler(RedirectService redirectService, + RedirectConfiguration redirectConfiguration, + AccessTokenCookieConfiguration accessTokenCookieConfiguration, + RefreshTokenCookieConfiguration refreshTokenCookieConfiguration, + AccessTokenConfiguration accessTokenConfiguration, + AccessRefreshTokenGenerator accessRefreshTokenGenerator, + @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, + CsrfConfiguration csrfConfiguration, + CsrfTokenGenerator csrfTokenGenerator) { + super(redirectService, redirectConfiguration, accessTokenCookieConfiguration, refreshTokenCookieConfiguration, accessTokenConfiguration, accessRefreshTokenGenerator, priorToLoginPersistence); + this.csrfConfiguration = csrfConfiguration; + this.csrfTokenGenerator = csrfTokenGenerator; + } + + @Override + public List getCookies(Authentication authentication, HttpRequest request) { + List cookies = super.getCookies(authentication, request); + cookies.add(csrfCookie(request)); + return cookies; + } + + @Override + public List getCookies(Authentication authentication, String refreshToken, HttpRequest request) { + List cookies = super.getCookies(authentication, refreshToken, request); + cookies.add(csrfCookie(request)); + return cookies; + } + + @NonNull + private Cookie csrfCookie(@NonNull HttpRequest request) { + String csrfToken = csrfTokenGenerator.generate(); + return csrfCookie(csrfToken, request); + } + + @NonNull + private Cookie csrfCookie(@NonNull String csrfToken, @NonNull HttpRequest request) { + Cookie cookie = Cookie.of(csrfConfiguration.getCookieName(), csrfToken); + cookie.configure(csrfConfiguration, request.isSecure()); + return cookie; + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java new file mode 100644 index 0000000000..3cc63a1f26 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.repository; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.csrf.CsrfConfiguration; +import jakarta.inject.Singleton; +import java.util.Optional; + +/** + * Implementation of {@link CsrfTokenRepository}, which retrieves a CSRF token from a cookie value. + * It is used within a Double-Submit Cookie Pattern. + * + * @author Sergio del Amo + * @since 4.11.0 + */ +@Singleton +public class CsrfCookieTokenRepository implements CsrfTokenRepository> { + private final CsrfConfiguration csrfConfiguration; + + public CsrfCookieTokenRepository(CsrfConfiguration csrfConfiguration) { + this.csrfConfiguration = csrfConfiguration; + } + + @Override + public Optional findCsrfToken(HttpRequest request) { + return request.getCookies() + .findCookie(csrfConfiguration.getCookieName()) + .map(Cookie::getValue); + } +} diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java new file mode 100644 index 0000000000..6b3ee86d3b --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -0,0 +1,132 @@ +package io.micronaut.security.csrf.repository; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.*; +import io.micronaut.http.annotation.*; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider; +import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario; +import io.micronaut.serde.annotation.Serdeable; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "micronaut.security.authentication", value = "cookie") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "pleaseChangeThisSecretForANewOne") +@Property(name = "micronaut.security.redirect.enabled", value = StringUtils.FALSE) +@Property(name = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") +@MicronautTest +class CsrfDoubleSubmitCookiePatternTest { + + @Test + void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient) { + BlockingHttpClient client = httpClient.toBlocking(); + HttpRequest csrfEcho = HttpRequest.GET("/csrf/echo"); + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(csrfEcho)); + assertEquals(HttpStatus.NOT_FOUND, ex.getStatus()); + + HttpRequest loginRequest = HttpRequest.POST("/login",Map.of("username", "sherlock", "password", "password")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + + HttpResponse loginRsp = assertDoesNotThrow(() -> client.exchange(loginRequest)); + assertEquals(HttpStatus.OK, loginRsp.getStatus()); + Optional cookieJwtOptional = loginRsp.getCookie("JWT"); + assertTrue(cookieJwtOptional.isPresent()); + Cookie cookieJwt = cookieJwtOptional.get(); + Optional cookieCsrfTokenOptional = loginRsp.getCookie("csrfToken"); + assertTrue(cookieCsrfTokenOptional.isPresent()); + Cookie cookieCsrfToken = cookieCsrfTokenOptional.get(); + + HttpRequest csrfEchoRequestWithSession = HttpRequest.GET("/csrf/echo") + .cookie(Cookie.of("JWT", cookieJwt.getValue())) + .cookie(Cookie.of("csrfToken", cookieCsrfToken.getValue())); + String csrfToken = assertDoesNotThrow(() -> client.retrieve(csrfEchoRequestWithSession)); + assertNotNull(csrfToken); + + PasswordChange form = new PasswordChange("sherlock", "evil"); + HttpRequest passwordChangeRequestNoSessionCookie = HttpRequest.POST("/password/change", form) + .cookie(Cookie.of("JWT", cookieJwt.getValue())) + .cookie(Cookie.of("csrfToken", cookieCsrfToken.getValue())) + .accept(MediaType.TEXT_HTML) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(passwordChangeRequestNoSessionCookie)); + assertEquals(HttpStatus.FORBIDDEN, ex.getStatus()); + + PasswordChangeForm formWithCsrfToken = new PasswordChangeForm("sherlock", "evil", csrfToken); + HttpRequest passwordChangeRequestWithSessionCookie = HttpRequest.POST("/password/change", formWithCsrfToken) + .cookie(Cookie.of("JWT", cookieJwt.getValue())) + .cookie(Cookie.of("csrfToken", cookieCsrfToken.getValue())) + .accept(MediaType.TEXT_HTML) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + HttpResponse passwordChangeRequestWithSessionCookieResponse = assertDoesNotThrow(() -> client.exchange(passwordChangeRequestWithSessionCookie, String.class)); + assertEquals(HttpStatus.OK, passwordChangeRequestWithSessionCookieResponse.getStatus()); + Optional htmlOptional = passwordChangeRequestWithSessionCookieResponse.getBody(); + assertTrue(htmlOptional.isPresent()); + assertEquals("sherlock", htmlOptional.get()); + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") + @Singleton + static class AuthenticationProviderUserPassword extends MockAuthenticationProvider { + AuthenticationProviderUserPassword() { + super(List.of(new SuccessAuthenticationScenario("sherlock"))); + } + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") + @Controller + static class PasswordChangeController { + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_HTML) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Post("/password/change") + String changePassword(@Body PasswordChange passwordChangeForm) { + return passwordChangeForm.username; + } + } + + @Serdeable + record PasswordChange( + String username, + String password) { + } + + @Serdeable + record PasswordChangeForm( + String username, + String password, + String csrfToken) { + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") + @Controller("/csrf") + static class CsrfTokenEchoController { + + private final CsrfTokenRepository> csrfTokenRepository; + + CsrfTokenEchoController(CsrfTokenRepository> csrfTokenRepository) { + this.csrfTokenRepository = csrfTokenRepository; + } + + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_PLAIN) + @Get("/echo") + Optional echo(HttpRequest request) { + return csrfTokenRepository.findCsrfToken(request); + } + } +} \ No newline at end of file diff --git a/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java b/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java index a38fcd9a09..3df44235a9 100644 --- a/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java +++ b/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java @@ -2,7 +2,6 @@ import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.StringUtils; import io.micronaut.http.*; import io.micronaut.http.annotation.*; From bb1131d221c8aa0bcde6a97165846039228c5227 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 12:58:57 +0200 Subject: [PATCH 017/108] more implementation --- .../security/csrf/CsrfConfiguration.java | 6 + .../csrf/CsrfConfigurationProperties.java | 14 +++ .../csrf/generator/CsrfTokenGenerator.java | 7 +- .../generator/DefaultCsrfTokenGenerator.java | 85 +++++++++++++- .../CrsrfTokenCookieLoginHandler.java | 2 +- .../CompositeCsrfTokenValidator.java | 51 +++++++++ .../csrf/validator/CsrfTokenValidator.java | 6 +- .../generator/CsrfTokenGeneratorTest.java | 6 +- .../csrf/generator/CsrfTokenSignerTest.java | 16 +++ .../CsrfDoubleSubmitCookiePatternTest.java | 86 ++++++++++---- security-csrf/src/test/resources/logback.xml | 1 + .../JsonWebTokenIdSessionIdResolver.java | 49 ++++++++ security-oauth2/build.gradle.kts | 1 + .../response/CsrfIdTokenLoginHandler.java | 106 ++++++++++++++++++ .../csrf/HttpSessionSessionIdResolver.java | 26 ++--- .../session/CompositeSessionIdResolver.java | 50 +++++++++ .../security/session/SessionIdResolver.java | 37 ++++++ .../security/session/package-info.java | 20 ++++ .../micronaut/security/utils/HMacUtils.java | 67 +++++++++++ .../security/utils/HMacUtilsTest.java | 22 ++++ 20 files changed, 605 insertions(+), 53 deletions(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/validator/CompositeCsrfTokenValidator.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java create mode 100644 security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java create mode 100644 security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java rename security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java => security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java (50%) create mode 100644 security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java create mode 100644 security/src/main/java/io/micronaut/security/session/SessionIdResolver.java create mode 100644 security/src/main/java/io/micronaut/security/session/package-info.java create mode 100644 security/src/main/java/io/micronaut/security/utils/HMacUtils.java create mode 100644 security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index 2a2609cf56..a199779c11 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -31,6 +31,12 @@ public interface CsrfConfiguration extends CookieConfiguration, Toggleable { */ int getTokenSize(); + /** + * + * @return CSRF token HMAC signature key + */ + String getSignatureKey(); + /** * * @return HTTP Header name to look for the CSRF token. diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index 4842204550..eda8b6c5c5 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -74,6 +74,20 @@ class CsrfConfigurationProperties implements CsrfConfiguration { private Boolean cookieHttpOnly = DEFAULT_HTTPONLY; private Duration cookieMaxAge = DEFAULT_MAX_AGE; private String cookieName = DEFAULT_COOKIE_NAME; + private String signatureKey; + + @Override + public String getSignatureKey() { + return signatureKey; + } + + /** + * CSRF token HMAC signature key. + * @param signatureKey CSRF token HMAC signature key + */ + public void setSignatureKey(String signatureKey) { + this.signatureKey = signatureKey; + } @Override public String getHttpSessionName() { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java index 234309dabf..4eac7550bf 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java @@ -21,14 +21,15 @@ * CSRF token Generation. * @author Sergio del Amo * @since 4.11.0 + * @param Request */ @DefaultImplementation(DefaultCsrfTokenGenerator.class) @FunctionalInterface -public interface CsrfTokenGenerator { +public interface CsrfTokenGenerator { /** - * + * @param request Request * @return A CSRF Token. */ - String generate(); + String generate(T request); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index 895db669e9..16491c3186 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -15,12 +15,24 @@ */ package io.micronaut.security.csrf.generator; +import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.cookie.Cookie; import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.validator.CsrfTokenValidator; +import io.micronaut.security.session.SessionIdResolver; +import io.micronaut.security.utils.HMacUtils; import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; +import java.util.Optional; /** * Default implementation of {@link CsrfTokenGenerator} which generates a random base 64 encoded string using an instance of {@link SecureRandom} and random byte array of size {@link CsrfConfiguration#getTokenSize()}. @@ -29,19 +41,82 @@ */ @Singleton @Internal -final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator { - +final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator>, CsrfTokenValidator> { + private static final Logger LOG = LoggerFactory.getLogger(DefaultCsrfTokenGenerator.class); + private static final String SESSION_RANDOM_SEPARATOR = "!"; + private static final String HMAC_RANDOM_SEPARATOR = "."; private final SecureRandom secureRandom = new SecureRandom(); private final CsrfConfiguration csrfConfiguration; + private final SessionIdResolver> sessionIdResolver; - DefaultCsrfTokenGenerator(CsrfConfiguration csrfConfiguration) { + DefaultCsrfTokenGenerator(CsrfConfiguration csrfConfiguration, + SessionIdResolver> sessionIdResolver) { this.csrfConfiguration = csrfConfiguration; + this.sessionIdResolver = sessionIdResolver; } @Override - public String generate() { + public String generate(HttpRequest request) { + // Gather the values + String secret = csrfConfiguration.getSignatureKey(); + String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session byte[] tokenBytes = new byte[csrfConfiguration.getTokenSize()]; secureRandom.nextBytes(tokenBytes); - return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes); + String randomValue = Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes); // Cryptographic random value + + // Create the CSRF Token + String message = sessionID + SESSION_RANDOM_SEPARATOR + randomValue; // HMAC message payload + try { + String hmac = secret != null + ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash + : ""; + // Add the `randomValue` to the HMAC hash to create the final CSRF token. Avoid using the `message` because it contains the sessionID in plain text, which the server already stores separately. + return hmac + HMAC_RANDOM_SEPARATOR + randomValue; + } catch (InvalidKeyException ex) { + throw new ConfigurationException("Invalid secret key for signing the CSRF token"); + } catch (NoSuchAlgorithmException ex) { + throw new ConfigurationException("Invalid algorithm for signing the CSRF token"); + } + } + + @Override + public boolean validateCsrfToken(@NonNull HttpRequest request, @NonNull String token) { + Optional csrfCookieOptional = findCsrfToken(request); + if (csrfCookieOptional.isEmpty()) { + return false; + } + String csrfCookie = csrfCookieOptional.get(); + return validateHmac(request, csrfCookie) && csrfCookie.equals(token); + } + + private boolean validateHmac(HttpRequest request, @NonNull String csrfToken) { + try { + String[] arr = csrfToken.split("\\."); + if (arr.length != 2) { + if (LOG.isWarnEnabled()) { + LOG.warn("Invalid CSRF token: {}", csrfToken); + } + return false; + } + String hmac = arr[0]; + String randomValue = arr[1]; + String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session + String message = sessionID + SESSION_RANDOM_SEPARATOR + randomValue; + String secret = csrfConfiguration.getSignatureKey(); + String expectedHmac = secret != null + ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash + : ""; + return hmac.contains(expectedHmac); + } catch (InvalidKeyException ex) { + throw new ConfigurationException("Invalid secret key for signing the CSRF token"); + } catch (NoSuchAlgorithmException ex) { + throw new ConfigurationException("Invalid algorithm for signing the CSRF token"); + } + } + + private Optional findCsrfToken(HttpRequest request) { + return request.getCookies() + .findCookie(csrfConfiguration.getCookieName()) + .map(Cookie::getValue); } } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java index ad85a6f3b6..450a03cdf8 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java @@ -94,7 +94,7 @@ public List getCookies(Authentication authentication, String refreshToke @NonNull private Cookie csrfCookie(@NonNull HttpRequest request) { - String csrfToken = csrfTokenGenerator.generate(); + String csrfToken = csrfTokenGenerator.generate(request); return csrfCookie(csrfToken, request); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CompositeCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CompositeCsrfTokenValidator.java new file mode 100644 index 0000000000..cc1d839291 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CompositeCsrfTokenValidator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.validator; + +import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; +import java.util.List; + +/** + * Composite Pattern implementation of {@link CsrfTokenValidator}. + * @see Composite Pattern + * @param Request + */ +@Primary +@Singleton +public class CompositeCsrfTokenValidator implements CsrfTokenValidator { + + private final List> csrfTokenValidators; + + /** + * + * @param csrfTokenValidators CSRF Token Validators + */ + public CompositeCsrfTokenValidator(List> csrfTokenValidators) { + this.csrfTokenValidators = csrfTokenValidators; + } + + @Override + public boolean validateCsrfToken(@NonNull T request, @NonNull String csrfToken) { + for (CsrfTokenValidator csrfTokenValidator : csrfTokenValidators) { + if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { + return true; + } + } + return false; + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java index af8b4778ee..a2d292b855 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java @@ -16,7 +16,7 @@ package io.micronaut.security.csrf.validator; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.order.Ordered; /** * CSRF Token Validation. @@ -25,12 +25,12 @@ * @param request */ @FunctionalInterface -public interface CsrfTokenValidator { +public interface CsrfTokenValidator extends Ordered { /** * * @param request Request * @param token CSRF Token * @return Whether the CSRF token is valid */ - boolean validateCsrfToken(@Nullable T request, @NonNull String token); + boolean validateCsrfToken(@NonNull T request, @NonNull String token); } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java index f419317633..5e02680020 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java @@ -1,5 +1,8 @@ package io.micronaut.security.csrf.generator; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.simple.SimpleHttpRequest; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import org.junit.jupiter.api.Test; @@ -14,9 +17,10 @@ class CsrfTokenGeneratorTest { @Test void generatedCsrfTokensAreUnique(CsrfTokenGenerator csrfTokenGenerator) { int attempts = 100; + HttpRequest request = new SimpleHttpRequest<>(HttpMethod.POST, "/password/change", "usenrame=sherlock&password=123456"); Set results = new HashSet<>(); for (int i = 0; i < attempts; i++) { - results.add(csrfTokenGenerator.generate()); + results.add(csrfTokenGenerator.generate(request)); } assertEquals(attempts, results.size()); } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java new file mode 100644 index 0000000000..d2b5aa3e2d --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java @@ -0,0 +1,16 @@ +package io.micronaut.security.csrf.generator; + +import io.micronaut.context.annotation.Property; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +@Property(name = "micronaut.security.csrf.signature-key", value = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") +@MicronautTest(startApplication = false) +class CsrfTokenSignerTest { + + @Test + void tokenIsSigned() { + + } + +} \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java index 6b3ee86d3b..aa8ee44aeb 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -11,14 +11,20 @@ import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.http.cookie.Cookie; import io.micronaut.security.annotation.Secured; +import io.micronaut.security.csrf.CsrfConfiguration; import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.session.SessionIdResolver; import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider; import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario; +import io.micronaut.security.token.cookie.TokenCookieConfigurationProperties; +import io.micronaut.security.utils.HMacUtils; import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; import java.util.Optional; @@ -27,17 +33,17 @@ @Property(name = "micronaut.security.authentication", value = "cookie") @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "pleaseChangeThisSecretForANewOne") +@Property(name = "micronaut.security.csrf.signature-key", value = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") @Property(name = "micronaut.security.redirect.enabled", value = StringUtils.FALSE) @Property(name = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") @MicronautTest class CsrfDoubleSubmitCookiePatternTest { + public static final String FIX_SESSION_ID = "123456789"; @Test - void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient) { + void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, + CsrfConfiguration csrfConfiguration) throws NoSuchAlgorithmException, InvalidKeyException { BlockingHttpClient client = httpClient.toBlocking(); - HttpRequest csrfEcho = HttpRequest.GET("/csrf/echo"); - HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(csrfEcho)); - assertEquals(HttpStatus.NOT_FOUND, ex.getStatus()); HttpRequest loginRequest = HttpRequest.POST("/login",Map.of("username", "sherlock", "password", "password")) .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); @@ -50,33 +56,67 @@ void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient) { Optional cookieCsrfTokenOptional = loginRsp.getCookie("csrfToken"); assertTrue(cookieCsrfTokenOptional.isPresent()); Cookie cookieCsrfToken = cookieCsrfTokenOptional.get(); + String csrfTokenCookieName = "csrfToken"; - HttpRequest csrfEchoRequestWithSession = HttpRequest.GET("/csrf/echo") - .cookie(Cookie.of("JWT", cookieJwt.getValue())) - .cookie(Cookie.of("csrfToken", cookieCsrfToken.getValue())); - String csrfToken = assertDoesNotThrow(() -> client.retrieve(csrfEchoRequestWithSession)); - assertNotNull(csrfToken); + // CSRF Only in the cookie, not in the request headers or field, request is denied + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, new PasswordChange("sherlock", "evil"), cookieCsrfToken.getValue()); - PasswordChange form = new PasswordChange("sherlock", "evil"); - HttpRequest passwordChangeRequestNoSessionCookie = HttpRequest.POST("/password/change", form) - .cookie(Cookie.of("JWT", cookieJwt.getValue())) - .cookie(Cookie.of("csrfToken", cookieCsrfToken.getValue())) + // CSRF Token in request and in cookie don't match, request is unauthorized + String csrfToken = "abcdefg"; + assertNotEquals(cookieCsrfToken.getValue(), csrfToken); + PasswordChangeForm formWithCsrfToken = new PasswordChangeForm("sherlock", "evil", csrfToken); + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, formWithCsrfToken, csrfToken); + + // CSRF Token with HMAC but not session id feed into HMAC calculation, request is unauthorized + String randomValue = "abcdefg"; + String hmac = HMacUtils.base64EncodedHmacSha256(randomValue, csrfConfiguration.getSignatureKey()); + String csrfTokenCalculatedWithoutSessionId = hmac + "." + randomValue; + PasswordChangeForm body = new PasswordChangeForm("sherlock", "evil", csrfTokenCalculatedWithoutSessionId); + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, body, csrfToken); + + String message = FIX_SESSION_ID + "!" + randomValue; + hmac = HMacUtils.base64EncodedHmacSha256(message, csrfConfiguration.getSignatureKey()); + csrfToken = hmac + "." + randomValue; + assertOk(client, cookieJwt.getValue(), csrfTokenCookieName, csrfToken); + + // Even if you have the same session id and random value, the attacker cannot generate the same hmac as he does not have the same secret key + String evilSignatureKey = "evilAyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAowevil"; + csrfToken = HMacUtils.base64EncodedHmacSha256(message, evilSignatureKey); + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, new PasswordChangeForm("sherlock", "evil", csrfToken), csrfToken); + + // CSRF Token in request match token in cookie and hmac signature is valid. + csrfToken = cookieCsrfToken.getValue(); + assertOk(client, cookieJwt.getValue(), csrfTokenCookieName, csrfToken); + } + + private void assertDenied(BlockingHttpClient client, String cookieJwt, String csrfTokenCookieName, Object body, String csrfToken) { + HttpRequest request = HttpRequest.POST("/password/change", body) + .cookie(Cookie.of(TokenCookieConfigurationProperties.DEFAULT_COOKIENAME, cookieJwt)) + .cookie(Cookie.of(csrfTokenCookieName, csrfToken)) .accept(MediaType.TEXT_HTML) .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); - ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(passwordChangeRequestNoSessionCookie)); + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(request)); assertEquals(HttpStatus.FORBIDDEN, ex.getStatus()); + } - PasswordChangeForm formWithCsrfToken = new PasswordChangeForm("sherlock", "evil", csrfToken); - HttpRequest passwordChangeRequestWithSessionCookie = HttpRequest.POST("/password/change", formWithCsrfToken) - .cookie(Cookie.of("JWT", cookieJwt.getValue())) - .cookie(Cookie.of("csrfToken", cookieCsrfToken.getValue())) + private void assertOk(BlockingHttpClient client, String cookieJwt, String csrfTokenCookieName, String csrfToken) { + PasswordChangeForm body = new PasswordChangeForm("sherlock", "evil", csrfToken); + HttpRequest request = HttpRequest.POST("/password/change", body) + .cookie(Cookie.of(TokenCookieConfigurationProperties.DEFAULT_COOKIENAME, cookieJwt)) + .cookie(Cookie.of(csrfTokenCookieName, csrfToken)) .accept(MediaType.TEXT_HTML) .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); - HttpResponse passwordChangeRequestWithSessionCookieResponse = assertDoesNotThrow(() -> client.exchange(passwordChangeRequestWithSessionCookie, String.class)); - assertEquals(HttpStatus.OK, passwordChangeRequestWithSessionCookieResponse.getStatus()); - Optional htmlOptional = passwordChangeRequestWithSessionCookieResponse.getBody(); - assertTrue(htmlOptional.isPresent()); - assertEquals("sherlock", htmlOptional.get()); + HttpResponse response = assertDoesNotThrow(() -> client.exchange(request, String.class)); + assertEquals(HttpStatus.OK, response.getStatus()); + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") + @Singleton + static class MockSessionIdResolver implements SessionIdResolver> { + @Override + public Optional findSessionId(HttpRequest request) { + return Optional.of(FIX_SESSION_ID); + } } @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") diff --git a/security-csrf/src/test/resources/logback.xml b/security-csrf/src/test/resources/logback.xml index 432f5aa24e..67787a909e 100644 --- a/security-csrf/src/test/resources/logback.xml +++ b/security-csrf/src/test/resources/logback.xml @@ -7,4 +7,5 @@ + \ No newline at end of file diff --git a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java new file mode 100644 index 0000000000..39cf0294b5 --- /dev/null +++ b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.token.jwt.validator; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.security.session.SessionIdResolver; +import jakarta.inject.Singleton; + +import java.util.Optional; + +import static io.micronaut.security.filters.SecurityFilter.TOKEN; +import static io.micronaut.security.token.Claims.TOKEN_ID; + +/** + * Implementation of {@link SessionIdResolver} that returns the jti claim JWT ID if a JWT token is associated with the request. + * + * @since 4.11.0 + * @author Sergio del Amo + */ +@Requires(bean = JsonWebTokenParser.class) +@Singleton +public class JsonWebTokenIdSessionIdResolver implements SessionIdResolver> { + private final JsonWebTokenParser jsonWebTokenParser; + + public JsonWebTokenIdSessionIdResolver(JsonWebTokenParser jsonWebTokenParser) { + this.jsonWebTokenParser = jsonWebTokenParser; + } + + @Override + public Optional findSessionId(HttpRequest request) { + return request.getAttribute(TOKEN, String.class) + .flatMap(jsonWebTokenParser::parseClaims) + .flatMap(claims -> Optional.ofNullable(claims.get(TOKEN_ID)).map(Object::toString)); + } +} diff --git a/security-oauth2/build.gradle.kts b/security-oauth2/build.gradle.kts index 2f235e0fa2..7d7570afad 100644 --- a/security-oauth2/build.gradle.kts +++ b/security-oauth2/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { testImplementation(mnValidation.micronaut.validation) compileOnly(mn.micronaut.inject.java) compileOnly(projects.micronautSecurityJwt) + compileOnly(projects.micronautSecurityCsrf) compileOnly(mn.micronaut.http.server) api(projects.micronautSecurity) implementation(mn.micronaut.http.client.core) diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java new file mode 100644 index 0000000000..1ee25b6fe9 --- /dev/null +++ b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.oauth2.endpoint.token.response; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.authentication.AuthenticationMode; +import io.micronaut.security.config.RedirectConfiguration; +import io.micronaut.security.config.RedirectService; +import io.micronaut.security.config.SecurityConfigurationProperties; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.generator.CsrfTokenGenerator; +import io.micronaut.security.csrf.validator.CsrfTokenValidator; +import io.micronaut.security.errors.OauthErrorResponseException; +import io.micronaut.security.errors.ObtainingAuthorizationErrorCode; +import io.micronaut.security.errors.PriorToLoginPersistence; +import io.micronaut.security.token.cookie.AccessTokenCookieConfiguration; +import io.micronaut.security.token.cookie.CookieLoginHandler; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.ParseException; +import java.time.Duration; +import java.util.*; + +/** + * Sets {@link CookieLoginHandler}`s cookie value to the idtoken received from an authentication provider. + * The cookie expiration is set to the expiration of the IDToken exp claim. + * + * @author Sergio del Amo + * @since 2.0.0 + */ +@Requires(property = SecurityConfigurationProperties.PREFIX + ".authentication", value = "idtoken") +@Requires(classes = HttpRequest.class) +@Requires(beans = { CsrfConfiguration.class, CsrfTokenGenerator.class }) +@Replaces(IdTokenLoginHandler.class) +@Singleton +public class CsrfIdTokenLoginHandler extends IdTokenLoginHandler { + private final CsrfTokenGenerator> csrfTokenGenerator; + private final CsrfConfiguration csrfConfiguration; + /** + * @param accessTokenCookieConfiguration Access token cookie configuration + * @param redirectConfiguration Redirect configuration + * @param redirectService Redirect service + * @param priorToLoginPersistence The prior to login persistence strategy + */ + public CsrfIdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfiguration, + RedirectConfiguration redirectConfiguration, + RedirectService redirectService, + PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, + CsrfTokenGenerator> csrfTokenGenerator, + CsrfConfiguration csrfConfiguration) { + super(accessTokenCookieConfiguration, redirectConfiguration, redirectService, priorToLoginPersistence); + this.csrfTokenGenerator = csrfTokenGenerator; + this.csrfConfiguration = csrfConfiguration; + } + + @Override + public List getCookies(Authentication authentication, HttpRequest request) { + List cookies = super.getCookies(authentication, request); + cookies.add(csrfCookie(request)); + return cookies; + } + + @Override + public List getCookies(Authentication authentication, String refreshToken, HttpRequest request) { + List cookies = super.getCookies(authentication, refreshToken, request); + cookies.add(csrfCookie(request)); + return cookies; + } + + @NonNull + private Cookie csrfCookie(@NonNull HttpRequest request) { + String csrfToken = csrfTokenGenerator.generate(request); + return csrfCookie(csrfToken, request); + } + + @NonNull + private Cookie csrfCookie(@NonNull String csrfToken, @NonNull HttpRequest request) { + Cookie cookie = Cookie.of(csrfConfiguration.getCookieName(), csrfToken); + cookie.configure(csrfConfiguration, request.isSecure()); + return cookie; + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java b/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java similarity index 50% rename from security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java rename to security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java index 3cc63a1f26..b2f284fcc0 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfCookieTokenRepository.java +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java @@ -13,33 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.security.csrf.repository; +package io.micronaut.security.session.csrf; import io.micronaut.http.HttpRequest; -import io.micronaut.http.cookie.Cookie; -import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.session.SessionIdResolver; +import io.micronaut.session.Session; +import io.micronaut.session.http.SessionForRequest; import jakarta.inject.Singleton; + import java.util.Optional; /** - * Implementation of {@link CsrfTokenRepository}, which retrieves a CSRF token from a cookie value. - * It is used within a Double-Submit Cookie Pattern. - * + * Implementation of {@link SessionIdResolver} that returns {@link Session#getId()} if an HTTP session is associated with the request. * @author Sergio del Amo * @since 4.11.0 */ @Singleton -public class CsrfCookieTokenRepository implements CsrfTokenRepository> { - private final CsrfConfiguration csrfConfiguration; - - public CsrfCookieTokenRepository(CsrfConfiguration csrfConfiguration) { - this.csrfConfiguration = csrfConfiguration; - } - +public class HttpSessionSessionIdResolver implements SessionIdResolver> { @Override - public Optional findCsrfToken(HttpRequest request) { - return request.getCookies() - .findCookie(csrfConfiguration.getCookieName()) - .map(Cookie::getValue); + public Optional findSessionId(HttpRequest request) { + return SessionForRequest.find(request).map(Session::getId); } } diff --git a/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java new file mode 100644 index 0000000000..bd7727da14 --- /dev/null +++ b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.session; + +import io.micronaut.context.annotation.Primary; +import jakarta.inject.Singleton; +import java.util.List; +import java.util.Optional; + +/** + * Composite Pattern implementation of {@link SessionIdResolver}. + * @see Composite Pattern + * @param Request + */ +@Primary +@Singleton +public class CompositeSessionIdResolver implements SessionIdResolver { + + private final List> sessionIdResolvers; + + /** + * + * @param sessionIdResolvers List of session id resolvers + */ + public CompositeSessionIdResolver(List> sessionIdResolvers) { + this.sessionIdResolvers = sessionIdResolvers; + } + + @Override + public Optional findSessionId(T request) { + return sessionIdResolvers.stream() + .map(sessionIdResolver -> sessionIdResolver.findSessionId(request)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } +} diff --git a/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java new file mode 100644 index 0000000000..a01cda90aa --- /dev/null +++ b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.session; + + +import io.micronaut.core.order.Ordered; + +import java.util.Optional; + +/** + * API to resolve a session id for a given request. A session ID could be an HTTP Session ID but also a JSON Web Token Identifier in a token based state-less authentication. + * @author Sergio del Amo + * @since 4.11.0 + * @param Request + */ +public interface SessionIdResolver extends Ordered { + + /** + * + * @param request Request + * @return Session ID for the given request. Empty if no session ID was found. + */ + Optional findSessionId(T request); +} diff --git a/security/src/main/java/io/micronaut/security/session/package-info.java b/security/src/main/java/io/micronaut/security/session/package-info.java new file mode 100644 index 0000000000..f0d84ae2ff --- /dev/null +++ b/security/src/main/java/io/micronaut/security/session/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @author Sergio del Amo + * @since 4.11.0 + */ +package io.micronaut.security.session; \ No newline at end of file diff --git a/security/src/main/java/io/micronaut/security/utils/HMacUtils.java b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java new file mode 100644 index 0000000000..45aafe8c94 --- /dev/null +++ b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.utils; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * Utility methods for HMAC. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Internal +public final class HMacUtils { + private static final String HMAC_SHA256 = "HmacSHA256"; + + private HMacUtils() { + } + + /** + * + * @param data Data + * @param key Signature Key + * @return HMAC SHA-256 encoded in Base64 + * @throws NoSuchAlgorithmException if no {@code Provider} supports a {@code MacSpi} implementation for the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing this MAC. + */ + public static String base64EncodedHmacSha256(@NonNull String data, @NonNull String key) throws NoSuchAlgorithmException, InvalidKeyException { + return base64EncodedHmac(HMAC_SHA256, data, key); + } + + /** + * + * @param algorithm HMAC algorithm + * @param data Data + * @param key Signature Key + * @return HMAC encoded in Base64 + * @throws NoSuchAlgorithmException if no {@code Provider} supports a {@code MacSpi} implementation for the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing this MAC. + */ + public static String base64EncodedHmac(@NonNull String algorithm, @NonNull String data, @NonNull String key) + throws NoSuchAlgorithmException, InvalidKeyException { + SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm); + Mac mac = Mac.getInstance(algorithm); + mac.init(secretKeySpec); + return Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(data.getBytes())); + } +} diff --git a/security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java b/security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java new file mode 100644 index 0000000000..0a6a1ca0d4 --- /dev/null +++ b/security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java @@ -0,0 +1,22 @@ +package io.micronaut.security.utils; + +import org.junit.jupiter.api.Test; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.*; + +class HMacUtilsTest { + + @Test + void testHmacSha256() throws NoSuchAlgorithmException, InvalidKeyException { + String data = "abcdedf"; + String signatureKey = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"; + String hmac = HMacUtils.base64EncodedHmacSha256(data, signatureKey); + Assertions.assertNotNull(hmac); + Assertions.assertEquals(hmac, HMacUtils.base64EncodedHmacSha256(data, signatureKey)); + Assertions.assertNotEquals(hmac, HMacUtils.base64EncodedHmacSha256("foobar", signatureKey)); + Assertions.assertNotEquals(hmac, HMacUtils.base64EncodedHmacSha256(data, signatureKey + "evil")); + } +} \ No newline at end of file From bcd2216faac61adbd32543e3d5b27b399448343c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 15:05:28 +0200 Subject: [PATCH 018/108] more docs --- .../security/csrf/CsrfConfiguration.java | 8 ++-- .../csrf/CsrfConfigurationProperties.java | 25 ++++++----- .../generator/DefaultCsrfTokenGenerator.java | 10 ++--- .../security/csrf/CsrfConfigurationTest.java | 45 ++++++++++++++++++- .../CsrfDoubleSubmitCookiePatternTest.java | 4 +- .../csrf/CsrfSessionLogingHandler.java | 2 +- security/build.gradle.kts | 4 ++ .../security/utils/HMacUtilsTest.java | 8 ++-- src/main/docs/guide/csrf.adoc | 5 +-- src/main/docs/guide/csrf/csrfApis.adoc | 7 +++ .../syncronizerTokenPattern.adoc | 14 +++++- src/main/docs/guide/toc.yml | 1 + 12 files changed, 98 insertions(+), 35 deletions(-) create mode 100644 src/main/docs/guide/csrf/csrfApis.adoc diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index a199779c11..6479e1aa1b 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -27,15 +27,15 @@ public interface CsrfConfiguration extends CookieConfiguration, Toggleable { /** * - * @return Random CSRF Token size in bytes. + * @return Random value's size in bytes. The random value used is used to build a CSRF Token. */ - int getTokenSize(); + int getRandomValueSize(); /** * - * @return CSRF token HMAC signature key + * @return The Secret Key that is used to calculate an HMAC as part of a CSRF token generation. */ - String getSignatureKey(); + String getSecretKey(); /** * diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index eda8b6c5c5..6f05661336 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -55,21 +55,22 @@ class CsrfConfigurationProperties implements CsrfConfiguration { @SuppressWarnings("WeakerAccess") public static final String DEFAULT_HTTP_SESSION_NAME = "csrfToken"; - public static final int DEFAULT_TOKEN_SIZE = 16; + public static final int DEFAULT_RANDOM_VALUE_SIZE = 16; public static final boolean DEFAULT_ENABLED = true; private static final boolean DEFAULT_HTTPONLY = true; private static final String DEFAULT_COOKIEPATH = "/"; + private static final Boolean DEFAULT_SECURE = true; private static final Duration DEFAULT_MAX_AGE = Duration.ofSeconds(AccessTokenConfigurationProperties.DEFAULT_EXPIRATION); private boolean enabled = DEFAULT_ENABLED; private String headerName = DEFAULT_HTTP_HEADER_NAME; private String fieldName = DEFAULT_FIELD_NAME; - private int tokenSize = DEFAULT_TOKEN_SIZE; + private int randomValueSize = DEFAULT_RANDOM_VALUE_SIZE; private String httpSessionName = DEFAULT_HTTP_SESSION_NAME; private String cookieDomain; - private Boolean cookieSecure; + private Boolean cookieSecure = DEFAULT_SECURE; private String cookiePath = DEFAULT_COOKIEPATH; private Boolean cookieHttpOnly = DEFAULT_HTTPONLY; private Duration cookieMaxAge = DEFAULT_MAX_AGE; @@ -77,13 +78,13 @@ class CsrfConfigurationProperties implements CsrfConfiguration { private String signatureKey; @Override - public String getSignatureKey() { + public String getSecretKey() { return signatureKey; } /** - * CSRF token HMAC signature key. - * @param signatureKey CSRF token HMAC signature key + * The Secret Key that is used to calculate an HMAC as part of a CSRF token generation. Default Value `null`. + * @param signatureKey The Secret Key that is used to calculate an HMAC as part of a CSRF token generation. */ public void setSignatureKey(String signatureKey) { this.signatureKey = signatureKey; @@ -103,16 +104,16 @@ public void setHttpSessionName(String httpSessionName) { } @Override - public int getTokenSize() { - return tokenSize; + public int getRandomValueSize() { + return randomValueSize; } /** - * Random CSRF Token size in bytes. Default Value: {@value #DEFAULT_TOKEN_SIZE}. - * @param tokenSize Random CSRF Token size in bytes. + * 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. */ - public void setTokenSize(int tokenSize) { - this.tokenSize = tokenSize; + public void setRandomValueSize(int randomValueSize) { + this.randomValueSize = randomValueSize; } @Override diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index 16491c3186..68d74067e3 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -35,7 +35,7 @@ import java.util.Optional; /** - * Default implementation of {@link CsrfTokenGenerator} which generates a random base 64 encoded string using an instance of {@link SecureRandom} and random byte array of size {@link CsrfConfiguration#getTokenSize()}. + * Default implementation of {@link CsrfTokenGenerator} which generates a random base 64 encoded string using an instance of {@link SecureRandom} and random byte array of size {@link CsrfConfiguration#getRandomValueSize()}. * @author Sergio del Amo * @since 4.11.0 */ @@ -58,9 +58,9 @@ final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator request) { // Gather the values - String secret = csrfConfiguration.getSignatureKey(); + String secret = csrfConfiguration.getSecretKey(); String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session - byte[] tokenBytes = new byte[csrfConfiguration.getTokenSize()]; + byte[] tokenBytes = new byte[csrfConfiguration.getRandomValueSize()]; secureRandom.nextBytes(tokenBytes); String randomValue = Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes); // Cryptographic random value @@ -86,7 +86,7 @@ public boolean validateCsrfToken(@NonNull HttpRequest request, @NonNull Strin return false; } String csrfCookie = csrfCookieOptional.get(); - return validateHmac(request, csrfCookie) && csrfCookie.equals(token); + return csrfCookie.equals(token) && validateHmac(request, csrfCookie); } private boolean validateHmac(HttpRequest request, @NonNull String csrfToken) { @@ -102,7 +102,7 @@ private boolean validateHmac(HttpRequest request, @NonNull String csrfToken) String randomValue = arr[1]; String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session String message = sessionID + SESSION_RANDOM_SEPARATOR + randomValue; - String secret = csrfConfiguration.getSignatureKey(); + String secret = csrfConfiguration.getSecretKey(); String expectedHmac = secret != null ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash : ""; diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java index 27d16b3dee..4b5763ec30 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java @@ -3,7 +3,8 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; - +import java.time.Duration; +import java.time.temporal.TemporalAmount; import static org.junit.jupiter.api.Assertions.*; @MicronautTest(startApplication = false) @@ -34,6 +35,46 @@ void defaultHttpSessionName() { @Test void defaultTokenSize() { - assertEquals(16, csrfConfiguration.getTokenSize()); + assertEquals(16, csrfConfiguration.getRandomValueSize()); + } + + @Test + void defaultCookiePath() { + assertTrue(csrfConfiguration.getCookiePath().isPresent()); + assertEquals("/", csrfConfiguration.getCookiePath().get()); + } + + @Test + void defaultCookieName() { + assertEquals("csrfToken", csrfConfiguration.getCookieName()); + } + + @Test + void defaultCookieSecure() { + assertTrue(csrfConfiguration.isCookieSecure().isPresent()); + assertEquals(Boolean.TRUE, csrfConfiguration.isCookieSecure().get()); + } + + @Test + void defaultCookieHttpOnly() { + assertTrue(csrfConfiguration.isCookieHttpOnly().isPresent()); + assertEquals(Boolean.TRUE, csrfConfiguration.isCookieHttpOnly().get()); + } + + @Test + void defaultCookieDomain() { + assertTrue(csrfConfiguration.getCookieDomain().isEmpty()); + } + + @Test + void defaultCookieMaxAge() { + assertTrue(csrfConfiguration.getCookieMaxAge().isPresent()); + TemporalAmount expected = Duration.ofSeconds(3600); + assertEquals(expected, csrfConfiguration.getCookieMaxAge().get()); + } + + @Test + void defaultSignatureKey() { + assertNull(csrfConfiguration.getSecretKey()); } } \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java index aa8ee44aeb..860588f650 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -69,13 +69,13 @@ void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, // CSRF Token with HMAC but not session id feed into HMAC calculation, request is unauthorized String randomValue = "abcdefg"; - String hmac = HMacUtils.base64EncodedHmacSha256(randomValue, csrfConfiguration.getSignatureKey()); + String hmac = HMacUtils.base64EncodedHmacSha256(randomValue, csrfConfiguration.getSecretKey()); String csrfTokenCalculatedWithoutSessionId = hmac + "." + randomValue; PasswordChangeForm body = new PasswordChangeForm("sherlock", "evil", csrfTokenCalculatedWithoutSessionId); assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, body, csrfToken); String message = FIX_SESSION_ID + "!" + randomValue; - hmac = HMacUtils.base64EncodedHmacSha256(message, csrfConfiguration.getSignatureKey()); + hmac = HMacUtils.base64EncodedHmacSha256(message, csrfConfiguration.getSecretKey()); csrfToken = hmac + "." + randomValue; assertOk(client, cookieJwt.getValue(), csrfTokenCookieName, csrfToken); diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java index df1cbc3b06..645b8e5130 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java @@ -72,7 +72,7 @@ public CsrfSessionLogingHandler( @Override protected Session saveAuthenticationInSession(Authentication authentication, HttpRequest request) { Session session = super.saveAuthenticationInSession(authentication, request); - session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generate()); + session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generate(request)); return session; } } diff --git a/security/build.gradle.kts b/security/build.gradle.kts index 7e4768aaa3..e16aa3ff01 100644 --- a/security/build.gradle.kts +++ b/security/build.gradle.kts @@ -34,4 +34,8 @@ dependencies { testImplementation(projects.testSuiteUtils) testImplementation(mn.snakeyaml) testImplementation(libs.bcpkix) + + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(mnTest.micronaut.test.junit5) + testRuntimeOnly(libs.junit.jupiter.engine) } diff --git a/security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java b/security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java index 0a6a1ca0d4..627226f756 100644 --- a/security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java +++ b/security/src/test/java/io/micronaut/security/utils/HMacUtilsTest.java @@ -14,9 +14,9 @@ void testHmacSha256() throws NoSuchAlgorithmException, InvalidKeyException { String data = "abcdedf"; String signatureKey = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"; String hmac = HMacUtils.base64EncodedHmacSha256(data, signatureKey); - Assertions.assertNotNull(hmac); - Assertions.assertEquals(hmac, HMacUtils.base64EncodedHmacSha256(data, signatureKey)); - Assertions.assertNotEquals(hmac, HMacUtils.base64EncodedHmacSha256("foobar", signatureKey)); - Assertions.assertNotEquals(hmac, HMacUtils.base64EncodedHmacSha256(data, signatureKey + "evil")); + assertNotNull(hmac); + assertEquals(hmac, HMacUtils.base64EncodedHmacSha256(data, signatureKey)); + assertNotEquals(hmac, HMacUtils.base64EncodedHmacSha256("foobar", signatureKey)); + assertNotEquals(hmac, HMacUtils.base64EncodedHmacSha256(data, signatureKey + "evil")); } } \ No newline at end of file diff --git a/src/main/docs/guide/csrf.adoc b/src/main/docs/guide/csrf.adoc index ca01cf5e26..fc6d521ffa 100644 --- a/src/main/docs/guide/csrf.adoc +++ b/src/main/docs/guide/csrf.adoc @@ -1,7 +1,4 @@ -https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)]. - +https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)]: ____ Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated ____ - - diff --git a/src/main/docs/guide/csrf/csrfApis.adoc b/src/main/docs/guide/csrf/csrfApis.adoc new file mode 100644 index 0000000000..71bec6b856 --- /dev/null +++ b/src/main/docs/guide/csrf/csrfApis.adoc @@ -0,0 +1,7 @@ +* api:security.csrf.CsrConfiguration[] +* api:security.csrf.filter.CsrFilter[] +* api:security.csrf.filter.CsrFilterConfiguration[] +* api:security.csrf.resolvers.CsrfTokenResolver[] +* api:security.csrf.generator.CsrfTokenGenerator[] +* api:security.csrf.CsrTokenValidator[] +* api:security.csrf.CsrfTokenRepository[] diff --git a/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc index 07a0bd23e6..08f3cbc88e 100644 --- a/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc +++ b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc @@ -1 +1,13 @@ -https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern[Syncronizer Token Pattern]. \ No newline at end of file +https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern[Syncronizer Token Pattern]. + +____ +In a synchronized token pattern, the server generates a CSRF token and shares it with the client before returning it, +usually through a hidden form parameter for the associated action. On form submission, the server checks the CSRF token against +one stored in the user’s session. If they match, the request is approved; otherwise, it’s rejected +____ + +If you use <> and Micronaut Security CSRF, a CSRF token is automatically generated upon login. + +For the following requests, the api::security.csrf.filter.CsrfFilter[] <>, and it validates the token against the value stored in the user's session. + +If you save the CSRF tokens in something like a database. Provide an implementation of api::security.csrf.CsrfTokenRepository[]. diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 53ec560208..8d18e0cd96 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -85,6 +85,7 @@ authenticationStrategies: rejection: Rejection Handling csrf: title: Cross-Site Request Forgery (CSRF) + csrfApis: CSRF APIs csrfDependency: CSRF Dependency csrfMitigations: title: CSRF Mitigations From d3b8aeb11503754696eb45de1fd2124563b77cdc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 16:17:14 +0200 Subject: [PATCH 019/108] fix checkstyle --- .../token/response/CsrfIdTokenLoginHandler.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java index 1ee25b6fe9..fb2ee49fed 100644 --- a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java +++ b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java @@ -15,8 +15,6 @@ */ package io.micronaut.security.oauth2.endpoint.token.response; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTParser; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; @@ -25,24 +23,16 @@ import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.cookie.Cookie; import io.micronaut.security.authentication.Authentication; -import io.micronaut.security.authentication.AuthenticationMode; import io.micronaut.security.config.RedirectConfiguration; import io.micronaut.security.config.RedirectService; import io.micronaut.security.config.SecurityConfigurationProperties; import io.micronaut.security.csrf.CsrfConfiguration; import io.micronaut.security.csrf.generator.CsrfTokenGenerator; -import io.micronaut.security.csrf.validator.CsrfTokenValidator; -import io.micronaut.security.errors.OauthErrorResponseException; -import io.micronaut.security.errors.ObtainingAuthorizationErrorCode; import io.micronaut.security.errors.PriorToLoginPersistence; import io.micronaut.security.token.cookie.AccessTokenCookieConfiguration; import io.micronaut.security.token.cookie.CookieLoginHandler; import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.text.ParseException; -import java.time.Duration; import java.util.*; /** @@ -60,16 +50,19 @@ public class CsrfIdTokenLoginHandler extends IdTokenLoginHandler { private final CsrfTokenGenerator> csrfTokenGenerator; private final CsrfConfiguration csrfConfiguration; + /** * @param accessTokenCookieConfiguration Access token cookie configuration * @param redirectConfiguration Redirect configuration * @param redirectService Redirect service * @param priorToLoginPersistence The prior to login persistence strategy + * @param csrfTokenGenerator CSRF Token Generator + * @param csrfConfiguration CSRF Configuration */ public CsrfIdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfiguration, RedirectConfiguration redirectConfiguration, RedirectService redirectService, - PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, + @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, CsrfTokenGenerator> csrfTokenGenerator, CsrfConfiguration csrfConfiguration) { super(accessTokenCookieConfiguration, redirectConfiguration, redirectService, priorToLoginPersistence); From 09bab4e2e2345540f0dcd35b74453c5023c4df36 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 17:50:16 +0200 Subject: [PATCH 020/108] more docs --- .../security/csrf/CsrfConfiguration.java | 2 +- .../csrf/CsrfConfigurationProperties.java | 23 ++++++++++++++- .../security/csrf/filter/CsrfFilter.java | 11 +++++-- .../CsrfFilterConfigurationProperties.java | 4 +-- .../generator/DefaultCsrfTokenGenerator.java | 2 ++ .../csrf/resolver/FieldCsrfTokenResolver.java | 1 + .../resolver/HttpHeaderCsrfTokenResolver.java | 1 + .../csrf/validator/CsrfTokenValidator.java | 2 +- .../security/csrf/CsrfConfigurationTest.java | 11 +++++-- .../filter/CsrfFilterConfigurationTest.java | 29 +++++++++++++++++-- .../csrf/generator/CsrfTokenSignerTest.java | 16 ---------- .../CsrfDoubleSubmitCookiePatternTest.java | 7 +++-- .../csrf/resolver/CsrfTokenResolverTest.java | 1 + .../JsonWebTokenIdSessionIdResolver.java | 1 + .../csrf/SessionCsrfTokenRepository.java | 2 ++ src/main/docs/guide/csrf/csrfApis.adoc | 2 ++ src/main/docs/guide/csrf/csrfFilter.adoc | 6 +++- src/main/docs/guide/csrf/csrfMitigations.adoc | 2 +- .../doubleSubmitCookiePattern.adoc | 22 +++++++++++++- .../signedDoubleSubmitCookiePattern.adoc | 11 +++++++ .../syncronizerTokenPattern.adoc | 6 ---- .../csrfAndSession.adoc | 4 +++ .../docs/guide/csrf/csrfTokenResolvers.adoc | 13 +-------- .../fieldCsrfTokenResolver.adoc | 15 ++++++++++ .../httpHeaderCsrfTokenResolver.adoc | 22 ++++++++++++++ .../docs/guide/endpoints/builtInHandlers.adoc | 23 --------------- ...micronautSecurityAuthenticationBearer.adoc | 3 ++ ...micronautSecurityAuthenticationCookie.adoc | 3 ++ ...icronautSecurityAuthenticationIdToken.adoc | 3 ++ ...icronautSecurityAuthenticationSession.adoc | 3 ++ src/main/docs/guide/toc.yml | 21 ++++++++++---- test-suite-http/build.gradle | 1 + 32 files changed, 193 insertions(+), 80 deletions(-) delete mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java create mode 100644 src/main/docs/guide/csrf/csrfMitigations/signedDoubleSubmitCookiePattern.adoc create mode 100644 src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern/csrfAndSession.adoc create mode 100644 src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc create mode 100644 src/main/docs/guide/csrf/csrfTokenResolvers/httpHeaderCsrfTokenResolver.adoc create mode 100644 src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationBearer.adoc create mode 100644 src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationCookie.adoc create mode 100644 src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationIdToken.adoc create mode 100644 src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationSession.adoc diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index 6479e1aa1b..763f3dda6b 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -38,7 +38,7 @@ public interface CsrfConfiguration extends CookieConfiguration, Toggleable { String getSecretKey(); /** - * + * HTTP Header name to look for the CSRF token. It is recommended to use a custom request header. By using a custom HTTP Header name, it will not be possible to send them cross-origin without a permissive CORS implementation. * @return HTTP Header name to look for the CSRF token. */ @NonNull diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index 6f05661336..86b6c98b0f 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.cookie.SameSite; import io.micronaut.security.config.SecurityConfigurationProperties; import io.micronaut.security.token.generator.AccessTokenConfigurationProperties; @@ -45,9 +46,10 @@ class CsrfConfigurationProperties implements CsrfConfiguration { /** * The default cookie name.. + * @see Using Cookies with Host Prefixes to Identify Origins */ @SuppressWarnings("WeakerAccess") - public static final String DEFAULT_COOKIE_NAME = "csrfToken"; + public static final String DEFAULT_COOKIE_NAME = "__Host-csrfToken"; /** * The default HTTP Session name. @@ -55,6 +57,12 @@ class CsrfConfigurationProperties implements CsrfConfiguration { @SuppressWarnings("WeakerAccess") public static final String DEFAULT_HTTP_SESSION_NAME = "csrfToken"; + /** + * The default Same Site Configuration. + */ + @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; @@ -75,6 +83,7 @@ class CsrfConfigurationProperties implements CsrfConfiguration { private Boolean cookieHttpOnly = DEFAULT_HTTPONLY; private Duration cookieMaxAge = DEFAULT_MAX_AGE; private String cookieName = DEFAULT_COOKIE_NAME; + private SameSite sameSite = DEFAULT_SAME_SITE; private String signatureKey; @Override @@ -240,4 +249,16 @@ public Optional getCookieMaxAge() { public void setCookieMaxAge(Duration cookieMaxAge) { this.cookieMaxAge = cookieMaxAge; } + + public Optional getCookieSameSite() { + return Optional.of(this.sameSite); + } + + /** + * Cookie Same Site Configuration. It defaults to Strict. + * @param sameSite Same Site Configuration + */ + public void setCookieSameSite(SameSite sameSite) { + this.sameSite = sameSite; + } } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index bec149d114..39cf678b36 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -19,11 +19,13 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.order.Ordered; import io.micronaut.core.util.StringUtils; import io.micronaut.http.*; import io.micronaut.http.annotation.RequestFilter; import io.micronaut.http.annotation.ServerFilter; import io.micronaut.http.filter.FilterPatternStyle; +import io.micronaut.http.filter.ServerFilterPhase; import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; @@ -39,7 +41,7 @@ import java.util.Optional; /** - * {@link RequestFilter} which validates CSRF tokens and returns a 401 Unauthorized if the token is invalid. + * {@link RequestFilter} which validates CSRF tokens and rejects a request if the token is invalid. * Which requests are intercepted can be controlled via {@link io.micronaut.security.csrf.CsrfConfiguration}. * @author Sergio del Amo * @since 4.11.0 @@ -50,7 +52,7 @@ @Requires(beans = { CsrfTokenValidator.class }) @ServerFilter(patternStyle = FilterPatternStyle.REGEX, value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") -final class CsrfFilter { +final class CsrfFilter implements Ordered { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); private final List>> csrfTokenResolvers; private final CsrfTokenValidator> csrfTokenValidator; @@ -145,4 +147,9 @@ private HttpResponse unauthorized(@NonNull HttpRequest request) { return exceptionHandler.handle(request, new AuthorizationException(authentication)); } + + @Override + public int getOrder() { + return ServerFilterPhase.SECURITY.order() + 100; // after {@link SecurityFilter} + } } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java index cdc86535a3..b1708e7523 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java @@ -40,7 +40,7 @@ final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration * The default regex pattern. */ @SuppressWarnings("WeakerAccess") - public static final String DEFAULT_REGEX_PATTERN = "^(?!\\/(login|logout)).*$"; + public static final String DEFAULT_REGEX_PATTERN = "^.*$"; private static final Set DEFAULT_METHODS = Set.of( HttpMethod.POST, @@ -105,7 +105,7 @@ public String getRegexPattern() { } /** - * CSRF filter processes only request paths matching this regular expression. Default Value {@value #DEFAULT_REGEX_PATTERN}. + * CSRF filter processes only request paths matching this regular expression. Default Value: {@value #DEFAULT_REGEX_PATTERN} * @param regexPattern Regular expression pattern for the filter. */ public void setRegexPattern(String regexPattern) { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index 68d74067e3..bf203a6cfc 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.csrf.generator; +import io.micronaut.context.annotation.Requires; import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -39,6 +40,7 @@ * @author Sergio del Amo * @since 4.11.0 */ +@Requires(classes = HttpRequest.class) @Singleton @Internal final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator>, CsrfTokenValidator> { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 49b481082c..7116242f76 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -35,6 +35,7 @@ * * @since 2.0.0 */ +@Requires(classes = HttpRequest.class) @Requires(property = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton class FieldCsrfTokenResolver implements CsrfTokenResolver> { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index a4214eef5b..2bf11ebaeb 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -29,6 +29,7 @@ * @author Sergio del Amo * @since 4.11.0 */ +@Requires(classes = HttpRequest.class) @Requires(property = "micronaut.security.csrf.token-resolvers.http-header.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton @Internal diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java index a2d292b855..8d7bb57bfb 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CsrfTokenValidator.java @@ -27,7 +27,7 @@ @FunctionalInterface public interface CsrfTokenValidator extends Ordered { /** - * + * Given a CSRF Token, validates whether it is valid. * @param request Request * @param token CSRF Token * @return Whether the CSRF token is valid diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java index 4b5763ec30..92f4a7bb20 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java @@ -1,5 +1,6 @@ package io.micronaut.security.csrf; +import io.micronaut.http.cookie.SameSite; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; @@ -46,7 +47,13 @@ void defaultCookiePath() { @Test void defaultCookieName() { - assertEquals("csrfToken", csrfConfiguration.getCookieName()); + assertEquals("__Host-csrfToken", csrfConfiguration.getCookieName()); + } + + @Test + void defaultSameSite() { + assertTrue(csrfConfiguration.getCookieSameSite().isPresent()); + assertEquals(SameSite.Strict, csrfConfiguration.getCookieSameSite().get()); } @Test @@ -74,7 +81,7 @@ void defaultCookieMaxAge() { } @Test - void defaultSignatureKey() { + void defaultSecretKey() { assertNull(csrfConfiguration.getSecretKey()); } } \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java index 1af23d70b1..3c77be63dd 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationTest.java @@ -1,22 +1,46 @@ package io.micronaut.security.csrf.filter; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.order.OrderUtil; +import io.micronaut.core.order.Ordered; import io.micronaut.core.util.PathMatcher; import io.micronaut.http.HttpMethod; import io.micronaut.http.MediaType; +import io.micronaut.security.filters.SecurityFilter; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; +@Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$") @MicronautTest(startApplication = false) class CsrfFilterConfigurationTest { @Inject CsrfFilterConfiguration csrfFilterConfiguration; + @Inject + CsrfFilter csrfFilter; + + @Inject + SecurityFilter securityFilter; + + @Test + void orderOfFilters() { + List filters = new ArrayList<>(List.of(csrfFilter, securityFilter)); + OrderUtil.sort(filters); + assertInstanceOf(SecurityFilter.class, filters.get(0)); + + filters = new ArrayList<>(List.of(securityFilter, csrfFilter)); + OrderUtil.sort(filters); + assertInstanceOf(SecurityFilter.class, filters.get(0)); + } + @Test void defaultMethods() { assertEquals(Set.of(HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PUT, HttpMethod.PATCH), csrfFilterConfiguration.getMethods()); @@ -28,12 +52,11 @@ void defaultContentType() { } @Test - void defaultRegexPattern() { + void regexPatternCanBeChanged() { String regexPattern = csrfFilterConfiguration.getRegexPattern(); assertFalse(PathMatcher.REGEX.matches(csrfFilterConfiguration.getRegexPattern(), "/login")); - assertFalse(PathMatcher.REGEX.matches(csrfFilterConfiguration.getRegexPattern(), "/logout")); assertTrue(PathMatcher.REGEX.matches(csrfFilterConfiguration.getRegexPattern(), "/todo/list")); - assertEquals("^(?!\\/(login|logout)).*$", regexPattern); + assertEquals("^(?!\\/login).*$", regexPattern); } @Test diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java deleted file mode 100644 index d2b5aa3e2d..0000000000 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenSignerTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.micronaut.security.csrf.generator; - -import io.micronaut.context.annotation.Property; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import org.junit.jupiter.api.Test; - -@Property(name = "micronaut.security.csrf.signature-key", value = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") -@MicronautTest(startApplication = false) -class CsrfTokenSignerTest { - - @Test - void tokenIsSigned() { - - } - -} \ No newline at end of file diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java index 860588f650..deb2100146 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -33,8 +33,9 @@ @Property(name = "micronaut.security.authentication", value = "cookie") @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "pleaseChangeThisSecretForANewOne") -@Property(name = "micronaut.security.csrf.signature-key", value = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") +@Property(name = "micronaut.security.csrf.signature-key", value = "pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") @Property(name = "micronaut.security.redirect.enabled", value = StringUtils.FALSE) +@Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$") @Property(name = "spec.name", value = "CsrfDoubleSubmitCookiePatternTest") @MicronautTest class CsrfDoubleSubmitCookiePatternTest { @@ -53,10 +54,10 @@ void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, Optional cookieJwtOptional = loginRsp.getCookie("JWT"); assertTrue(cookieJwtOptional.isPresent()); Cookie cookieJwt = cookieJwtOptional.get(); - Optional cookieCsrfTokenOptional = loginRsp.getCookie("csrfToken"); + String csrfTokenCookieName = "__Host-csrfToken"; + Optional cookieCsrfTokenOptional = loginRsp.getCookie(csrfTokenCookieName); assertTrue(cookieCsrfTokenOptional.isPresent()); Cookie cookieCsrfToken = cookieCsrfTokenOptional.get(); - String csrfTokenCookieName = "csrfToken"; // CSRF Only in the cookie, not in the request headers or field, request is denied assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, new PasswordChange("sherlock", "evil"), cookieCsrfToken.getValue()); diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java index 2e9ed16e8e..689c9bed3c 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java @@ -21,6 +21,7 @@ void csrfTokenResolversOrder() { Collection csrfTokenResolverCollection = beanContext.getBeansOfType(CsrfTokenResolver.class); List csrfTokenResolverList = new ArrayList<>(csrfTokenResolverCollection); assertEquals(2, csrfTokenResolverList.size()); + // It is important for HTTP Header to be the first one. FieldCsrfTokenResolver requires Netty. Moreover, it is more secure to supply the CSRF token via custom HTTP Header instead of a form field as it is more difficult to exploit. assertInstanceOf(HttpHeaderCsrfTokenResolver.class, csrfTokenResolverList.get(0)); assertInstanceOf(FieldCsrfTokenResolver.class, csrfTokenResolverList.get(1)); diff --git a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java index 39cf0294b5..2fabc9ae1b 100644 --- a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java +++ b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java @@ -31,6 +31,7 @@ * @since 4.11.0 * @author Sergio del Amo */ +@Requires(classes = HttpRequest.class) @Requires(bean = JsonWebTokenParser.class) @Singleton public class JsonWebTokenIdSessionIdResolver implements SessionIdResolver> { diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java b/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java index ede3b88ef9..80de897f6f 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java @@ -16,6 +16,7 @@ package io.micronaut.security.session.csrf; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.security.csrf.CsrfConfiguration; import io.micronaut.security.csrf.repository.CsrfTokenRepository; @@ -31,6 +32,7 @@ */ @Requires(classes = HttpRequest.class) @Requires(beans = CsrfConfiguration.class) +@Requires(property = "micronaut.security.csrf.repositories.session.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton public class SessionCsrfTokenRepository implements CsrfTokenRepository> { private final CsrfConfiguration csrfConfiguration; diff --git a/src/main/docs/guide/csrf/csrfApis.adoc b/src/main/docs/guide/csrf/csrfApis.adoc index 71bec6b856..91881a4f59 100644 --- a/src/main/docs/guide/csrf/csrfApis.adoc +++ b/src/main/docs/guide/csrf/csrfApis.adoc @@ -1,3 +1,5 @@ +The main APIs for CSRF protection are: + * api:security.csrf.CsrConfiguration[] * api:security.csrf.filter.CsrFilter[] * api:security.csrf.filter.CsrFilterConfiguration[] diff --git a/src/main/docs/guide/csrf/csrfFilter.adoc b/src/main/docs/guide/csrf/csrfFilter.adoc index 42b3e5de99..445b325bfb 100644 --- a/src/main/docs/guide/csrf/csrfFilter.adoc +++ b/src/main/docs/guide/csrf/csrfFilter.adoc @@ -1,4 +1,8 @@ -The following configuration options are available for CSRF: +The core of Micronaut Security CSRF implementation is `io.micronaut.security.csrf.filter.CsrfFilter`. +A https://docs.micronaut.io/latest/guide/#filtermethods[Request Filter Method] which attempts to resolve a CSRF Token with +every bean of type api:security.csrf.resolvers.CsrfTokenResolver[] and validates it with beans of type api:security.csrf.validator.CsrfTokenValidator[]. + +The following configuration options are available for the CSRF Filter: include::{includedir}configurationProperties/io.micronaut.security.csrf.filter.CsrfFilterConfigurationProperties.adoc[] diff --git a/src/main/docs/guide/csrf/csrfMitigations.adoc b/src/main/docs/guide/csrf/csrfMitigations.adoc index 139597f9cb..5d34dede8a 100644 --- a/src/main/docs/guide/csrf/csrfMitigations.adoc +++ b/src/main/docs/guide/csrf/csrfMitigations.adoc @@ -1,2 +1,2 @@ - +IMPORTANT: Ensure your application does not perform state-changing actions via the GET request method. Your application should perform state-changing actions only via POST, PUT, PATCH, or DELETE methods. diff --git a/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc b/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc index 09e04b7d19..c09d7e30fe 100644 --- a/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc +++ b/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc @@ -1 +1,21 @@ -https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern[Double Submit Cookie Pattern]. \ No newline at end of file +https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern[Double Submit Cookie Pattern]. + +In a double-submit cookie pattern, the server generates a CSRF token, and it sends the CSRF token to the client in a cookie. + +Then, the server only needs to verify that following requests cookie's value matches the CSRF token sent in a request parameter (a hidden form field) or header. This process is stateless, as the server doesn’t need to store any information about the CSRF token. + +[source, bash] +---- +POST /transfer HTTP/1.1 +Host: vulnerable bank +Content-Type: application/x-www-form-urlencoded +Cookie: session=; __Host-csrfToken=o24b65486f506e2cd4403caf0d640024 +[...] + +amount=100&toUser=intended&csrfToken=o24b65486f506e2cd4403caf0d640024 +---- + +When you use Micronaut Security Authentication <>, or + <> a CSRF Token is saved in a Cookie upon login. + +You can <>. For example, by default the cookie name uses a `__Host-` https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes[Cookie prefix], can extend https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#using-cookies-with-host-prefixes-to-identify-origins[security protections against CSF Attacks]. \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfMitigations/signedDoubleSubmitCookiePattern.adoc b/src/main/docs/guide/csrf/csrfMitigations/signedDoubleSubmitCookiePattern.adoc new file mode 100644 index 0000000000..275902dbf4 --- /dev/null +++ b/src/main/docs/guide/csrf/csrfMitigations/signedDoubleSubmitCookiePattern.adoc @@ -0,0 +1,11 @@ +IMPORTANT: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#signed-double-submit-cookie-recommended[Signed Double-Submit Cookie]. Sign the CSRF Token with to prevent attackers from overriding the cookie value with their own (e.g. with taken-over subdomain attacks) . + +To do sign the CSRF Token, set the property `micronaut.security.csrf.signature-key`. + +[configuration] +---- +micronaut: + security: + csrf: + signature-key: pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +---- \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc index 08f3cbc88e..a51f520cd1 100644 --- a/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc +++ b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern.adoc @@ -5,9 +5,3 @@ In a synchronized token pattern, the server generates a CSRF token and shares it usually through a hidden form parameter for the associated action. On form submission, the server checks the CSRF token against one stored in the user’s session. If they match, the request is approved; otherwise, it’s rejected ____ - -If you use <> and Micronaut Security CSRF, a CSRF token is automatically generated upon login. - -For the following requests, the api::security.csrf.filter.CsrfFilter[] <>, and it validates the token against the value stored in the user's session. - -If you save the CSRF tokens in something like a database. Provide an implementation of api::security.csrf.CsrfTokenRepository[]. diff --git a/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern/csrfAndSession.adoc b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern/csrfAndSession.adoc new file mode 100644 index 0000000000..f5e4ab222f --- /dev/null +++ b/src/main/docs/guide/csrf/csrfMitigations/syncronizerTokenPattern/csrfAndSession.adoc @@ -0,0 +1,4 @@ +If you use <> and Micronaut Security CSRF, a CSRF token is automatically generated upon login and saved into the HTTP Session. <> provides an implementation +api:security.csrf.CsrfTokenRepository[] which fetches the CSRF token from the user's HTTP session. Thus, when the application sends new request to the sever with a CSRF token (e.g. in a hidden form field or HTTP Header), the server validates the supplied token against the value stored in the HTTP Session. + +You can disable the CSRF Session repository by setting `micronaut.security.csrf.repositories.session.enabled` to false. diff --git a/src/main/docs/guide/csrf/csrfTokenResolvers.adoc b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc index 16126ce13d..2fb1ce1b77 100644 --- a/src/main/docs/guide/csrf/csrfTokenResolvers.adoc +++ b/src/main/docs/guide/csrf/csrfTokenResolvers.adoc @@ -1,12 +1 @@ -|=== -|Resolver | Enabled by Default | Disable with - -|`io.micronaut.security.csrf.resolver.HttpHeaderCsrfTokenResolver` -| Yes -| `micronaut.security.csrf.token-resolvers.http-header.enabled=false` - -| `io.micronaut.security.csrf.resolver.FieldCsrfTokenResolver` -| Yes -| `micronaut.security.csrf.token-resolvers.field.enabled=false` - -|=== +Micronaut Security CSRF resolves a CSRF Token with beans of type api:security.csrf.resolver.CsrfTokenResolver[] \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc b/src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc new file mode 100644 index 0000000000..41c25e38ec --- /dev/null +++ b/src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc @@ -0,0 +1,15 @@ +Micronaut Security CSRF ships a `io.micronaut.security.csrf.resolver.FieldCsrfTokenResolver` an implementation of api:security.csrf.resolver.CsrfTokenResolver[] which looks for a CSRF Token in a Request's form-url-encoded field. + +[source, bash] +---- +POST /transfer HTTP/1.1 +Host: vulnerable bank +Content-Type: application/x-www-form-urlencoded +Cookie: session= +[...] +amount=100&toUser=intended&csrfToken=o24b65486f506e2cd4403caf0d640024 +---- + +You can disable it by setting `micronaut.security.csrf.token-resolvers.field.enabled=false` + +NOTE: `FieldCsrfTokenResolver` only works for Netty runtime. \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfTokenResolvers/httpHeaderCsrfTokenResolver.adoc b/src/main/docs/guide/csrf/csrfTokenResolvers/httpHeaderCsrfTokenResolver.adoc new file mode 100644 index 0000000000..0da166e300 --- /dev/null +++ b/src/main/docs/guide/csrf/csrfTokenResolvers/httpHeaderCsrfTokenResolver.adoc @@ -0,0 +1,22 @@ +Micronaut Security CSRF ships a `io.micronaut.security.csrf.resolver.HttpHeaderCsrfTokenResolver` an implementation of api:security.csrf.resolver.CsrfTokenResolver[] which looks for a CSRF Token in a Request's HTTP Header. + +[source, bash] +---- +POST /transfer HTTP/1.1 +Host: vulnerable bank +Content-Type: application/x-www-form-urlencoded +Cookie: session= +X-CSRF-TOKEN: o24b65486f506e2cd4403caf0d640024 +[...] +amount=100&toUser=intended +---- + +You can disable it by setting `micronaut.security.csrf.token-resolvers.http-header.enabled=false` + +The HTTP Header name used by `HttpHeaderCsrfTokenResolver` <>. +It is recommended to use a custom HTTP Header Name. By using a custom HTTP Header name, it will not be possible to send them cross-origin without a permissive CORS implementation. + +Moreover, If possible, we recommend you to send the CSRF token via an HTTP Header instead of a form field as it is harder to attack. + +For example, https://turbo.hotwired.dev/handbook/frames#anti-forgery-support-(csrf)[Turbo] sends the CSRF token via a custom HTTP Header upon form submission. You can find information about https://micronaut-projects.github.io/micronaut-views/latest/guide/#turbo[Micronaut Turbo integration]. + diff --git a/src/main/docs/guide/endpoints/builtInHandlers.adoc b/src/main/docs/guide/endpoints/builtInHandlers.adoc index 98531c0392..6ae2739a0e 100644 --- a/src/main/docs/guide/endpoints/builtInHandlers.adoc +++ b/src/main/docs/guide/endpoints/builtInHandlers.adoc @@ -28,28 +28,5 @@ However, Micronaut security modules ship with several implementations which you These handlers allow you to set the following scenarios: -#### Micronaut Security Authentication Bearer - -When you set `micronaut.security.authentication=bearer`, api:security.token.bearer.AccessRefreshTokenLoginHandler[] a bean of type api:security.handlers.LoginHandler[] is enabled. - -image::micronaut-security-authentication-bearer.png[] - -#### Micronaut Security Authentication Cookie - -When you set `micronaut.security.authentication=cookie`, api:security.token.cookie.TokenCookieLoginHandler[] a bean of type api:security.handlers.LoginHandler[] and api:security.token.jwt.cookie.JwtCookieClearerLogoutHandler[] a bean of type api:security.handlers.LogoutHandler[] are enabled. - -image::micronaut-security-authentication-cookie.png[] - -#### Micronaut Security Authentication SessionLoginHandler - -When you set `micronaut.security.authentication=session`, api:security.session.SessionLoginHandler[] a bean of type api:security.handlers.LoginHandler[] and api:security.session.SessionLogoutHandler[] a bean of type api:security.handlers.LogoutHandler[] are enabled. - -image::micronaut-security-authentication-session.png[] - -#### Micronaut Security Authentication ID Token - -When you set `micronaut.security.authentication=idtoken`, api:security.oauth2.endpoint.token.response.IdTokenLoginHandler[] a bean of type api:security.handlers.LoginHandler[] and api:security.token.jwt.cookie.JwtCookieClearerLogoutHandler[] a bean of type api:security.handlers.LogoutHandler[] are enabled. - -image::micronaut-security-authentication-idtoken.png[] diff --git a/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationBearer.adoc b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationBearer.adoc new file mode 100644 index 0000000000..02b45a8e1e --- /dev/null +++ b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationBearer.adoc @@ -0,0 +1,3 @@ +When you set `micronaut.security.authentication=bearer`, api:security.token.bearer.AccessRefreshTokenLoginHandler[] a bean of type api:security.handlers.LoginHandler[] is enabled. + +image::micronaut-security-authentication-bearer.png[] \ No newline at end of file diff --git a/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationCookie.adoc b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationCookie.adoc new file mode 100644 index 0000000000..37f0671c70 --- /dev/null +++ b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationCookie.adoc @@ -0,0 +1,3 @@ +When you set `micronaut.security.authentication=cookie`, api:security.token.cookie.TokenCookieLoginHandler[] a bean of type api:security.handlers.LoginHandler[] and api:security.token.jwt.cookie.JwtCookieClearerLogoutHandler[] a bean of type api:security.handlers.LogoutHandler[] are enabled. + +image::micronaut-security-authentication-cookie.png[] \ No newline at end of file diff --git a/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationIdToken.adoc b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationIdToken.adoc new file mode 100644 index 0000000000..396dbeab24 --- /dev/null +++ b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationIdToken.adoc @@ -0,0 +1,3 @@ +When you set `micronaut.security.authentication=idtoken`, api:security.oauth2.endpoint.token.response.IdTokenLoginHandler[] a bean of type api:security.handlers.LoginHandler[] and api:security.token.jwt.cookie.JwtCookieClearerLogoutHandler[] a bean of type api:security.handlers.LogoutHandler[] are enabled. + +image::micronaut-security-authentication-idtoken.png[] \ No newline at end of file diff --git a/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationSession.adoc b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationSession.adoc new file mode 100644 index 0000000000..685e5897ea --- /dev/null +++ b/src/main/docs/guide/endpoints/builtInHandlers/micronautSecurityAuthenticationSession.adoc @@ -0,0 +1,3 @@ +When you set `micronaut.security.authentication=session`, api:security.session.SessionLoginHandler[] a bean of type api:security.handlers.LoginHandler[] and api:security.session.SessionLogoutHandler[] a bean of type api:security.handlers.LogoutHandler[] are enabled. + +image::micronaut-security-authentication-session.png[] diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 8d18e0cd96..ab1bfcf8a1 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -33,7 +33,12 @@ endpoints: logout: title: Logout Controller logoutHandler: Logout Handler - builtInHandlers: Built-in Login and Logout Handlers + builtInHandlers: + title: Built-in Login and Logout Handlers + micronautSecurityAuthenticationBearer: Micronaut Security Authentication Bearer + micronautSecurityAuthenticationSession: Micronaut Security Session Login Handler + micronautSecurityAuthenticationCookie: Micronaut Security Authentication Cookie + micronautSecurityAuthenticationIdToken: Micronaut Security Authentication ID Token securityConfiguration: title: Security Configuration rejectNotFound: Reject Not Found Routes @@ -85,15 +90,21 @@ authenticationStrategies: rejection: Rejection Handling csrf: title: Cross-Site Request Forgery (CSRF) - csrfApis: CSRF APIs csrfDependency: CSRF Dependency + csrfFilter: CSRF Filter csrfMitigations: title: CSRF Mitigations - syncronizerTokenPattern: Syncronizer Token Pattern + syncronizerTokenPattern: + title: Syncronizer Token Pattern + csrfAndSession: CSRF and Session doubleSubmitCookiePattern: Double Submit Cookie Pattern + signedDoubleSubmitCookiePattern: Signed Double Submit Cookie Pattern csrfConfiguration: CSRF Configuration - csrfFilter: CSRF Filter - csrfTokenResolvers: CSRF Token Resolvers + csrfTokenResolvers: + title: CSRF Token Resolvers + httpHeaderCsrfTokenResolver: HTTP Header CSRF Token Resolution + fieldCsrfTokenResolver: Field CSRF Token Resolution + csrfApis: CSRF APIs tokenPropagation: Token Propagation tokenendpoints: title: Built-In Security Token Controllers diff --git a/test-suite-http/build.gradle b/test-suite-http/build.gradle index dc51cd9d7e..25b6b88b53 100644 --- a/test-suite-http/build.gradle +++ b/test-suite-http/build.gradle @@ -13,6 +13,7 @@ dependencies { testRuntimeOnly(mnLogging.logback.classic) testImplementation(projects.micronautSecurity) + testImplementation(projects.micronautSecurityCsrf) testImplementation(projects.micronautSecurityJwt) //testImplementation(projects.micronautSecurityOauth2) testImplementation(projects.micronautSecurityLdap) From 9615cb94884ec0b6d5274d7ef7eec65fcfcf79b2 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 19:19:31 +0200 Subject: [PATCH 021/108] fix test --- .../security/session/csrf/CsrfSessionLogingHandlerTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java b/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java index 3df44235a9..620652ef60 100644 --- a/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java +++ b/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java @@ -28,6 +28,7 @@ @Property(name = "micronaut.security.authentication", value = "session") @Property(name = "micronaut.security.redirect.enabled", value = StringUtils.FALSE) +@Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$") @Property(name = "spec.name", value = "CsrfSessionLogingHandlerTest") @MicronautTest class CsrfSessionLogingHandlerTest { From 8f6cf8eef00426cd8927daa65e9fbc87a234bf72 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 19:59:58 +0200 Subject: [PATCH 022/108] extract to a variable --- .../java/io/micronaut/security/csrf/filter/CsrfFilter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 39cf678b36..28c4a35e75 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -89,12 +89,13 @@ public HttpResponse csrfFilter(@NonNull HttpRequest request) { } private boolean shouldTheFilterProcessTheRequestAccordingToTheContentType(@NonNull HttpRequest request) { - if (request.getContentType().isPresent() && csrfFilterConfiguration.getContentTypes().stream().noneMatch(method -> method.equals(request.getContentType().get()))) { + final MediaType contentType = request.getContentType().orElse(null); + if (contentType != null && csrfFilterConfiguration.getContentTypes().stream().noneMatch(method -> method.equals(contentType))) { if (LOG.isTraceEnabled()) { LOG.trace("Request {} {} with content type {} is not processed by the CSRF filter. CSRF filter only processes Content Types: {}", request.getMethod(), request.getPath(), - request.getContentType().get(), + contentType, csrfFilterConfiguration.getContentTypes().stream().map(MediaType::toString).toList()); } return false; From ad750968df164cc79df0aa2b05fdde3e3c491812 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 20:00:05 +0200 Subject: [PATCH 023/108] add missing @Override --- .../io/micronaut/security/csrf/CsrfConfigurationProperties.java | 1 + 1 file changed, 1 insertion(+) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index 86b6c98b0f..2a9ae6f8a3 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -250,6 +250,7 @@ public void setCookieMaxAge(Duration cookieMaxAge) { this.cookieMaxAge = cookieMaxAge; } + @Override public Optional getCookieSameSite() { return Optional.of(this.sameSite); } From 5b50cfd7d8af49dd8c05d40ceb42674d2974c524 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 20:01:57 +0200 Subject: [PATCH 024/108] make field static --- .../security/csrf/resolver/HttpHeaderCsrfTokenResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index 2bf11ebaeb..41abe14ede 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -35,7 +35,7 @@ @Internal final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; - private final int ORDER = -100; + private static final int ORDER = -100; HttpHeaderCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { this.csrfConfiguration = csrfConfiguration; From adf3df72f2586dd722584a00c1f67e4b4ad766d0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 20:02:12 +0200 Subject: [PATCH 025/108] provide the parametrized type for this generic --- .../csrf/repository/CrsrfTokenCookieLoginHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java index 450a03cdf8..22e6266f5e 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java @@ -51,7 +51,7 @@ @Singleton public class CrsrfTokenCookieLoginHandler extends TokenCookieLoginHandler { private final CsrfConfiguration csrfConfiguration; - private final CsrfTokenGenerator csrfTokenGenerator; + private final CsrfTokenGenerator> csrfTokenGenerator; /** * @param redirectService Redirection Service @@ -72,7 +72,7 @@ public CrsrfTokenCookieLoginHandler(RedirectService redirectService, AccessRefreshTokenGenerator accessRefreshTokenGenerator, @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, CsrfConfiguration csrfConfiguration, - CsrfTokenGenerator csrfTokenGenerator) { + CsrfTokenGenerator> csrfTokenGenerator) { super(redirectService, redirectConfiguration, accessTokenCookieConfiguration, refreshTokenCookieConfiguration, accessTokenConfiguration, accessRefreshTokenGenerator, priorToLoginPersistence); this.csrfConfiguration = csrfConfiguration; this.csrfTokenGenerator = csrfTokenGenerator; From 75599a30f948736a4bd59c60be56006bb88543d4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 21 Oct 2024 20:02:45 +0200 Subject: [PATCH 026/108] File does not end with a newline. --- .../main/java/io/micronaut/security/session/package-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/src/main/java/io/micronaut/security/session/package-info.java b/security/src/main/java/io/micronaut/security/session/package-info.java index f0d84ae2ff..666d1ebe9d 100644 --- a/security/src/main/java/io/micronaut/security/session/package-info.java +++ b/security/src/main/java/io/micronaut/security/session/package-info.java @@ -17,4 +17,4 @@ * @author Sergio del Amo * @since 4.11.0 */ -package io.micronaut.security.session; \ No newline at end of file +package io.micronaut.security.session; From a396d32ae1aef8c77a93ee49976e8f7f2ac1ff35 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 11:06:22 +0200 Subject: [PATCH 027/108] better implementation --- .../csrf/generator/CsrfTokenGenerator.java | 6 +- .../generator/DefaultCsrfTokenGenerator.java | 92 +++++++------------ .../CompositeCsrfTokenRepository.java} | 31 +++---- .../repository/CookieCsrfTokenRepository.java | 52 +++++++++++ .../CrsrfTokenCookieLoginHandler.java | 2 +- .../csrf/repository/CsrfTokenRepository.java | 4 +- .../resolver/HttpHeaderCsrfTokenResolver.java | 2 +- .../RepositoryCsrfTokenValidator.java | 52 +++++++++-- .../security/csrf/CsrfConfigurationTest.java | 2 +- .../generator/CsrfTokenGeneratorTest.java | 2 +- .../resolver/FieldCsrfTokenResolverTest.java | 23 ++++- .../response/CsrfIdTokenLoginHandler.java | 2 +- .../csrf/CsrfSessionLogingHandler.java | 2 +- 13 files changed, 173 insertions(+), 99 deletions(-) rename security-csrf/src/main/java/io/micronaut/security/csrf/{validator/CompositeCsrfTokenValidator.java => repository/CompositeCsrfTokenRepository.java} (52%) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java index 4eac7550bf..12f2b1e2f4 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfTokenGenerator.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.generator; import io.micronaut.context.annotation.DefaultImplementation; +import io.micronaut.core.annotation.NonNull; /** * CSRF token Generation. @@ -26,10 +27,11 @@ @DefaultImplementation(DefaultCsrfTokenGenerator.class) @FunctionalInterface public interface CsrfTokenGenerator { - /** + * Generates a CSRF Token. * @param request Request * @return A CSRF Token. */ - String generate(T request); + @NonNull + String generateCsrfToken(@NonNull T request); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index bf203a6cfc..b779101856 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -17,108 +17,78 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.context.exceptions.ConfigurationException; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.cookie.Cookie; +import io.micronaut.http.cookie.CookieConfiguration; import io.micronaut.security.csrf.CsrfConfiguration; -import io.micronaut.security.csrf.validator.CsrfTokenValidator; import io.micronaut.security.session.SessionIdResolver; import io.micronaut.security.utils.HMacUtils; import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; -import java.util.Optional; /** * Default implementation of {@link CsrfTokenGenerator} which generates a random base 64 encoded string using an instance of {@link SecureRandom} and random byte array of size {@link CsrfConfiguration#getRandomValueSize()}. * @author Sergio del Amo * @since 4.11.0 + * @param Request */ -@Requires(classes = HttpRequest.class) +@Requires(classes = CookieConfiguration.class) @Singleton -@Internal -final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator>, CsrfTokenValidator> { - private static final Logger LOG = LoggerFactory.getLogger(DefaultCsrfTokenGenerator.class); +public final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator { private static final String SESSION_RANDOM_SEPARATOR = "!"; private static final String HMAC_RANDOM_SEPARATOR = "."; private final SecureRandom secureRandom = new SecureRandom(); private final CsrfConfiguration csrfConfiguration; - private final SessionIdResolver> sessionIdResolver; + private final SessionIdResolver sessionIdResolver; - DefaultCsrfTokenGenerator(CsrfConfiguration csrfConfiguration, - SessionIdResolver> sessionIdResolver) { + /** + * + * @param csrfConfiguration CSRF Configuration + * @param sessionIdResolver SessionID Resolver + */ + public DefaultCsrfTokenGenerator(CsrfConfiguration csrfConfiguration, + SessionIdResolver sessionIdResolver) { this.csrfConfiguration = csrfConfiguration; this.sessionIdResolver = sessionIdResolver; } @Override - public String generate(HttpRequest request) { - // Gather the values - String secret = csrfConfiguration.getSecretKey(); - String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session + @NonNull + public String generateCsrfToken(@NonNull T request) { byte[] tokenBytes = new byte[csrfConfiguration.getRandomValueSize()]; secureRandom.nextBytes(tokenBytes); String randomValue = Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes); // Cryptographic random value - // Create the CSRF Token - String message = sessionID + SESSION_RANDOM_SEPARATOR + randomValue; // HMAC message payload - try { - String hmac = secret != null - ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash - : ""; - // Add the `randomValue` to the HMAC hash to create the final CSRF token. Avoid using the `message` because it contains the sessionID in plain text, which the server already stores separately. - return hmac + HMAC_RANDOM_SEPARATOR + randomValue; - } catch (InvalidKeyException ex) { - throw new ConfigurationException("Invalid secret key for signing the CSRF token"); - } catch (NoSuchAlgorithmException ex) { - throw new ConfigurationException("Invalid algorithm for signing the CSRF token"); - } + String hmac = hmac(request, randomValue); + // Add the `randomValue` to the HMAC hash to create the final CSRF token. Avoid using the `message` because it contains the sessionID in plain text, which the server already stores separately. + return hmac + HMAC_RANDOM_SEPARATOR + randomValue; } - @Override - public boolean validateCsrfToken(@NonNull HttpRequest request, @NonNull String token) { - Optional csrfCookieOptional = findCsrfToken(request); - if (csrfCookieOptional.isEmpty()) { - return false; - } - String csrfCookie = csrfCookieOptional.get(); - return csrfCookie.equals(token) && validateHmac(request, csrfCookie); - } + /** + * + * @param request Request + * @param randomValue Cryptographic random value + * @return HMAC hash + */ + @NonNull + public String hmac(@NonNull T request, String randomValue) { + // Gather the values + String secret = csrfConfiguration.getSecretKey(); + String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session - private boolean validateHmac(HttpRequest request, @NonNull String csrfToken) { + // Create the CSRF Token + String message = sessionID + SESSION_RANDOM_SEPARATOR + randomValue; // HMAC message payload try { - String[] arr = csrfToken.split("\\."); - if (arr.length != 2) { - if (LOG.isWarnEnabled()) { - LOG.warn("Invalid CSRF token: {}", csrfToken); - } - return false; - } - String hmac = arr[0]; - String randomValue = arr[1]; - String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session - String message = sessionID + SESSION_RANDOM_SEPARATOR + randomValue; - String secret = csrfConfiguration.getSecretKey(); - String expectedHmac = secret != null + return secret != null ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash : ""; - return hmac.contains(expectedHmac); } catch (InvalidKeyException ex) { throw new ConfigurationException("Invalid secret key for signing the CSRF token"); } catch (NoSuchAlgorithmException ex) { throw new ConfigurationException("Invalid algorithm for signing the CSRF token"); } } - - private Optional findCsrfToken(HttpRequest request) { - return request.getCookies() - .findCookie(csrfConfiguration.getCookieName()) - .map(Cookie::getValue); - } } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CompositeCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java similarity index 52% rename from security-csrf/src/main/java/io/micronaut/security/csrf/validator/CompositeCsrfTokenValidator.java rename to security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java index cc1d839291..69790dd0e1 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/CompositeCsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java @@ -13,39 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.security.csrf.validator; +package io.micronaut.security.csrf.repository; import io.micronaut.context.annotation.Primary; -import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; + import java.util.List; +import java.util.Optional; /** - * Composite Pattern implementation of {@link CsrfTokenValidator}. + * Composite Pattern implementation of {@link CsrfTokenRepository}. * @see Composite Pattern * @param Request */ @Primary @Singleton -public class CompositeCsrfTokenValidator implements CsrfTokenValidator { - - private final List> csrfTokenValidators; +public class CompositeCsrfTokenRepository implements CsrfTokenRepository { + private final List> repositories; /** * - * @param csrfTokenValidators CSRF Token Validators + * @param repositories CSRF Token Repositories */ - public CompositeCsrfTokenValidator(List> csrfTokenValidators) { - this.csrfTokenValidators = csrfTokenValidators; + public CompositeCsrfTokenRepository(List> repositories) { + this.repositories = repositories; } @Override - public boolean validateCsrfToken(@NonNull T request, @NonNull String csrfToken) { - for (CsrfTokenValidator csrfTokenValidator : csrfTokenValidators) { - if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { - return true; - } - } - return false; + public Optional findCsrfToken(T request) { + return repositories.stream() + .map(r -> r.findCsrfToken(request)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); } } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java new file mode 100644 index 0000000000..dbd8c9d9e1 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.repository; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.csrf.CsrfConfiguration; +import jakarta.inject.Singleton; + +import java.util.Optional; + +/** + * Retrieves a CSRF Token from a Cookie, for example, in a Double Submit Cookie pattern. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Requires(classes = HttpRequest.class) +@Requires(property = "micronaut.security.csrf.repository.cookie.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Singleton +public class CookieCsrfTokenRepository implements CsrfTokenRepository> { + private final CsrfConfiguration csrfConfiguration; + + /** + * + * @param csrfConfiguration CSRF Configuration + */ + public CookieCsrfTokenRepository(CsrfConfiguration csrfConfiguration) { + this.csrfConfiguration = csrfConfiguration; + } + + @Override + public Optional findCsrfToken(HttpRequest request) { + return request.getCookies() + .findCookie(csrfConfiguration.getCookieName()) + .map(Cookie::getValue); + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java index 22e6266f5e..551805e4ca 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java @@ -94,7 +94,7 @@ public List getCookies(Authentication authentication, String refreshToke @NonNull private Cookie csrfCookie(@NonNull HttpRequest request) { - String csrfToken = csrfTokenGenerator.generate(request); + String csrfToken = csrfTokenGenerator.generateCsrfToken(request); return csrfCookie(csrfToken, request); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java index c8c4be85b4..acc7af8c87 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java @@ -15,6 +15,8 @@ */ package io.micronaut.security.csrf.repository; +import io.micronaut.core.order.Ordered; + import java.util.Optional; /** @@ -22,7 +24,7 @@ * @param Request */ @FunctionalInterface -public interface CsrfTokenRepository { +public interface CsrfTokenRepository extends Ordered { /** * * @param request Request diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index 41abe14ede..bad47f8845 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -34,8 +34,8 @@ @Singleton @Internal final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver> { - private final CsrfConfiguration csrfConfiguration; private static final int ORDER = -100; + private final CsrfConfiguration csrfConfiguration; HttpHeaderCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { this.csrfConfiguration = csrfConfiguration; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java index 5ac1ab789b..1e0df7cec5 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java @@ -16,8 +16,14 @@ package io.micronaut.security.csrf.validator; import io.micronaut.context.annotation.Requires; +import io.micronaut.security.csrf.generator.DefaultCsrfTokenGenerator; import io.micronaut.security.csrf.repository.CsrfTokenRepository; import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Optional; + /** * {@link CsrfTokenValidator} implementation that uses a {@link CsrfTokenRepository}. @@ -26,19 +32,49 @@ * @since 4.11.0 * @author Sergio del Amo */ -@Requires(bean = CsrfTokenRepository.class) +@Requires(beans = { CsrfTokenRepository.class, DefaultCsrfTokenGenerator.class}) @Singleton public class RepositoryCsrfTokenValidator implements CsrfTokenValidator { - private final CsrfTokenRepository csrfTokenRepository; + private static final Logger LOG = LoggerFactory.getLogger(RepositoryCsrfTokenValidator.class); + private final List> repositories; + private final DefaultCsrfTokenGenerator defaultCsrfTokenGenerator; - public RepositoryCsrfTokenValidator(CsrfTokenRepository csrfTokenRepository) { - this.csrfTokenRepository = csrfTokenRepository; + /** + * + * @param repositories CSRF Token Repositories + * @param defaultCsrfTokenGenerator Default CSRF Token Generator + */ + public RepositoryCsrfTokenValidator(List> repositories, + DefaultCsrfTokenGenerator defaultCsrfTokenGenerator) { + this.repositories = repositories; + this.defaultCsrfTokenGenerator = defaultCsrfTokenGenerator; } @Override - public boolean validateCsrfToken(T request, String token) { - return csrfTokenRepository.findCsrfToken(request) - .map(storedToken -> storedToken.equals(token)) - .orElse(false); + public boolean validateCsrfToken(T request, String csrfTokenInRequest) { + for (CsrfTokenRepository repo : repositories) { + Optional csrfTokenOptional = repo.findCsrfToken(request); + if (csrfTokenOptional.isPresent()) { + String csrfTokenInRepository = csrfTokenOptional.get(); + if (csrfTokenInRepository.equals(csrfTokenInRequest) && validateHmac(request, csrfTokenInRequest)) { + return true; + } + } + } + return false; + } + + private boolean validateHmac(T request, String csrfTokenInRequest) { + String[] arr = csrfTokenInRequest.split("\\."); + if (arr.length != 2) { + if (LOG.isWarnEnabled()) { + LOG.warn("Invalid CSRF token: {}", csrfTokenInRequest); + } + return false; + } + String hmac = arr[0]; + String randomValue = arr[1]; + String expectedHmac = defaultCsrfTokenGenerator.hmac(request, randomValue); + return hmac.contains(expectedHmac); } } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java index 92f4a7bb20..c919d9d1a1 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationTest.java @@ -35,7 +35,7 @@ void defaultHttpSessionName() { } @Test - void defaultTokenSize() { + void defaultRandomValueSize() { assertEquals(16, csrfConfiguration.getRandomValueSize()); } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java index 5e02680020..22a07be5e9 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfTokenGeneratorTest.java @@ -20,7 +20,7 @@ void generatedCsrfTokensAreUnique(CsrfTokenGenerator csrfTokenGenerator) { HttpRequest request = new SimpleHttpRequest<>(HttpMethod.POST, "/password/change", "usenrame=sherlock&password=123456"); Set results = new HashSet<>(); for (int i = 0; i < attempts; i++) { - results.add(csrfTokenGenerator.generate(request)); + results.add(csrfTokenGenerator.generateCsrfToken(request)); } assertEquals(attempts, results.size()); } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java index 528e012282..fb8f102e89 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java @@ -1,19 +1,24 @@ package io.micronaut.security.csrf.resolver; +import io.micronaut.context.BeanContext; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.*; import io.micronaut.http.client.BlockingHttpClient; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.simple.SimpleHttpRequest; import io.micronaut.security.annotation.Secured; +import io.micronaut.security.csrf.generator.CsrfTokenGenerator; import io.micronaut.security.csrf.repository.CsrfTokenRepository; import io.micronaut.security.rules.SecurityRule; import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; @@ -26,10 +31,16 @@ @MicronautTest class FieldCsrfTokenResolverTest { + @Inject + BeanContext beanContext; + @Test - void fieldTokenResolver(@Client("/") HttpClient httpClient) { + void fieldTokenResolver(@Client("/") HttpClient httpClient, + CsrfTokenGenerator> csrfTokenGenerator) { BlockingHttpClient client = httpClient.toBlocking(); - HttpRequest request = HttpRequest.POST("/password/change", "username=sherlock&csrfToken=abcde&password=elementary") + String csrfToken = csrfTokenGenerator.generateCsrfToken(new SimpleHttpRequest<>(HttpMethod.POST,"/password/change", "username=sherlock&password=elementary")); + beanContext.registerSingleton(new CsrfTokenRepositoryReplacement(csrfToken)); + HttpRequest request = HttpRequest.POST("/password/change", "username=sherlock&csrfToken="+ csrfToken + "&password=elementary") .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .accept(MediaType.TEXT_HTML); String result = assertDoesNotThrow(() -> client.retrieve(request)); @@ -37,12 +48,14 @@ void fieldTokenResolver(@Client("/") HttpClient httpClient) { } @Requires(property = "spec.name", value = "FieldCsrfTokenResolverTest") - @Singleton - @Replaces(CsrfTokenRepository.class) static class CsrfTokenRepositoryReplacement implements CsrfTokenRepository> { + private final String csrfToken; + CsrfTokenRepositoryReplacement(String csrfToken) { + this.csrfToken = csrfToken; + } @Override public Optional findCsrfToken(HttpRequest request) { - return Optional.of("abcde"); + return Optional.of(csrfToken); } } diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java index fb2ee49fed..d9596f59ef 100644 --- a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java +++ b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java @@ -86,7 +86,7 @@ public List getCookies(Authentication authentication, String refreshToke @NonNull private Cookie csrfCookie(@NonNull HttpRequest request) { - String csrfToken = csrfTokenGenerator.generate(request); + String csrfToken = csrfTokenGenerator.generateCsrfToken(request); return csrfCookie(csrfToken, request); } diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java index 645b8e5130..86d3ccf0e7 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java @@ -72,7 +72,7 @@ public CsrfSessionLogingHandler( @Override protected Session saveAuthenticationInSession(Authentication authentication, HttpRequest request) { Session session = super.saveAuthenticationInSession(authentication, request); - session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generate(request)); + session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generateCsrfToken(request)); return session; } } From 328b6abf1b00edae10ad43099ec71cc814fb22d7 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:06:58 +0200 Subject: [PATCH 028/108] small improvements --- .../csrf/generator/DefaultCsrfTokenGenerator.java | 8 ++++++-- .../csrf/repository/CookieCsrfTokenRepository.java | 2 +- .../security/csrf/resolver/FieldCsrfTokenResolver.java | 2 +- .../csrf/validator/RepositoryCsrfTokenValidator.java | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index b779101856..42c10f1c84 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -30,7 +30,8 @@ import java.util.Base64; /** - * Default implementation of {@link CsrfTokenGenerator} which generates a random base 64 encoded string using an instance of {@link SecureRandom} and random byte array of size {@link CsrfConfiguration#getRandomValueSize()}. + * Default implementation of {@link CsrfTokenGenerator} which generates a CSRF Token prefixed by an HMAC if a secret key is set. + * @see Pseudo Code for implementing hmac CSRF tokens * @author Sergio del Amo * @since 4.11.0 * @param Request @@ -38,8 +39,11 @@ @Requires(classes = CookieConfiguration.class) @Singleton public final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator { + /** + * hmac random value separator. + */ + public static final String HMAC_RANDOM_SEPARATOR = "."; private static final String SESSION_RANDOM_SEPARATOR = "!"; - private static final String HMAC_RANDOM_SEPARATOR = "."; private final SecureRandom secureRandom = new SecureRandom(); private final CsrfConfiguration csrfConfiguration; private final SessionIdResolver sessionIdResolver; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java index dbd8c9d9e1..6440f1b959 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java @@ -25,7 +25,7 @@ import java.util.Optional; /** - * Retrieves a CSRF Token from a Cookie, for example, in a Double Submit Cookie pattern. + * Retrieves a CSRF Token from a Cookie named {@link CsrfConfiguration#getCookieName()}, for example, in a Double Submit Cookie pattern. * @author Sergio del Amo * @since 4.11.0 */ diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 7116242f76..8b1c3fd900 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -31,7 +31,7 @@ import java.util.Optional; /** - * Resolves a CSRF token from a form-urlencoded body using the {@link ServerHttpRequest#byteBody()} API.. + * Resolves a CSRF token from a form-urlencoded body using the {@link ServerHttpRequest#byteBody()} API. * * @since 2.0.0 */ diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java index 1e0df7cec5..e01fc0d9b0 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java @@ -65,7 +65,7 @@ public boolean validateCsrfToken(T request, String csrfTokenInRequest) { } private boolean validateHmac(T request, String csrfTokenInRequest) { - String[] arr = csrfTokenInRequest.split("\\."); + String[] arr = csrfTokenInRequest.split("\\" + DefaultCsrfTokenGenerator.HMAC_RANDOM_SEPARATOR); if (arr.length != 2) { if (LOG.isWarnEnabled()) { LOG.warn("Invalid CSRF token: {}", csrfTokenInRequest); @@ -75,6 +75,6 @@ private boolean validateHmac(T request, String csrfTokenInRequest) { String hmac = arr[0]; String randomValue = arr[1]; String expectedHmac = defaultCsrfTokenGenerator.hmac(request, randomValue); - return hmac.contains(expectedHmac); + return hmac.equals(expectedHmac); } } From d6d723f837f73c05fd8ee8fd271ff12092b4ce9e Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:10:53 +0200 Subject: [PATCH 029/108] change test --- .../csrf/repository/CsrfDoubleSubmitCookiePatternTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java index deb2100146..5b6ab4de8a 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -66,7 +66,7 @@ void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, String csrfToken = "abcdefg"; assertNotEquals(cookieCsrfToken.getValue(), csrfToken); PasswordChangeForm formWithCsrfToken = new PasswordChangeForm("sherlock", "evil", csrfToken); - assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, formWithCsrfToken, csrfToken); + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, formWithCsrfToken, cookieCsrfToken.getValue()); // CSRF Token with HMAC but not session id feed into HMAC calculation, request is unauthorized String randomValue = "abcdefg"; From bd59e29141354e0e87adca21f7e5ecb5c7e44e48 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:12:27 +0200 Subject: [PATCH 030/108] test --- .../csrf/repository/CsrfDoubleSubmitCookiePatternTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java index 5b6ab4de8a..d6109604d3 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -73,7 +73,7 @@ void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, String hmac = HMacUtils.base64EncodedHmacSha256(randomValue, csrfConfiguration.getSecretKey()); String csrfTokenCalculatedWithoutSessionId = hmac + "." + randomValue; PasswordChangeForm body = new PasswordChangeForm("sherlock", "evil", csrfTokenCalculatedWithoutSessionId); - assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, body, csrfToken); + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, body, csrfTokenCalculatedWithoutSessionId); String message = FIX_SESSION_ID + "!" + randomValue; hmac = HMacUtils.base64EncodedHmacSha256(message, csrfConfiguration.getSecretKey()); From 8b646be17b9d77ac609556010173f7f8a4b8167d Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:14:03 +0200 Subject: [PATCH 031/108] better test --- .../csrf/repository/CsrfDoubleSubmitCookiePatternTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java index d6109604d3..d82b365f55 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -82,7 +82,7 @@ void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, // Even if you have the same session id and random value, the attacker cannot generate the same hmac as he does not have the same secret key String evilSignatureKey = "evilAyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAowevil"; - csrfToken = HMacUtils.base64EncodedHmacSha256(message, evilSignatureKey); + csrfToken = HMacUtils.base64EncodedHmacSha256(message, evilSignatureKey) + "." + randomValue; assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, new PasswordChangeForm("sherlock", "evil", csrfToken), csrfToken); // CSRF Token in request match token in cookie and hmac signature is valid. From ebb0c25ad2e2cee4eedb89278d01b8fae4fceb95 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:19:31 +0200 Subject: [PATCH 032/108] add nullability annotation --- .../csrf/repository/CsrfDoubleSubmitCookiePatternTest.java | 4 +++- .../token/jwt/validator/JsonWebTokenIdSessionIdResolver.java | 4 +++- .../security/session/csrf/HttpSessionSessionIdResolver.java | 4 +++- .../security/session/CompositeSessionIdResolver.java | 4 +++- .../java/io/micronaut/security/session/SessionIdResolver.java | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java index d82b365f55..3595fe510d 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java @@ -2,6 +2,7 @@ import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.StringUtils; import io.micronaut.http.*; import io.micronaut.http.annotation.*; @@ -115,7 +116,8 @@ private void assertOk(BlockingHttpClient client, String cookieJwt, String csrfTo @Singleton static class MockSessionIdResolver implements SessionIdResolver> { @Override - public Optional findSessionId(HttpRequest request) { + @NonNull + public Optional findSessionId(@NonNull HttpRequest request) { return Optional.of(FIX_SESSION_ID); } } diff --git a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java index 2fabc9ae1b..537164aaeb 100644 --- a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java +++ b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java @@ -16,6 +16,7 @@ package io.micronaut.security.token.jwt.validator; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequest; import io.micronaut.security.session.SessionIdResolver; import jakarta.inject.Singleton; @@ -42,7 +43,8 @@ public JsonWebTokenIdSessionIdResolver(JsonWebTokenParser jsonWebTokenParser) } @Override - public Optional findSessionId(HttpRequest request) { + @NonNull + public Optional findSessionId(@NonNull HttpRequest request) { return request.getAttribute(TOKEN, String.class) .flatMap(jsonWebTokenParser::parseClaims) .flatMap(claims -> Optional.ofNullable(claims.get(TOKEN_ID)).map(Object::toString)); diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java b/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java index b2f284fcc0..3fe626e9a5 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java +++ b/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.session.csrf; +import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequest; import io.micronaut.security.session.SessionIdResolver; import io.micronaut.session.Session; @@ -31,7 +32,8 @@ @Singleton public class HttpSessionSessionIdResolver implements SessionIdResolver> { @Override - public Optional findSessionId(HttpRequest request) { + @NonNull + public Optional findSessionId(@NonNull HttpRequest request) { return SessionForRequest.find(request).map(Session::getId); } } diff --git a/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java index bd7727da14..d3d50b521d 100644 --- a/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java +++ b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java @@ -16,6 +16,7 @@ package io.micronaut.security.session; import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; import java.util.List; import java.util.Optional; @@ -40,7 +41,8 @@ public CompositeSessionIdResolver(List> sessionIdResolvers) } @Override - public Optional findSessionId(T request) { + @NonNull + public Optional findSessionId(@NonNull T request) { return sessionIdResolvers.stream() .map(sessionIdResolver -> sessionIdResolver.findSessionId(request)) .filter(Optional::isPresent) diff --git a/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java index a01cda90aa..d9563f2783 100644 --- a/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java +++ b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java @@ -16,6 +16,7 @@ package io.micronaut.security.session; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.Ordered; import java.util.Optional; @@ -33,5 +34,6 @@ public interface SessionIdResolver extends Ordered { * @param request Request * @return Session ID for the given request. Empty if no session ID was found. */ - Optional findSessionId(T request); + @NonNull + Optional findSessionId(@NonNull T request); } From 288b81eb93385323f242fc809ae88be0418ff67a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:20:53 +0200 Subject: [PATCH 033/108] remove extra space --- .../src/main/java/io/micronaut/security/utils/HMacUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/security/src/main/java/io/micronaut/security/utils/HMacUtils.java b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java index 45aafe8c94..f0d1a95ecc 100644 --- a/security/src/main/java/io/micronaut/security/utils/HMacUtils.java +++ b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java @@ -44,7 +44,7 @@ private HMacUtils() { * @throws NoSuchAlgorithmException if no {@code Provider} supports a {@code MacSpi} implementation for the specified algorithm. * @throws InvalidKeyException if the given key is inappropriate for initializing this MAC. */ - public static String base64EncodedHmacSha256(@NonNull String data, @NonNull String key) throws NoSuchAlgorithmException, InvalidKeyException { + public static String base64EncodedHmacSha256(@NonNull String data, @NonNull String key) throws NoSuchAlgorithmException, InvalidKeyException { return base64EncodedHmac(HMAC_SHA256, data, key); } @@ -57,7 +57,7 @@ public static String base64EncodedHmacSha256(@NonNull String data, @NonNull Str * @throws NoSuchAlgorithmException if no {@code Provider} supports a {@code MacSpi} implementation for the specified algorithm. * @throws InvalidKeyException if the given key is inappropriate for initializing this MAC. */ - public static String base64EncodedHmac(@NonNull String algorithm, @NonNull String data, @NonNull String key) + public static String base64EncodedHmac(@NonNull String algorithm, @NonNull String data, @NonNull String key) throws NoSuchAlgorithmException, InvalidKeyException { SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm); Mac mac = Mac.getInstance(algorithm); From 0433459ade61dcdf6de18469049934fa11843ce7 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:42:43 +0200 Subject: [PATCH 034/108] Add SessionPopulator api --- security-csrf/build.gradle.kts | 2 + .../csrf/session/CsrfSessionPopulator.java | 39 ++++++++++ .../HttpSessionSessionIdResolver.java | 2 +- .../session}/SessionCsrfTokenRepository.java | 2 +- .../security/csrf/session}/package-info.java | 10 +-- .../CsrfSessionLogingHandlerTest.java | 2 +- security-session/build.gradle.kts | 2 - .../session/DefaultSessionPopulator.java | 30 +++++++ .../security/session/SessionLoginHandler.java | 33 ++++++-- .../security/session/SessionPopulator.java | 40 ++++++++++ .../csrf/CsrfSessionLogingHandler.java | 78 ------------------- ...SessionAuthenticationNoRedirectSpec.groovy | 1 - ...tpRequestAuthenticationProviderSpec.groovy | 1 - ...tExecutorAuthenticationProviderSpec.groovy | 1 - ...tReactiveAuthenticationProviderSpec.groovy | 1 - .../RedirectRejectionHandlerSpec.groovy | 1 - .../security/session/ContextPathSpec.groovy | 1 - .../RejectionHandlerResolutionSpec.groovy | 4 +- ...essionBeansWithSecurityDisabledSpec.groovy | 3 +- ...eansWithSecuritySessionDisabledSpec.groovy | 1 - .../SessionLoginHandlerContextPathSpec.groovy | 1 - ...SessionLogoutHandlerContextPathSpec.groovy | 1 - .../security/session/SessionReUseSpec.groovy | 1 - .../session/UnauthorizedTargetUrlSpec.groovy | 1 - 24 files changed, 145 insertions(+), 113 deletions(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java rename {security-session/src/main/java/io/micronaut/security/session/csrf => security-csrf/src/main/java/io/micronaut/security/csrf/session}/HttpSessionSessionIdResolver.java (96%) rename {security-session/src/main/java/io/micronaut/security/session/csrf => security-csrf/src/main/java/io/micronaut/security/csrf/session}/SessionCsrfTokenRepository.java (97%) rename {security-session/src/main/java/io/micronaut/security/session/csrf => security-csrf/src/main/java/io/micronaut/security/csrf/session}/package-info.java (65%) rename {security-session/src/test/java/io/micronaut/security/session/csrf => security-csrf/src/test/java/io/micronaut/security/csrf/session}/CsrfSessionLogingHandlerTest.java (99%) create mode 100644 security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java create mode 100644 security-session/src/main/java/io/micronaut/security/session/SessionPopulator.java delete mode 100644 security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index 57b5c8b274..7984988305 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(projects.micronautSecurity) compileOnly(mn.micronaut.http.server) + compileOnly(projects.micronautSecuritySession) testAnnotationProcessor(mn.micronaut.inject.java) testImplementation(mnTest.micronaut.test.junit5) @@ -16,6 +17,7 @@ dependencies { testImplementation(mnSerde.micronaut.serde.jackson) testImplementation(projects.testSuiteUtilsSecurity) testImplementation(projects.micronautSecurityJwt) + testImplementation(projects.micronautSecuritySession) } tasks.withType { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java new file mode 100644 index 0000000000..d66ca4aca9 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.session; + +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.generator.CsrfTokenGenerator; +import io.micronaut.security.session.SessionPopulator; +import io.micronaut.session.Session; +import jakarta.inject.Singleton; + +@Singleton +public class CsrfSessionPopulator implements SessionPopulator { + private final CsrfConfiguration csrfConfiguration; + private final CsrfTokenGenerator csrfTokenGenerator; + + public CsrfSessionPopulator(CsrfConfiguration csrfConfiguration, CsrfTokenGenerator csrfTokenGenerator) { + this.csrfConfiguration = csrfConfiguration; + this.csrfTokenGenerator = csrfTokenGenerator; + } + + @Override + public void populateSession(T request, Authentication authentication, Session session) { + session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generateCsrfToken(request)); + } +} diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java similarity index 96% rename from security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java rename to security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java index 3fe626e9a5..999537504e 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/HttpSessionSessionIdResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.security.session.csrf; +package io.micronaut.security.csrf.session; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequest; diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java similarity index 97% rename from security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java rename to security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java index 80de897f6f..27d3286db7 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/SessionCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.security.session.csrf; +package io.micronaut.security.csrf.session; import io.micronaut.context.annotation.Requires; import io.micronaut.core.util.StringUtils; diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java similarity index 65% rename from security-session/src/main/java/io/micronaut/security/session/csrf/package-info.java rename to security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java index 62198cd79a..951d1d674f 100644 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java @@ -14,14 +14,8 @@ * limitations under the License. */ /** - * Classes related to Cross Site Request Forgery (CSRF) and HTTP Session. + * Classes related to CSRF and HTTP session. * @author Sergio del Amo * @since 4.11.0 */ -@Requires(classes = CsrfTokenRepository.class) -@Configuration -package io.micronaut.security.session.csrf; - -import io.micronaut.context.annotation.Configuration; -import io.micronaut.context.annotation.Requires; -import io.micronaut.security.csrf.repository.CsrfTokenRepository; +package io.micronaut.security.csrf.session; \ No newline at end of file diff --git a/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/session/CsrfSessionLogingHandlerTest.java similarity index 99% rename from security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java rename to security-csrf/src/test/java/io/micronaut/security/csrf/session/CsrfSessionLogingHandlerTest.java index 620652ef60..67658030ca 100644 --- a/security-session/src/test/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandlerTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/session/CsrfSessionLogingHandlerTest.java @@ -1,4 +1,4 @@ -package io.micronaut.security.session.csrf; +package io.micronaut.security.csrf.session; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; diff --git a/security-session/build.gradle.kts b/security-session/build.gradle.kts index 151fd1132b..13d690277d 100644 --- a/security-session/build.gradle.kts +++ b/security-session/build.gradle.kts @@ -7,7 +7,6 @@ dependencies { api(mnSession.micronaut.session) api(projects.micronautSecurity) implementation(mnReactor.micronaut.reactor) - compileOnly(projects.micronautSecurityCsrf) testAnnotationProcessor(mnSerde.micronaut.serde.processor) testImplementation(mnSerde.micronaut.serde.jackson) @@ -20,7 +19,6 @@ dependencies { testAnnotationProcessor(mn.micronaut.inject.java) testImplementation(mnTest.micronaut.test.junit5) - testImplementation(projects.micronautSecurityCsrf) testRuntimeOnly(libs.junit.jupiter.engine) testRuntimeOnly(mnLogging.logback.classic) diff --git a/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java b/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java new file mode 100644 index 0000000000..315ad4c667 --- /dev/null +++ b/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.session; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.filters.SecurityFilter; +import io.micronaut.session.Session; +import jakarta.inject.Singleton; + +@Singleton +public class DefaultSessionPopulator implements SessionPopulator { + @Override + public void populateSession(T request, @NonNull Authentication authentication, @NonNull Session session) { + session.put(SecurityFilter.AUTHENTICATION, authentication); + } +} diff --git a/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java b/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java index 3a745cc6c6..546992e7e7 100644 --- a/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java +++ b/security-session/src/main/java/io/micronaut/security/session/SessionLoginHandler.java @@ -28,14 +28,15 @@ import io.micronaut.security.config.RedirectConfiguration; import io.micronaut.security.config.RedirectService; import io.micronaut.security.errors.PriorToLoginPersistence; -import io.micronaut.security.filters.SecurityFilter; import io.micronaut.security.handlers.RedirectingLoginHandler; import io.micronaut.session.Session; import io.micronaut.session.SessionStore; import io.micronaut.session.http.SessionForRequest; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; import java.util.Optional; /** @@ -60,22 +61,44 @@ public class SessionLoginHandler implements RedirectingLoginHandler, MutableHttpResponse> priorToLoginPersistence; + private final List>> sessionPopulators; + /** * Constructor. * @param redirectConfiguration Redirect configuration * @param sessionStore The session store * @param priorToLoginPersistence The persistence to store the original url * @param redirectService Redirection Service + * @param sessionPopulators Session Populators */ + @Inject public SessionLoginHandler(RedirectConfiguration redirectConfiguration, SessionStore sessionStore, @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, - RedirectService redirectService) { + RedirectService redirectService, + List>> sessionPopulators) { this.loginFailure = redirectConfiguration.isEnabled() ? redirectService.loginFailureUrl() : null; this.loginSuccess = redirectConfiguration.isEnabled() ? redirectService.loginSuccessUrl() : null; this.redirectConfiguration = redirectConfiguration; this.sessionStore = sessionStore; this.priorToLoginPersistence = priorToLoginPersistence; + this.sessionPopulators = sessionPopulators; + } + + /** + * Constructor. + * @param redirectConfiguration Redirect configuration + * @param sessionStore The session store + * @param priorToLoginPersistence The persistence to store the original url + * @param redirectService Redirection Service + * @deprecated Use {@link #SessionLoginHandler(RedirectConfiguration, SessionStore, PriorToLoginPersistence, RedirectService, List)} instead. + */ + @Deprecated(forRemoval = true, since = "4.11.0") + public SessionLoginHandler(RedirectConfiguration redirectConfiguration, + SessionStore sessionStore, + @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, + RedirectService redirectService) { + this(redirectConfiguration, sessionStore, priorToLoginPersistence, redirectService, List.of(new DefaultSessionPopulator<>())); } @Override @@ -134,11 +157,9 @@ private ThrowingSupplier loginSuccessUriSupplier(@NonNu * Saves the authentication in the session. * @param authentication Authentication * @param request HTTP Request - * @return The session found or created where the authentication was saved. */ - protected Session saveAuthenticationInSession(Authentication authentication, HttpRequest request) { + private void saveAuthenticationInSession(Authentication authentication, HttpRequest request) { Session session = SessionForRequest.find(request).orElseGet(() -> SessionForRequest.create(sessionStore, request)); - session.put(SecurityFilter.AUTHENTICATION, authentication); - return session; + sessionPopulators.forEach(sessionPopulator -> sessionPopulator.populateSession(request, authentication, session)); } } diff --git a/security-session/src/main/java/io/micronaut/security/session/SessionPopulator.java b/security-session/src/main/java/io/micronaut/security/session/SessionPopulator.java new file mode 100644 index 0000000000..c6ea9a3e2b --- /dev/null +++ b/security-session/src/main/java/io/micronaut/security/session/SessionPopulator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.session; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.order.Ordered; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.session.Session; + +/** + * API that allows to populate the session after a successful login. You can create extra beans of type {@link SessionPopulator} to add extra data to the session. + * @author Sergio del Amo + * @since 4.11.0 + * @param Request + */ +public interface SessionPopulator extends Ordered { + + /** + * Populates the session. + * @param request The request + * @param authentication The authenticated user. + * @param session The session + */ + void populateSession(@NonNull T request, + @NonNull Authentication authentication, + @NonNull Session session); +} diff --git a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java b/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java deleted file mode 100644 index 86d3ccf0e7..0000000000 --- a/security-session/src/main/java/io/micronaut/security/session/csrf/CsrfSessionLogingHandler.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * 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 - * - * https://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 License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.security.session.csrf; - -import io.micronaut.context.annotation.Replaces; -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.security.authentication.Authentication; -import io.micronaut.security.config.RedirectConfiguration; -import io.micronaut.security.config.RedirectService; -import io.micronaut.security.csrf.CsrfConfiguration; -import io.micronaut.security.csrf.generator.CsrfTokenGenerator; -import io.micronaut.security.errors.PriorToLoginPersistence; -import io.micronaut.security.session.SessionAuthenticationModeCondition; -import io.micronaut.security.session.SessionLoginHandler; -import io.micronaut.session.Session; -import io.micronaut.session.SessionStore; -import jakarta.inject.Singleton; - -/** - * Replacement of {@link SessionLoginHandler} that extends it and saves a CSRF token in the session. - * @author Sergio del Amo - * @since 4.11.0 - */ -@Requires(condition = SessionAuthenticationModeCondition.class) -@Requires(beans = { CsrfConfiguration.class, CsrfTokenGenerator.class }) -@Replaces(SessionLoginHandler.class) -@Singleton -public class CsrfSessionLogingHandler extends SessionLoginHandler { - - private final CsrfConfiguration csrfConfiguration; - private final CsrfTokenGenerator csrfTokenGenerator; - - /** - * Constructor. - * - * @param redirectConfiguration Redirect configuration - * @param sessionStore The session store - * @param priorToLoginPersistence The persistence to store the original url - * @param redirectService Redirection Service - * @param csrfConfiguration CSRF Configuration - * @param csrfTokenGenerator CSRF Token Generator - */ - public CsrfSessionLogingHandler( - RedirectConfiguration redirectConfiguration, - SessionStore sessionStore, - @Nullable PriorToLoginPersistence, - MutableHttpResponse> priorToLoginPersistence, - RedirectService redirectService, - CsrfConfiguration csrfConfiguration, - CsrfTokenGenerator csrfTokenGenerator) { - super(redirectConfiguration, sessionStore, priorToLoginPersistence, redirectService); - this.csrfConfiguration = csrfConfiguration; - this.csrfTokenGenerator = csrfTokenGenerator; - } - - @Override - protected Session saveAuthenticationInSession(Authentication authentication, HttpRequest request) { - Session session = super.saveAuthenticationInSession(authentication, request); - session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generateCsrfToken(request)); - return session; - } -} diff --git a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy index 8b1384a8cd..4d6daf97aa 100644 --- a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy @@ -24,7 +24,6 @@ class SessionAuthenticationNoRedirectSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.authentication': 'session', 'micronaut.security.redirect.enabled': false, 'spec.name': 'SessionAuthenticationNoRedirectSpec', diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy index a0294e3189..5ba4d0865d 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestAuthenticationProviderSpec.groovy @@ -32,7 +32,6 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification -@Property(name = "micronaut.security.csrf.enabled", value=StringUtils.FALSE) @Property(name = "micronaut.security.authentication", value="session") @Property(name = "micronaut.http.client.follow-redirects", value = StringUtils.FALSE) @Property(name = "micronaut.security.redirect.login-failure", value="/security/login") diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy index 0b96488a84..7ed848a746 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestExecutorAuthenticationProviderSpec.groovy @@ -35,7 +35,6 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification -@Property(name = "micronaut.security.csrf.enabled", value=StringUtils.FALSE) @Property(name = "micronaut.security.authentication", value="session") @Property(name = "micronaut.http.client.follow-redirects", value = StringUtils.FALSE) @Property(name = "micronaut.security.redirect.login-failure", value="/security/login") diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy index a000816513..6043997aea 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/LoginFailedHttpRequestReactiveAuthenticationProviderSpec.groovy @@ -35,7 +35,6 @@ import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification -@Property(name = "micronaut.security.csrf.enabled", value=StringUtils.FALSE) @Property(name = "micronaut.security.authentication", value="session") @Property(name = "micronaut.http.client.follow-redirects", value = StringUtils.FALSE) @Property(name = "micronaut.security.redirect.login-failure", value="/security/login") diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy index ef59790dc0..011dda8d8a 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy @@ -29,7 +29,6 @@ class RedirectRejectionHandlerSpec extends EmbeddedServerSpecification { Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.redirect.unauthorized.url': '/login', 'micronaut.security.redirect.forbidden.url': '/forbidden' ] diff --git a/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy index 94f870a243..3d15e75b84 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy @@ -28,7 +28,6 @@ class ContextPathSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.server.context-path': 'foo', 'micronaut.security.authentication': 'session' ] diff --git a/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy index 282fd884e7..88cdc99553 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy @@ -24,9 +24,7 @@ class RejectionHandlerResolutionSpec extends ApplicationContextSpecification { @Override Map getConfiguration() { - return super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE - ] + return super.configuration } void "RedirectRejectionHandler is the default rejection handler resolved"() { diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy index 8e7764f2a5..b5d7c92c8e 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy @@ -9,8 +9,7 @@ class SecuritySessionBeansWithSecurityDisabledSpec extends ApplicationContextSpe @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.enabled': false, - 'micronaut.security.csrf.enabled': StringUtils.FALSE + 'micronaut.security.enabled': false ] } diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy index 09f8a10bb4..d7c6e6acb7 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy @@ -10,7 +10,6 @@ class SecuritySessionBeansWithSecuritySessionDisabledSpec extends ApplicationCon @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.session.enabled': false, ] } diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy index d5745df1af..86b5c1e748 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy @@ -7,7 +7,6 @@ class SessionLoginHandlerContextPathSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.server.context-path': 'foo', 'micronaut.security.authentication': 'session' ] diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy index 453ddae2e7..7cd9eb4722 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy @@ -7,7 +7,6 @@ class SessionLogoutHandlerContextPathSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.server.context-path': 'foo', 'micronaut.security.authentication': 'session' ] diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy index 3960410e88..a21c3d72f0 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy @@ -23,7 +23,6 @@ class SessionReUseSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.authentication': 'session', ] } diff --git a/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy index c97bd2a8d9..562b3e6d3d 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy @@ -18,7 +18,6 @@ class UnauthorizedTargetUrlSpec extends EmbeddedServerSpecification { @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.csrf.enabled': StringUtils.FALSE, 'micronaut.security.redirect.unauthorized.url': '/login/auth', 'micronaut.security.intercept-url-map': [ [pattern: '/login/auth', httpMethod: 'GET', access: ['isAnonymous()']] From 38f66ab3094846351f3335f92b05a8cd443ea899 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:45:31 +0200 Subject: [PATCH 035/108] remove unused imports --- .../session/SessionAuthenticationNoRedirectSpec.groovy | 2 -- .../security/handlers/RedirectRejectionHandlerSpec.groovy | 7 ++----- .../io/micronaut/security/session/ContextPathSpec.groovy | 1 - .../security/session/RejectionHandlerResolutionSpec.groovy | 7 ------- .../SecuritySessionBeansWithSecurityDisabledSpec.groovy | 3 +-- ...uritySessionBeansWithSecuritySessionDisabledSpec.groovy | 1 - .../session/SessionLoginHandlerContextPathSpec.groovy | 1 - .../session/SessionLogoutHandlerContextPathSpec.groovy | 1 - .../io/micronaut/security/session/SessionReUseSpec.groovy | 1 - .../security/session/UnauthorizedTargetUrlSpec.groovy | 1 - 10 files changed, 3 insertions(+), 22 deletions(-) diff --git a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy index 4d6daf97aa..270d8c7455 100644 --- a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy @@ -2,7 +2,6 @@ package io.micronaut.docs.security.session import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.Nullable -import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.MediaType @@ -14,7 +13,6 @@ import io.micronaut.security.annotation.Secured import io.micronaut.security.testutils.EmbeddedServerSpecification import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario -import io.netty.util.internal.StringUtil import jakarta.inject.Singleton import java.security.Principal diff --git a/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy index 011dda8d8a..be5074a3e1 100644 --- a/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/handlers/RedirectRejectionHandlerSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.security.handlers 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.HttpStatus @@ -28,10 +27,8 @@ class RedirectRejectionHandlerSpec extends EmbeddedServerSpecification { String accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" Map getConfiguration() { - super.configuration + [ - 'micronaut.security.redirect.unauthorized.url': '/login', - 'micronaut.security.redirect.forbidden.url': '/forbidden' - ] + super.configuration + ['micronaut.security.redirect.unauthorized.url': '/login', + 'micronaut.security.redirect.forbidden.url': '/forbidden'] } void "UnauthorizedRejectionUriProvider is used for 401"() { diff --git a/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy index 3d15e75b84..7141e0c43c 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/ContextPathSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.security.session import io.micronaut.context.annotation.Requires -import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType diff --git a/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy index 88cdc99553..bbbac53026 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/RejectionHandlerResolutionSpec.groovy @@ -4,7 +4,6 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Replaces import io.micronaut.context.annotation.Requires import io.micronaut.context.exceptions.NoSuchBeanException -import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.MutableHttpResponse import io.micronaut.http.server.exceptions.ExceptionHandler @@ -12,7 +11,6 @@ import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.security.authentication.AuthorizationException import io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler import io.micronaut.security.testutils.ApplicationContextSpecification -import io.micronaut.security.testutils.ConfigurationFixture import jakarta.inject.Singleton class RejectionHandlerResolutionSpec extends ApplicationContextSpecification { @@ -22,11 +20,6 @@ class RejectionHandlerResolutionSpec extends ApplicationContextSpecification { 'RejectionHandlerResolutionSpec' } - @Override - Map getConfiguration() { - return super.configuration - } - void "RedirectRejectionHandler is the default rejection handler resolved"() { given: ApplicationContext ctx = ApplicationContext.run([:]) diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy index b5d7c92c8e..af953f420f 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecurityDisabledSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.security.session import io.micronaut.context.exceptions.NoSuchBeanException -import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.ApplicationContextSpecification import spock.lang.Unroll @@ -9,7 +8,7 @@ class SecuritySessionBeansWithSecurityDisabledSpec extends ApplicationContextSpe @Override Map getConfiguration() { super.configuration + [ - 'micronaut.security.enabled': false + 'micronaut.security.enabled': false, ] } diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy index d7c6e6acb7..9cc7c6886e 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SecuritySessionBeansWithSecuritySessionDisabledSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.security.session import io.micronaut.context.exceptions.NoSuchBeanException -import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.ApplicationContextSpecification import spock.lang.Unroll diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy index 86b5c1e748..ab74db0e5b 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionLoginHandlerContextPathSpec.groovy @@ -1,6 +1,5 @@ package io.micronaut.security.session -import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.EmbeddedServerSpecification class SessionLoginHandlerContextPathSpec extends EmbeddedServerSpecification { diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy index 7cd9eb4722..2485e97284 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionLogoutHandlerContextPathSpec.groovy @@ -1,6 +1,5 @@ package io.micronaut.security.session -import io.micronaut.core.util.StringUtils import io.micronaut.security.testutils.EmbeddedServerSpecification class SessionLogoutHandlerContextPathSpec extends EmbeddedServerSpecification { diff --git a/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy index a21c3d72f0..a986732c38 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/SessionReUseSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.security.session 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 diff --git a/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy b/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy index 562b3e6d3d..2053c805d7 100644 --- a/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/security/session/UnauthorizedTargetUrlSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.security.session import io.micronaut.context.annotation.Requires -import io.micronaut.core.util.StringUtils import io.micronaut.http.HttpRequest import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get From 50e9c8ea8c49be043e9f46786f1c73580c97bfc2 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:46:04 +0200 Subject: [PATCH 036/108] remove extra dependencies --- security-session/build.gradle.kts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/security-session/build.gradle.kts b/security-session/build.gradle.kts index 13d690277d..49bb4890c9 100644 --- a/security-session/build.gradle.kts +++ b/security-session/build.gradle.kts @@ -16,11 +16,4 @@ dependencies { testImplementation(mn.micronaut.http.server.netty) testImplementation(projects.testSuiteUtils) testImplementation(projects.testSuiteUtilsSecurity) - - testAnnotationProcessor(mn.micronaut.inject.java) - testImplementation(mnTest.micronaut.test.junit5) - - testRuntimeOnly(libs.junit.jupiter.engine) - testRuntimeOnly(mnLogging.logback.classic) - } From 3270995aed99a438a2a5996fbb3768ff86462159 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 12:46:38 +0200 Subject: [PATCH 037/108] remove logback config --- security-session/src/test/resources/logback.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/security-session/src/test/resources/logback.xml b/security-session/src/test/resources/logback.xml index 67787a909e..432f5aa24e 100644 --- a/security-session/src/test/resources/logback.xml +++ b/security-session/src/test/resources/logback.xml @@ -7,5 +7,4 @@ - \ No newline at end of file From 6e44aaa3dbb9c5af9e67313aec17c160c0c1275c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 13:26:31 +0200 Subject: [PATCH 038/108] add LoginCookieProvider api --- .../CrsrfTokenCookieLoginHandler.java | 107 ------------------ .../repository/CsrfLoginCookieProvider.java | 56 +++++++++ .../csrf/session/CsrfSessionPopulator.java | 6 + .../security/csrf/session/package-info.java | 2 +- .../response/CsrfIdTokenLoginHandler.java | 99 ---------------- .../token/response/IdTokenLoginHandler.java | 32 +++++- .../token/cookie/LoginCookieProvider.java | 29 +++++ .../token/cookie/TokenCookieLoginHandler.java | 38 ++++++- 8 files changed, 152 insertions(+), 217 deletions(-) delete mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java delete mode 100644 security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java create mode 100644 security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java deleted file mode 100644 index 551805e4ca..0000000000 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CrsrfTokenCookieLoginHandler.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * 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 - * - * https://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 License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.security.csrf.repository; - -import io.micronaut.context.annotation.Replaces; -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.cookie.Cookie; -import io.micronaut.security.authentication.Authentication; -import io.micronaut.security.config.RedirectConfiguration; -import io.micronaut.security.config.RedirectService; -import io.micronaut.security.config.SecurityConfigurationProperties; -import io.micronaut.security.csrf.CsrfConfiguration; -import io.micronaut.security.csrf.generator.CsrfTokenGenerator; -import io.micronaut.security.errors.PriorToLoginPersistence; -import io.micronaut.security.token.cookie.AccessTokenCookieConfiguration; -import io.micronaut.security.token.cookie.RefreshTokenCookieConfiguration; -import io.micronaut.security.token.cookie.TokenCookieLoginHandler; -import io.micronaut.security.token.generator.AccessRefreshTokenGenerator; -import io.micronaut.security.token.generator.AccessTokenConfiguration; -import jakarta.inject.Singleton; - -import java.util.List; - -/** - * Replaces {@link TokenCookieLoginHandler} to add an extra CSRF Cookie to the response. - * @author Sergio del Amo - * @since 4.11.0 - */ -@Internal -@Requires(classes = { HttpRequest.class }) -@Requires(property = SecurityConfigurationProperties.PREFIX + ".authentication", value = "cookie") -@Replaces(TokenCookieLoginHandler.class) -@Singleton -public class CrsrfTokenCookieLoginHandler extends TokenCookieLoginHandler { - private final CsrfConfiguration csrfConfiguration; - private final CsrfTokenGenerator> csrfTokenGenerator; - - /** - * @param redirectService Redirection Service - * @param redirectConfiguration Redirect configuration - * @param accessTokenCookieConfiguration JWT Access Token Cookie Configuration - * @param refreshTokenCookieConfiguration Refresh Token Cookie Configuration - * @param accessTokenConfiguration JWT Generator Configuration - * @param accessRefreshTokenGenerator Access Refresh Token Generator - * @param priorToLoginPersistence Prior To Login Persistence Mechanism - * @param csrfConfiguration CSRF Configuration - * @param csrfTokenGenerator CSRF Token Generator - */ - public CrsrfTokenCookieLoginHandler(RedirectService redirectService, - RedirectConfiguration redirectConfiguration, - AccessTokenCookieConfiguration accessTokenCookieConfiguration, - RefreshTokenCookieConfiguration refreshTokenCookieConfiguration, - AccessTokenConfiguration accessTokenConfiguration, - AccessRefreshTokenGenerator accessRefreshTokenGenerator, - @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, - CsrfConfiguration csrfConfiguration, - CsrfTokenGenerator> csrfTokenGenerator) { - super(redirectService, redirectConfiguration, accessTokenCookieConfiguration, refreshTokenCookieConfiguration, accessTokenConfiguration, accessRefreshTokenGenerator, priorToLoginPersistence); - this.csrfConfiguration = csrfConfiguration; - this.csrfTokenGenerator = csrfTokenGenerator; - } - - @Override - public List getCookies(Authentication authentication, HttpRequest request) { - List cookies = super.getCookies(authentication, request); - cookies.add(csrfCookie(request)); - return cookies; - } - - @Override - public List getCookies(Authentication authentication, String refreshToken, HttpRequest request) { - List cookies = super.getCookies(authentication, refreshToken, request); - cookies.add(csrfCookie(request)); - return cookies; - } - - @NonNull - private Cookie csrfCookie(@NonNull HttpRequest request) { - String csrfToken = csrfTokenGenerator.generateCsrfToken(request); - return csrfCookie(csrfToken, request); - } - - @NonNull - private Cookie csrfCookie(@NonNull String csrfToken, @NonNull HttpRequest request) { - Cookie cookie = Cookie.of(csrfConfiguration.getCookieName(), csrfToken); - cookie.configure(csrfConfiguration, request.isSecure()); - return cookie; - } -} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java new file mode 100644 index 0000000000..e0e412a7cd --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.repository; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.generator.CsrfTokenGenerator; +import io.micronaut.security.token.cookie.LoginCookieProvider; +import jakarta.inject.Singleton; + +/** + * Provides a CSRF Cookie which can be included in the login response. + * @author Sergio del Amo + * @since 4.11.0 + */ +@Singleton +public class CsrfLoginCookieProvider implements LoginCookieProvider> { + private final CsrfTokenGenerator> csrfTokenGenerator; + private final CsrfConfiguration csrfConfiguration; + + public CsrfLoginCookieProvider(CsrfTokenGenerator> csrfTokenGenerator, + CsrfConfiguration csrfConfiguration) { + this.csrfTokenGenerator = csrfTokenGenerator; + this.csrfConfiguration = csrfConfiguration; + } + + @Override + @NonNull + public Cookie provideCookie(@NonNull HttpRequest request) { + String csrfToken = csrfTokenGenerator.generateCsrfToken(request); + return csrfCookie(csrfToken, request); + } + + @NonNull + private Cookie csrfCookie(@NonNull String csrfToken, @NonNull HttpRequest request) { + Cookie cookie = Cookie.of(csrfConfiguration.getCookieName(), csrfToken); + cookie.configure(csrfConfiguration, request.isSecure()); + return cookie; + } + +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java index d66ca4aca9..f60ea54a8b 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java @@ -22,6 +22,12 @@ import io.micronaut.session.Session; import jakarta.inject.Singleton; +/** + * Populates the session with a CSRF token. + * @author Sergio del Amo + * @since 4.11.0 + * @param Request + */ @Singleton public class CsrfSessionPopulator implements SessionPopulator { private final CsrfConfiguration csrfConfiguration; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java index 951d1d674f..9e40766a58 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java @@ -18,4 +18,4 @@ * @author Sergio del Amo * @since 4.11.0 */ -package io.micronaut.security.csrf.session; \ No newline at end of file +package io.micronaut.security.csrf.session; diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java deleted file mode 100644 index d9596f59ef..0000000000 --- a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/CsrfIdTokenLoginHandler.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * 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 - * - * https://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 License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.security.oauth2.endpoint.token.response; - -import io.micronaut.context.annotation.Replaces; -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.cookie.Cookie; -import io.micronaut.security.authentication.Authentication; -import io.micronaut.security.config.RedirectConfiguration; -import io.micronaut.security.config.RedirectService; -import io.micronaut.security.config.SecurityConfigurationProperties; -import io.micronaut.security.csrf.CsrfConfiguration; -import io.micronaut.security.csrf.generator.CsrfTokenGenerator; -import io.micronaut.security.errors.PriorToLoginPersistence; -import io.micronaut.security.token.cookie.AccessTokenCookieConfiguration; -import io.micronaut.security.token.cookie.CookieLoginHandler; -import jakarta.inject.Singleton; - -import java.util.*; - -/** - * Sets {@link CookieLoginHandler}`s cookie value to the idtoken received from an authentication provider. - * The cookie expiration is set to the expiration of the IDToken exp claim. - * - * @author Sergio del Amo - * @since 2.0.0 - */ -@Requires(property = SecurityConfigurationProperties.PREFIX + ".authentication", value = "idtoken") -@Requires(classes = HttpRequest.class) -@Requires(beans = { CsrfConfiguration.class, CsrfTokenGenerator.class }) -@Replaces(IdTokenLoginHandler.class) -@Singleton -public class CsrfIdTokenLoginHandler extends IdTokenLoginHandler { - private final CsrfTokenGenerator> csrfTokenGenerator; - private final CsrfConfiguration csrfConfiguration; - - /** - * @param accessTokenCookieConfiguration Access token cookie configuration - * @param redirectConfiguration Redirect configuration - * @param redirectService Redirect service - * @param priorToLoginPersistence The prior to login persistence strategy - * @param csrfTokenGenerator CSRF Token Generator - * @param csrfConfiguration CSRF Configuration - */ - public CsrfIdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfiguration, - RedirectConfiguration redirectConfiguration, - RedirectService redirectService, - @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, - CsrfTokenGenerator> csrfTokenGenerator, - CsrfConfiguration csrfConfiguration) { - super(accessTokenCookieConfiguration, redirectConfiguration, redirectService, priorToLoginPersistence); - this.csrfTokenGenerator = csrfTokenGenerator; - this.csrfConfiguration = csrfConfiguration; - } - - @Override - public List getCookies(Authentication authentication, HttpRequest request) { - List cookies = super.getCookies(authentication, request); - cookies.add(csrfCookie(request)); - return cookies; - } - - @Override - public List getCookies(Authentication authentication, String refreshToken, HttpRequest request) { - List cookies = super.getCookies(authentication, refreshToken, request); - cookies.add(csrfCookie(request)); - return cookies; - } - - @NonNull - private Cookie csrfCookie(@NonNull HttpRequest request) { - String csrfToken = csrfTokenGenerator.generateCsrfToken(request); - return csrfCookie(csrfToken, request); - } - - @NonNull - private Cookie csrfCookie(@NonNull String csrfToken, @NonNull HttpRequest request) { - Cookie cookie = Cookie.of(csrfConfiguration.getCookieName(), csrfToken); - cookie.configure(csrfConfiguration, request.isSecure()); - return cookie; - } -} diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java index ef26a28487..31d200deaf 100644 --- a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java +++ b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java @@ -32,17 +32,15 @@ import io.micronaut.security.errors.PriorToLoginPersistence; import io.micronaut.security.token.cookie.AccessTokenCookieConfiguration; import io.micronaut.security.token.cookie.CookieLoginHandler; +import io.micronaut.security.token.cookie.LoginCookieProvider; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.ParseException; import java.time.Duration; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; /** * Sets {@link CookieLoginHandler}`s cookie value to the idtoken received from an authentication provider. @@ -57,6 +55,7 @@ public class IdTokenLoginHandler extends CookieLoginHandler { private static final Logger LOG = LoggerFactory.getLogger(IdTokenLoginHandler.class); + private final List>> loginCookieProviders; /** * @param accessTokenCookieConfiguration Access token cookie configuration @@ -64,11 +63,29 @@ public class IdTokenLoginHandler extends CookieLoginHandler { * @param redirectService Redirect service * @param priorToLoginPersistence The prior to login persistence strategy */ + @Inject public IdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfiguration, RedirectConfiguration redirectConfiguration, RedirectService redirectService, - @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence) { + @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, + List>> loginCookieProviders) { super(accessTokenCookieConfiguration, redirectConfiguration, redirectService, priorToLoginPersistence); + this.loginCookieProviders = loginCookieProviders; + } + + /** + * @param accessTokenCookieConfiguration Access token cookie configuration + * @param redirectConfiguration Redirect configuration + * @param redirectService Redirect service + * @param priorToLoginPersistence The prior to login persistence strategy + * @deprecated Use {@link #IdTokenLoginHandler(AccessTokenCookieConfiguration, RedirectConfiguration, RedirectService, PriorToLoginPersistence, List)} instead. + */ + @Deprecated + public IdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfiguration, + RedirectConfiguration redirectConfiguration, + RedirectService redirectService, + @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence) { + this(accessTokenCookieConfiguration, redirectConfiguration, redirectService, priorToLoginPersistence, Collections.emptyList()); } /** @@ -83,6 +100,9 @@ public List getCookies(Authentication authentication, HttpRequest req jwtCookie.configure(accessTokenCookieConfiguration, request.isSecure()); jwtCookie.maxAge(cookieExpiration(authentication, request)); cookies.add(jwtCookie); + for (LoginCookieProvider> loginCookieProvider : loginCookieProviders) { + cookies.add(loginCookieProvider.provideCookie(request)); + } return cookies; } diff --git a/security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java b/security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java new file mode 100644 index 0000000000..1e981440bb --- /dev/null +++ b/security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.token.cookie; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.cookie.Cookie; + +/** + * @author Sergio del Amo + * @since 4.11.0 + * @param Request + */ +public interface LoginCookieProvider { + @NonNull + Cookie provideCookie(@NonNull T request); +} diff --git a/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java b/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java index 9f54a14dc1..84076dce72 100644 --- a/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java +++ b/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java @@ -31,10 +31,12 @@ import io.micronaut.security.token.generator.AccessRefreshTokenGenerator; import io.micronaut.security.token.generator.AccessTokenConfiguration; import io.micronaut.security.token.render.AccessRefreshToken; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.time.Duration; import java.time.temporal.TemporalAmount; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -50,6 +52,7 @@ public class TokenCookieLoginHandler extends CookieLoginHandler { protected final AccessRefreshTokenGenerator accessRefreshTokenGenerator; protected final RefreshTokenCookieConfiguration refreshTokenCookieConfiguration; protected final AccessTokenConfiguration accessTokenConfiguration; + private final List>> loginCookieProviders; /** * @param redirectService Redirection Service @@ -60,6 +63,33 @@ public class TokenCookieLoginHandler extends CookieLoginHandler { * @param accessRefreshTokenGenerator Access Refresh Token Generator * @param priorToLoginPersistence Prior To Login Persistence Mechanism */ + @Inject + public TokenCookieLoginHandler(RedirectService redirectService, + RedirectConfiguration redirectConfiguration, + AccessTokenCookieConfiguration accessTokenCookieConfiguration, + RefreshTokenCookieConfiguration refreshTokenCookieConfiguration, + AccessTokenConfiguration accessTokenConfiguration, + AccessRefreshTokenGenerator accessRefreshTokenGenerator, + @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence, + List>> loginCookieProviders) { + super(accessTokenCookieConfiguration, redirectConfiguration, redirectService, priorToLoginPersistence); + this.refreshTokenCookieConfiguration = refreshTokenCookieConfiguration; + this.accessTokenConfiguration = accessTokenConfiguration; + this.accessRefreshTokenGenerator = accessRefreshTokenGenerator; + this.loginCookieProviders = loginCookieProviders; + } + + /** + * @param redirectService Redirection Service + * @param redirectConfiguration Redirect configuration + * @param accessTokenCookieConfiguration JWT Access Token Cookie Configuration + * @param refreshTokenCookieConfiguration Refresh Token Cookie Configuration + * @param accessTokenConfiguration JWT Generator Configuration + * @param accessRefreshTokenGenerator Access Refresh Token Generator + * @param priorToLoginPersistence Prior To Login Persistence Mechanism + * @deprecated Use {@link TokenCookieLoginHandler#TokenCookieLoginHandler(RedirectService, RedirectConfiguration, AccessTokenCookieConfiguration, RefreshTokenCookieConfiguration, AccessTokenConfiguration, AccessRefreshTokenGenerator, PriorToLoginPersistence, List)} instead. + */ + @Deprecated public TokenCookieLoginHandler(RedirectService redirectService, RedirectConfiguration redirectConfiguration, AccessTokenCookieConfiguration accessTokenCookieConfiguration, @@ -67,10 +97,7 @@ public TokenCookieLoginHandler(RedirectService redirectService, AccessTokenConfiguration accessTokenConfiguration, AccessRefreshTokenGenerator accessRefreshTokenGenerator, @Nullable PriorToLoginPersistence, MutableHttpResponse> priorToLoginPersistence) { - super(accessTokenCookieConfiguration, redirectConfiguration, redirectService, priorToLoginPersistence); - this.refreshTokenCookieConfiguration = refreshTokenCookieConfiguration; - this.accessTokenConfiguration = accessTokenConfiguration; - this.accessRefreshTokenGenerator = accessRefreshTokenGenerator; + this(redirectService, redirectConfiguration, accessTokenCookieConfiguration, refreshTokenCookieConfiguration, accessTokenConfiguration, accessRefreshTokenGenerator, priorToLoginPersistence, Collections.emptyList()); } @Override @@ -113,6 +140,9 @@ protected List getCookies(AccessRefreshToken accessRefreshToken, HttpReq cookies.add(refreshCookie); } + for (LoginCookieProvider> loginCookieProvider : loginCookieProviders) { + cookies.add(loginCookieProvider.provideCookie(request)); + } return cookies; } } From 0e6bd441e61f84fb2b59dda0497cd9a593db4b35 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 13:29:03 +0200 Subject: [PATCH 039/108] better javadoc --- .../io/micronaut/security/token/cookie/LoginCookieProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java b/security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java index 1e981440bb..1d7947796d 100644 --- a/security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java +++ b/security/src/main/java/io/micronaut/security/token/cookie/LoginCookieProvider.java @@ -19,6 +19,7 @@ import io.micronaut.http.cookie.Cookie; /** + * Provides a Cookie which will be included in the login response. * @author Sergio del Amo * @since 4.11.0 * @param Request From 4280ae938cffb3f6aa2cf610db459eec95258e22 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 13:29:13 +0200 Subject: [PATCH 040/108] remove dependency --- security-oauth2/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/security-oauth2/build.gradle.kts b/security-oauth2/build.gradle.kts index 7d7570afad..2f235e0fa2 100644 --- a/security-oauth2/build.gradle.kts +++ b/security-oauth2/build.gradle.kts @@ -11,7 +11,6 @@ dependencies { testImplementation(mnValidation.micronaut.validation) compileOnly(mn.micronaut.inject.java) compileOnly(projects.micronautSecurityJwt) - compileOnly(projects.micronautSecurityCsrf) compileOnly(mn.micronaut.http.server) api(projects.micronautSecurity) implementation(mn.micronaut.http.client.core) From 0deafa6e3f45e16396ca701ec081ddec4dd9de70 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 13:30:34 +0200 Subject: [PATCH 041/108] remove method --- .../security/csrf/repository/CsrfLoginCookieProvider.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java index e0e412a7cd..6cc1269284 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java @@ -43,14 +43,8 @@ public CsrfLoginCookieProvider(CsrfTokenGenerator> csrfTokenGener @NonNull public Cookie provideCookie(@NonNull HttpRequest request) { String csrfToken = csrfTokenGenerator.generateCsrfToken(request); - return csrfCookie(csrfToken, request); - } - - @NonNull - private Cookie csrfCookie(@NonNull String csrfToken, @NonNull HttpRequest request) { Cookie cookie = Cookie.of(csrfConfiguration.getCookieName(), csrfToken); cookie.configure(csrfConfiguration, request.isSecure()); return cookie; } - } From 9b15e12f7f44e29bd531725553983157129aa5aa Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 13:33:12 +0200 Subject: [PATCH 042/108] add @Configuration --- .../io/micronaut/security/csrf/session/package-info.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java index 9e40766a58..8cc542e7c7 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java @@ -18,4 +18,10 @@ * @author Sergio del Amo * @since 4.11.0 */ +@Requires(classes = Session.class) +@Configuration package io.micronaut.security.csrf.session; + +import io.micronaut.context.annotation.Configuration; +import io.micronaut.context.annotation.Requires; +import io.micronaut.session.Session; \ No newline at end of file From 63a6c6be312947699037d1f363ade76d0770b938 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 22 Oct 2024 16:25:15 +0200 Subject: [PATCH 043/108] Use HttpServerFilter --- security-csrf/build.gradle.kts | 2 +- .../security/csrf/filter/CsrfFilter.java | 90 +++++++++++++------ .../csrf/resolver/FieldCsrfTokenResolver.java | 36 +++----- .../resolver/ReactiveCsrfTokenResolver.java | 61 +++++++++++++ .../ReactiveCsrfTokenResolverAdapter.java | 48 ++++++++++ .../security/csrf/session/package-info.java | 2 +- .../csrf/resolver/CsrfTokenResolverTest.java | 19 ++-- src/main/docs/guide/csrf/csrfFilter.adoc | 2 +- 8 files changed, 201 insertions(+), 59 deletions(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolver.java create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolverAdapter.java diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index 7984988305..b90b5df5f3 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -6,7 +6,7 @@ dependencies { api(projects.micronautSecurity) compileOnly(mn.micronaut.http.server) compileOnly(projects.micronautSecuritySession) - + implementation(mnReactor.micronaut.reactor) testAnnotationProcessor(mn.micronaut.inject.java) testImplementation(mnTest.micronaut.test.junit5) testRuntimeOnly(libs.junit.jupiter.engine) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 28c4a35e75..9fb9f2fcb3 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -22,23 +22,28 @@ import io.micronaut.core.order.Ordered; import io.micronaut.core.util.StringUtils; import io.micronaut.http.*; +import io.micronaut.http.annotation.Filter; import io.micronaut.http.annotation.RequestFilter; -import io.micronaut.http.annotation.ServerFilter; import io.micronaut.http.filter.FilterPatternStyle; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.filter.ServerFilterPhase; import io.micronaut.http.server.exceptions.ExceptionHandler; -import io.micronaut.scheduling.TaskExecutors; -import io.micronaut.scheduling.annotation.ExecuteOn; import io.micronaut.security.authentication.Authentication; import io.micronaut.security.authentication.AuthorizationException; import io.micronaut.security.csrf.resolver.CsrfTokenResolver; +import io.micronaut.security.csrf.resolver.ReactiveCsrfTokenResolver; import io.micronaut.security.csrf.validator.CsrfTokenValidator; import io.micronaut.security.filters.SecurityFilter; +import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.List; import java.util.Optional; +import java.util.function.Supplier; /** * {@link RequestFilter} which validates CSRF tokens and rejects a request if the token is invalid. @@ -50,42 +55,81 @@ @Requires(property = CsrfFilterConfigurationProperties.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Requires(classes = { ExceptionHandler.class, HttpRequest.class }) @Requires(beans = { CsrfTokenValidator.class }) -@ServerFilter(patternStyle = FilterPatternStyle.REGEX, +@Filter(patternStyle = FilterPatternStyle.REGEX, value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") -final class CsrfFilter implements Ordered { +final class CsrfFilter implements Ordered, HttpServerFilter { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); + private final List>> reactiveCsrfTokenResolvers; private final List>> csrfTokenResolvers; private final CsrfTokenValidator> csrfTokenValidator; private final ExceptionHandler> exceptionHandler; private final CsrfFilterConfiguration csrfFilterConfiguration; CsrfFilter(CsrfFilterConfiguration csrfFilterConfiguration, + List>> reactiveCsrfTokenResolvers, List>> csrfTokenResolvers, CsrfTokenValidator> csrfTokenValidator, ExceptionHandler> exceptionHandler) { this.csrfTokenResolvers = csrfTokenResolvers; + this.reactiveCsrfTokenResolvers = reactiveCsrfTokenResolvers.isEmpty() + ? reactiveCsrfTokenResolvers + : ReactiveCsrfTokenResolver.of(csrfTokenResolvers, reactiveCsrfTokenResolvers); this.csrfTokenValidator = csrfTokenValidator; this.exceptionHandler = exceptionHandler; this.csrfFilterConfiguration = csrfFilterConfiguration; } - @ExecuteOn(TaskExecutors.BLOCKING) - @RequestFilter - @Nullable - public HttpResponse csrfFilter(@NonNull HttpRequest request) { + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + Supplier>> proceedRequest = () -> chain.proceed(request); + Supplier>> denyRequest = () -> Mono.just(unauthorized(request)); if (!shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(request)) { - return null; // continue normally + return proceedRequest.get(); } if (!shouldTheFilterProcessTheRequestAccordingToTheContentType(request)) { - return null; // continue normally + return proceedRequest.get(); } - if (!validateCsrfToken(request)) { + return reactiveCsrfTokenResolvers.isEmpty() + ? imperativeFilter(request, proceedRequest, denyRequest) + : reactiveFilter(request, proceedRequest, denyRequest); + } + + private Publisher> reactiveFilter(HttpRequest request, + Supplier>> proceedRequest, + Supplier>> denyRequest) { + return Flux.fromIterable(this.reactiveCsrfTokenResolvers) + .concatMap(resolver -> Mono.from(resolver.resolveToken(request)) + .filter(csrfToken -> { + LOG.debug("CSRF Token resolved"); + if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { + return true; + } else { + LOG.debug("CSRF Token validation failed"); + return false; + } + })) + .next() + .flatMap(validToken -> Mono.from(proceedRequest.get())) + .switchIfEmpty(Mono.defer(() -> { + LOG.debug("Request rejected by the CsrfFilter"); + return Mono.from(denyRequest.get()); + })); + } + private Publisher> imperativeFilter(HttpRequest request, + Supplier>> proceedRequest, + Supplier>> denyRequest) { + String csrfToken = resolveCsrfToken(request); + if (csrfToken == null) { if (LOG.isDebugEnabled()) { - LOG.debug("Request rejected by the {} because the CSRF Token validation failed", this.getClass().getSimpleName()); + LOG.debug("Request rejected by the {} because no CSRF Token found", this.getClass().getSimpleName()); } - return unauthorized(request); + return denyRequest.get(); } - return null; // continue normally + if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { + return proceedRequest.get(); + } + LOG.debug("Request rejected by the CSRF Filter because the CSRF Token validation failed"); + return denyRequest.get(); } private boolean shouldTheFilterProcessTheRequestAccordingToTheContentType(@NonNull HttpRequest request) { @@ -118,31 +162,23 @@ private boolean shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(@NonNul @Nullable private String resolveCsrfToken(@NonNull HttpRequest request) { - String csrfToken = null; for (CsrfTokenResolver> tokenResolver : csrfTokenResolvers) { Optional tokenOptional = tokenResolver.resolveToken(request); if (tokenOptional.isPresent()) { if (LOG.isTraceEnabled()) { LOG.trace("CSRF token resolved via {}", tokenResolver.getClass().getSimpleName()); } - csrfToken = tokenOptional.get(); - break; + return tokenOptional.get(); } } - return csrfToken; - } - - private boolean validateCsrfToken(@NonNull HttpRequest request) { - String csrfToken = resolveCsrfToken(request); - if (csrfToken == null) { + if (LOG.isDebugEnabled()) { LOG.trace("No CSRF token found in request"); - return false; } - return csrfTokenValidator.validateCsrfToken(request, csrfToken); + return null; } @NonNull - private HttpResponse unauthorized(@NonNull HttpRequest request) { + private MutableHttpResponse unauthorized(@NonNull HttpRequest request) { Authentication authentication = request.getAttribute(SecurityFilter.AUTHENTICATION, Authentication.class) .orElse(null); return exceptionHandler.handle(request, diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 8b1c3fd900..171862d945 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.resolver; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.ServerHttpRequest; @@ -23,10 +24,8 @@ import io.micronaut.http.body.CloseableByteBody; import io.micronaut.security.csrf.CsrfConfiguration; import jakarta.inject.Singleton; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.Optional; @@ -38,7 +37,7 @@ @Requires(classes = HttpRequest.class) @Requires(property = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton -class FieldCsrfTokenResolver implements CsrfTokenResolver> { +class FieldCsrfTokenResolver implements ReactiveCsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; FieldCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { @@ -46,34 +45,25 @@ class FieldCsrfTokenResolver implements CsrfTokenResolver> { } @Override - public Optional resolveToken(HttpRequest request) { + @Singleton + public Publisher resolveToken(HttpRequest request) { if (request instanceof ServerHttpRequest serverHttpRequest) { return resolveToken(serverHttpRequest); } - return Optional.empty(); + return Publishers.empty(); } - private Optional resolveToken(ServerHttpRequest request) { + private Publisher resolveToken(ServerHttpRequest request) { try (CloseableByteBody ourCopy = request.byteBody() .split(ByteBody.SplitBackpressureMode.SLOWEST) .allowDiscard()) { - try (InputStream inputStream = ourCopy.toInputStream()) { - String str = ofInputStream(inputStream); - return extractCsrfTokenFromAFormUrlEncodedString(str); - } catch (IOException e) { - return Optional.empty(); - } - } - } - - private String ofInputStream(InputStream inputStream) throws IOException { - final ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - for (int length; (length = inputStream.read(buffer)) != -1; ) { - result.write(buffer, 0, length); + return Mono.from(ourCopy.toByteArrayPublisher()) + .map(byteArr -> new String(byteArr, StandardCharsets.UTF_8)) + .flatMap(str -> extractCsrfTokenFromAFormUrlEncodedString(str) + .map(Mono::just) + .orElseGet(Mono::empty)); } - return result.toString(StandardCharsets.UTF_8); } private Optional extractCsrfTokenFromAFormUrlEncodedString(String body) { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolver.java new file mode 100644 index 0000000000..fd3e1e7817 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolver.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.async.annotation.SingleResult; +import io.micronaut.core.order.OrderUtil; +import io.micronaut.core.order.Ordered; +import org.reactivestreams.Publisher; + +import java.util.ArrayList; +import java.util.List; + +/** + * Attempts to resolve a CSRF token from the provided request. + * {@link ReactiveCsrfTokenResolver} is an {@link Ordered} api. Override the {@link #getOrder()} method to provide a custom order. + * + * @author Sergio del Amo + * @since 1.1.0 + * @param request + */ +public interface ReactiveCsrfTokenResolver extends Ordered { + + /** + * + * @param request The Request. Maybe an HTTP Request. + * @return A CSRF token or an empty Optional if the token cannot be resolved. + */ + @SingleResult + @NonNull + Publisher resolveToken(T request); + + /** + * + * @param resolvers Imperative CSRF Token Resolvers + * @param reactiveCsrfTokenResolvers Reactive CSRF Token Resolvers + * @return Returns a List of {@link ReactiveCsrfTokenResolver} instances containing every reactive resolver plus the imperative resolvers adapted to imperative. + * @param + */ + static List> of(List> resolvers, + List> reactiveCsrfTokenResolvers) { + List> result = new ArrayList<>(); + result.addAll(reactiveCsrfTokenResolvers); + result.addAll(resolvers.stream().map(ReactiveCsrfTokenResolverAdapter::new).toList()); + OrderUtil.sort(result); + return result; + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolverAdapter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolverAdapter.java new file mode 100644 index 0000000000..fbef03675a --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolverAdapter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.resolver; + +import io.micronaut.core.async.publisher.Publishers; +import org.reactivestreams.Publisher; + +/** + * Adapter from {@link CsrfTokenResolver} to {@link ReactiveCsrfTokenResolver}. + * @param Request + */ +public class ReactiveCsrfTokenResolverAdapter implements ReactiveCsrfTokenResolver { + + private final CsrfTokenResolver csrfTokenResolver; + + /** + * + * @param csrfTokenResolver CSRF Token resolver + */ + public ReactiveCsrfTokenResolverAdapter(CsrfTokenResolver csrfTokenResolver) { + this.csrfTokenResolver = csrfTokenResolver; + } + + @Override + public Publisher resolveToken(T request) { + return csrfTokenResolver.resolveToken(request) + .map(Publishers::just) + .orElseGet(Publishers::empty); + } + + @Override + public int getOrder() { + return csrfTokenResolver.getOrder(); + } +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java index 8cc542e7c7..771a152c5b 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/package-info.java @@ -24,4 +24,4 @@ import io.micronaut.context.annotation.Configuration; import io.micronaut.context.annotation.Requires; -import io.micronaut.session.Session; \ No newline at end of file +import io.micronaut.session.Session; diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java index 689c9bed3c..e78272af0e 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java @@ -1,6 +1,9 @@ package io.micronaut.security.csrf.resolver; import io.micronaut.context.BeanContext; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; @@ -15,15 +18,19 @@ class CsrfTokenResolverTest { @Inject - BeanContext beanContext; + List>> csrfTokenResolvers; + + @Inject + List>> reactiveCsrfTokenResolvers; @Test void csrfTokenResolversOrder() { - Collection csrfTokenResolverCollection = beanContext.getBeansOfType(CsrfTokenResolver.class); - List csrfTokenResolverList = new ArrayList<>(csrfTokenResolverCollection); - assertEquals(2, csrfTokenResolverList.size()); + assertEquals(1, csrfTokenResolvers.size()); + assertEquals(1, reactiveCsrfTokenResolvers.size()); + List>> all = ReactiveCsrfTokenResolver.of(csrfTokenResolvers, reactiveCsrfTokenResolvers); + assertEquals(2, all.size()); // It is important for HTTP Header to be the first one. FieldCsrfTokenResolver requires Netty. Moreover, it is more secure to supply the CSRF token via custom HTTP Header instead of a form field as it is more difficult to exploit. - assertInstanceOf(HttpHeaderCsrfTokenResolver.class, csrfTokenResolverList.get(0)); - assertInstanceOf(FieldCsrfTokenResolver.class, csrfTokenResolverList.get(1)); + assertInstanceOf(ReactiveCsrfTokenResolverAdapter.class, all.get(0)); // with HttpHeaderCsrfTokenResolver inside + assertInstanceOf(FieldCsrfTokenResolver.class, all.get(1)); } } \ No newline at end of file diff --git a/src/main/docs/guide/csrf/csrfFilter.adoc b/src/main/docs/guide/csrf/csrfFilter.adoc index 445b325bfb..201b3c4fd0 100644 --- a/src/main/docs/guide/csrf/csrfFilter.adoc +++ b/src/main/docs/guide/csrf/csrfFilter.adoc @@ -1,5 +1,5 @@ The core of Micronaut Security CSRF implementation is `io.micronaut.security.csrf.filter.CsrfFilter`. -A https://docs.micronaut.io/latest/guide/#filtermethods[Request Filter Method] which attempts to resolve a CSRF Token with +A https://docs.micronaut.io/latest/guide/#filters[Server Filter] which attempts to resolve a CSRF Token with every bean of type api:security.csrf.resolvers.CsrfTokenResolver[] and validates it with beans of type api:security.csrf.validator.CsrfTokenValidator[]. The following configuration options are available for the CSRF Filter: From 66e88735167d245f91e13934c4d9cdf6afe273de Mon Sep 17 00:00:00 2001 From: yawkat Date: Tue, 22 Oct 2024 20:19:15 +0200 Subject: [PATCH 044/108] fix --- .../csrf/resolver/FieldCsrfTokenResolver.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 171862d945..b15c2a843a 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -21,12 +21,11 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.ServerHttpRequest; import io.micronaut.http.body.ByteBody; -import io.micronaut.http.body.CloseableByteBody; import io.micronaut.security.csrf.CsrfConfiguration; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import java.nio.charset.StandardCharsets; + import java.util.Optional; /** @@ -54,16 +53,10 @@ public Publisher resolveToken(HttpRequest request) { } private Publisher resolveToken(ServerHttpRequest request) { - try (CloseableByteBody ourCopy = - request.byteBody() - .split(ByteBody.SplitBackpressureMode.SLOWEST) - .allowDiscard()) { - return Mono.from(ourCopy.toByteArrayPublisher()) - .map(byteArr -> new String(byteArr, StandardCharsets.UTF_8)) - .flatMap(str -> extractCsrfTokenFromAFormUrlEncodedString(str) - .map(Mono::just) - .orElseGet(Mono::empty)); - } + return Mono.fromFuture(request.byteBody().split(ByteBody.SplitBackpressureMode.FASTEST).buffer()) + .map(bb -> bb.toString(request.getCharacterEncoding())) + .map(this::extractCsrfTokenFromAFormUrlEncodedString) + .flatMap(opt -> opt.map(Mono::just).orElseGet(Mono::empty)); } private Optional extractCsrfTokenFromAFormUrlEncodedString(String body) { From e22554d047e2a0b92d1ae2cdc833c8d07cb22081 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 23 Oct 2024 09:48:31 +0200 Subject: [PATCH 045/108] add @Requires --- .../security/csrf/repository/CsrfLoginCookieProvider.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java index 6cc1269284..e90fb762ce 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.csrf.repository; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequest; import io.micronaut.http.cookie.Cookie; @@ -28,6 +29,7 @@ * @author Sergio del Amo * @since 4.11.0 */ +@Requires(classes = HttpRequest.class) @Singleton public class CsrfLoginCookieProvider implements LoginCookieProvider> { private final CsrfTokenGenerator> csrfTokenGenerator; From 00df7d64cc7c9a8583c6d091d3cb785634922234 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 23 Oct 2024 09:53:04 +0200 Subject: [PATCH 046/108] ServerFilter/RequestFilter not HttpServerFilter --- .../security/csrf/filter/CsrfFilter.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 9fb9f2fcb3..8944c46d5e 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -22,11 +22,9 @@ import io.micronaut.core.order.Ordered; import io.micronaut.core.util.StringUtils; import io.micronaut.http.*; -import io.micronaut.http.annotation.Filter; import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.annotation.ServerFilter; import io.micronaut.http.filter.FilterPatternStyle; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.filter.ServerFilterPhase; import io.micronaut.http.server.exceptions.ExceptionHandler; import io.micronaut.security.authentication.Authentication; @@ -55,9 +53,9 @@ @Requires(property = CsrfFilterConfigurationProperties.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Requires(classes = { ExceptionHandler.class, HttpRequest.class }) @Requires(beans = { CsrfTokenValidator.class }) -@Filter(patternStyle = FilterPatternStyle.REGEX, +@ServerFilter(patternStyle = FilterPatternStyle.REGEX, value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") -final class CsrfFilter implements Ordered, HttpServerFilter { +final class CsrfFilter implements Ordered { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); private final List>> reactiveCsrfTokenResolvers; private final List>> csrfTokenResolvers; @@ -79,10 +77,11 @@ final class CsrfFilter implements Ordered, HttpServerFilter { this.csrfFilterConfiguration = csrfFilterConfiguration; } - @Override - public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - Supplier>> proceedRequest = () -> chain.proceed(request); - Supplier>> denyRequest = () -> Mono.just(unauthorized(request)); + @RequestFilter + @Nullable + public Publisher>> csrfFilter(@NonNull HttpRequest request) { + Supplier>>> proceedRequest = () -> Mono.just(Optional.empty()); + Supplier>>> denyRequest = () -> Mono.just(Optional.of(unauthorized(request))); if (!shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(request)) { return proceedRequest.get(); } @@ -94,9 +93,9 @@ public Publisher> doFilter(HttpRequest request, Server : reactiveFilter(request, proceedRequest, denyRequest); } - private Publisher> reactiveFilter(HttpRequest request, - Supplier>> proceedRequest, - Supplier>> denyRequest) { + private Publisher>> reactiveFilter(HttpRequest request, + Supplier>>> proceedRequest, + Supplier>>> denyRequest) { return Flux.fromIterable(this.reactiveCsrfTokenResolvers) .concatMap(resolver -> Mono.from(resolver.resolveToken(request)) .filter(csrfToken -> { @@ -115,9 +114,9 @@ private Publisher> reactiveFilter(HttpRequest request, return Mono.from(denyRequest.get()); })); } - private Publisher> imperativeFilter(HttpRequest request, - Supplier>> proceedRequest, - Supplier>> denyRequest) { + private Publisher>> imperativeFilter(HttpRequest request, + Supplier>>> proceedRequest, + Supplier>>> denyRequest) { String csrfToken = resolveCsrfToken(request); if (csrfToken == null) { if (LOG.isDebugEnabled()) { From 50998738265a2159b2c38c7e4f161ca98a967497 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 23 Oct 2024 10:08:06 +0200 Subject: [PATCH 047/108] =?UTF-8?q?simplify=20with=20.flatMap(=20=E2=80=A6?= =?UTF-8?q?strream())?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../csrf/repository/CompositeCsrfTokenRepository.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java index 69790dd0e1..7195f519c2 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java @@ -42,9 +42,7 @@ public CompositeCsrfTokenRepository(List> repositories) { @Override public Optional findCsrfToken(T request) { return repositories.stream() - .map(r -> r.findCsrfToken(request)) - .filter(Optional::isPresent) - .map(Optional::get) + .flatMap(r -> r.findCsrfToken(request).stream()) .findFirst(); } } From 77a157906008ec100b846761b90b94e7186368ff Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 23 Oct 2024 10:08:57 +0200 Subject: [PATCH 048/108] =?UTF-8?q?simplify=20with=20.flatMap(=20=E2=80=A6?= =?UTF-8?q?strream())?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/session/CompositeSessionIdResolver.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java index d3d50b521d..b4aa6a9cf0 100644 --- a/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java +++ b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java @@ -44,9 +44,7 @@ public CompositeSessionIdResolver(List> sessionIdResolvers) @NonNull public Optional findSessionId(@NonNull T request) { return sessionIdResolvers.stream() - .map(sessionIdResolver -> sessionIdResolver.findSessionId(request)) - .filter(Optional::isPresent) - .map(Optional::get) + .flatMap(sessionIdResolver -> sessionIdResolver.findSessionId(request).stream()) .findFirst(); } } From 1effdbf461849e9381a73318c54ca7eea99e3330 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 23 Oct 2024 10:15:18 +0200 Subject: [PATCH 049/108] remove suppliers --- .../security/csrf/filter/CsrfFilter.java | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 8944c46d5e..8a3be87bad 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -41,7 +41,6 @@ import java.util.List; import java.util.Optional; -import java.util.function.Supplier; /** * {@link RequestFilter} which validates CSRF tokens and rejects a request if the token is invalid. @@ -80,22 +79,22 @@ final class CsrfFilter implements Ordered { @RequestFilter @Nullable public Publisher>> csrfFilter(@NonNull HttpRequest request) { - Supplier>>> proceedRequest = () -> Mono.just(Optional.empty()); - Supplier>>> denyRequest = () -> Mono.just(Optional.of(unauthorized(request))); if (!shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(request)) { - return proceedRequest.get(); + return proceedRequest(); } if (!shouldTheFilterProcessTheRequestAccordingToTheContentType(request)) { - return proceedRequest.get(); + return proceedRequest(); } return reactiveCsrfTokenResolvers.isEmpty() - ? imperativeFilter(request, proceedRequest, denyRequest) - : reactiveFilter(request, proceedRequest, denyRequest); + ? imperativeFilter(request) + : reactiveFilter(request); } - private Publisher>> reactiveFilter(HttpRequest request, - Supplier>>> proceedRequest, - Supplier>>> denyRequest) { + private static Publisher>> proceedRequest() { + return Mono.just(Optional.empty()); + } + + private Publisher>> reactiveFilter(HttpRequest request) { return Flux.fromIterable(this.reactiveCsrfTokenResolvers) .concatMap(resolver -> Mono.from(resolver.resolveToken(request)) .filter(csrfToken -> { @@ -108,27 +107,26 @@ private Publisher>> reactiveFilter(HttpRequest Mono.from(proceedRequest.get())) + .flatMap(validToken -> Mono.from(proceedRequest())) .switchIfEmpty(Mono.defer(() -> { LOG.debug("Request rejected by the CsrfFilter"); - return Mono.from(denyRequest.get()); + return Mono.from(reactiveUnauthorized(request)); })); } - private Publisher>> imperativeFilter(HttpRequest request, - Supplier>>> proceedRequest, - Supplier>>> denyRequest) { + + private Publisher>> imperativeFilter(HttpRequest request) { String csrfToken = resolveCsrfToken(request); if (csrfToken == null) { if (LOG.isDebugEnabled()) { LOG.debug("Request rejected by the {} because no CSRF Token found", this.getClass().getSimpleName()); } - return denyRequest.get(); + return reactiveUnauthorized(request); } if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { - return proceedRequest.get(); + return proceedRequest(); } LOG.debug("Request rejected by the CSRF Filter because the CSRF Token validation failed"); - return denyRequest.get(); + return reactiveUnauthorized(request); } private boolean shouldTheFilterProcessTheRequestAccordingToTheContentType(@NonNull HttpRequest request) { @@ -176,6 +174,11 @@ private String resolveCsrfToken(@NonNull HttpRequest request) { return null; } + @NonNull + private Publisher>> reactiveUnauthorized(@NonNull HttpRequest request) { + return Mono.just(Optional.of(unauthorized(request))); + } + @NonNull private MutableHttpResponse unauthorized(@NonNull HttpRequest request) { Authentication authentication = request.getAttribute(SecurityFilter.AUTHENTICATION, Authentication.class) From 0fa439125b76dadad0c2adc113d5dcc722cac937 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 23 Oct 2024 10:16:45 +0200 Subject: [PATCH 050/108] make it final --- .../io/micronaut/security/csrf/CsrfConfigurationProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index 2a9ae6f8a3..d28b42ea08 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -29,7 +29,7 @@ @Internal @ConfigurationProperties(CsrfConfigurationProperties.PREFIX) -class CsrfConfigurationProperties implements CsrfConfiguration { +final class CsrfConfigurationProperties implements CsrfConfiguration { public static final String PREFIX = SecurityConfigurationProperties.PREFIX + ".csrf"; /** From 950ddff2ea96848b735266e153a7692364aaec44 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:14:15 +0200 Subject: [PATCH 051/108] Update src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc Co-authored-by: Jonas Konrad --- .../guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc b/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc index c09d7e30fe..385dacf171 100644 --- a/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc +++ b/src/main/docs/guide/csrf/csrfMitigations/doubleSubmitCookiePattern.adoc @@ -18,4 +18,4 @@ amount=100&toUser=intended&csrfToken=o24b65486f506e2cd4403caf0d640024 When you use Micronaut Security Authentication <>, or <> a CSRF Token is saved in a Cookie upon login. -You can <>. For example, by default the cookie name uses a `__Host-` https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes[Cookie prefix], can extend https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#using-cookies-with-host-prefixes-to-identify-origins[security protections against CSF Attacks]. \ No newline at end of file +You can <>. For example, by default the cookie name uses a `__Host-` https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes[Cookie prefix], can extend https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#using-cookies-with-host-prefixes-to-identify-origins[security protections against CSRF Attacks]. \ No newline at end of file From 629b8fee4acdef3db4ac2e8694b4d74d83825a9d Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:27:10 +0200 Subject: [PATCH 052/108] remove start imports --- .../endpoint/token/response/IdTokenLoginHandler.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java index 31d200deaf..701a7f5ffa 100644 --- a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java +++ b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java @@ -40,7 +40,12 @@ import java.text.ParseException; import java.time.Duration; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; /** * Sets {@link CookieLoginHandler}`s cookie value to the idtoken received from an authentication provider. From 4506c4748a809eba61ecb2ab9593f2fe702773dd Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:29:35 +0200 Subject: [PATCH 053/108] Use Mono in private methods --- .../micronaut/security/csrf/filter/CsrfFilter.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 8a3be87bad..3c8d32a914 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -90,11 +90,11 @@ public Publisher>> csrfFilter(@NonNull HttpReque : reactiveFilter(request); } - private static Publisher>> proceedRequest() { + private static Mono>> proceedRequest() { return Mono.just(Optional.empty()); } - private Publisher>> reactiveFilter(HttpRequest request) { + private Mono>> reactiveFilter(HttpRequest request) { return Flux.fromIterable(this.reactiveCsrfTokenResolvers) .concatMap(resolver -> Mono.from(resolver.resolveToken(request)) .filter(csrfToken -> { @@ -107,14 +107,14 @@ private Publisher>> reactiveFilter(HttpRequest Mono.from(proceedRequest())) + .flatMap(validToken -> proceedRequest()) .switchIfEmpty(Mono.defer(() -> { LOG.debug("Request rejected by the CsrfFilter"); - return Mono.from(reactiveUnauthorized(request)); + return reactiveUnauthorized(request); })); } - private Publisher>> imperativeFilter(HttpRequest request) { + private Mono>> imperativeFilter(HttpRequest request) { String csrfToken = resolveCsrfToken(request); if (csrfToken == null) { if (LOG.isDebugEnabled()) { @@ -175,7 +175,7 @@ private String resolveCsrfToken(@NonNull HttpRequest request) { } @NonNull - private Publisher>> reactiveUnauthorized(@NonNull HttpRequest request) { + private Mono>> reactiveUnauthorized(@NonNull HttpRequest request) { return Mono.just(Optional.of(unauthorized(request))); } From 7d306bea5051bf76848d97147a9fa0f98828f9a0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:30:17 +0200 Subject: [PATCH 054/108] javadoc: add missing @param --- .../oauth2/endpoint/token/response/IdTokenLoginHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java index 701a7f5ffa..87b01bcac1 100644 --- a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java +++ b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java @@ -67,6 +67,7 @@ public class IdTokenLoginHandler extends CookieLoginHandler { * @param redirectConfiguration Redirect configuration * @param redirectService Redirect service * @param priorToLoginPersistence The prior to login persistence strategy + * @param loginCookieProviders List of beans of type {@link LoginCookieProvider} */ @Inject public IdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfiguration, From b18f32f1b48a9c8788b224e7804b8d0f50655d90 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:37:48 +0200 Subject: [PATCH 055/108] remove NOTE about netty compatibility --- .../guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc b/src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc index 41c25e38ec..c15d7e809e 100644 --- a/src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc +++ b/src/main/docs/guide/csrf/csrfTokenResolvers/fieldCsrfTokenResolver.adoc @@ -11,5 +11,3 @@ amount=100&toUser=intended&csrfToken=o24b65486f506e2cd4403caf0d640024 ---- You can disable it by setting `micronaut.security.csrf.token-resolvers.field.enabled=false` - -NOTE: `FieldCsrfTokenResolver` only works for Netty runtime. \ No newline at end of file From 1f92c7c3c48866d517e91963e67cec6a054ae8f9 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:41:25 +0200 Subject: [PATCH 056/108] fix links to CSRF APIs --- src/main/docs/guide/csrf/csrfApis.adoc | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/docs/guide/csrf/csrfApis.adoc b/src/main/docs/guide/csrf/csrfApis.adoc index 91881a4f59..533b981edb 100644 --- a/src/main/docs/guide/csrf/csrfApis.adoc +++ b/src/main/docs/guide/csrf/csrfApis.adoc @@ -1,9 +1,8 @@ The main APIs for CSRF protection are: -* api:security.csrf.CsrConfiguration[] -* api:security.csrf.filter.CsrFilter[] -* api:security.csrf.filter.CsrFilterConfiguration[] -* api:security.csrf.resolvers.CsrfTokenResolver[] +* api:security.csrf.CsrfConfiguration[] +* api:security.csrf.filter.CsrfFilterConfiguration[] +* api:security.csrf.resolver.CsrfTokenResolver[] * api:security.csrf.generator.CsrfTokenGenerator[] -* api:security.csrf.CsrTokenValidator[] -* api:security.csrf.CsrfTokenRepository[] +* api:security.csrf.validator.CsrfTokenValidator[] +* api:security.csrf.repository.CsrfTokenRepository[] From 36f45cdf124125ad907cd4872c531600ed96f4da Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:47:39 +0200 Subject: [PATCH 057/108] extract a package private method --- .../security/csrf/generator/DefaultCsrfTokenGenerator.java | 6 +++++- .../CsrfDoubleSubmitCookiePatternTest.java | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) rename security-csrf/src/test/java/io/micronaut/security/csrf/{repository => generator}/CsrfDoubleSubmitCookiePatternTest.java (96%) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index 42c10f1c84..daa9da7364 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -84,7 +84,7 @@ public String hmac(@NonNull T request, String randomValue) { String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session // Create the CSRF Token - String message = sessionID + SESSION_RANDOM_SEPARATOR + randomValue; // HMAC message payload + String message = hmacMessagePayload(sessionID, randomValue); try { return secret != null ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash @@ -95,4 +95,8 @@ public String hmac(@NonNull T request, String randomValue) { throw new ConfigurationException("Invalid algorithm for signing the CSRF token"); } } + + static String hmacMessagePayload(String sessionId, String randomValue) { + return sessionId + SESSION_RANDOM_SEPARATOR + randomValue; + } } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfDoubleSubmitCookiePatternTest.java similarity index 96% rename from security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java rename to security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfDoubleSubmitCookiePatternTest.java index 3595fe510d..ba4fa2c406 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/repository/CsrfDoubleSubmitCookiePatternTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfDoubleSubmitCookiePatternTest.java @@ -1,4 +1,4 @@ -package io.micronaut.security.csrf.repository; +package io.micronaut.security.csrf.generator; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; @@ -13,6 +13,7 @@ import io.micronaut.http.cookie.Cookie; import io.micronaut.security.annotation.Secured; import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.repository.CsrfTokenRepository; import io.micronaut.security.rules.SecurityRule; import io.micronaut.security.session.SessionIdResolver; import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider; @@ -30,6 +31,7 @@ import java.util.Map; import java.util.Optional; +import static io.micronaut.security.csrf.generator.DefaultCsrfTokenGenerator.hmacMessagePayload; import static org.junit.jupiter.api.Assertions.*; @Property(name = "micronaut.security.authentication", value = "cookie") @@ -76,7 +78,7 @@ void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, PasswordChangeForm body = new PasswordChangeForm("sherlock", "evil", csrfTokenCalculatedWithoutSessionId); assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, body, csrfTokenCalculatedWithoutSessionId); - String message = FIX_SESSION_ID + "!" + randomValue; + String message = hmacMessagePayload(FIX_SESSION_ID, randomValue); hmac = HMacUtils.base64EncodedHmacSha256(message, csrfConfiguration.getSecretKey()); csrfToken = hmac + "." + randomValue; assertOk(client, cookieJwt.getValue(), csrfTokenCookieName, csrfToken); From b23375524f55750982f2dc3145f926caec86822d Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 11:59:50 +0200 Subject: [PATCH 058/108] ensure separator is not present as substring --- .../security/csrf/generator/DefaultCsrfTokenGenerator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index daa9da7364..1f8de3f560 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -97,6 +97,8 @@ public String hmac(@NonNull T request, String randomValue) { } static String hmacMessagePayload(String sessionId, String randomValue) { - return sessionId + SESSION_RANDOM_SEPARATOR + randomValue; + return sessionId.replace(SESSION_RANDOM_SEPARATOR, "") + // this ensures session ID, does not have the separator as a substring. + SESSION_RANDOM_SEPARATOR + + randomValue; // randomValue cannot have the separator, as ! is not a base64 character. } } From 07a755b4f1c445460a5c80d652134ae2c1d44f76 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 12:09:40 +0200 Subject: [PATCH 059/108] extract CsrfHmacTokenGenerator API --- .../generator/CsrfHmacTokenGenerator.java | 40 +++++++++++++++++++ .../generator/DefaultCsrfTokenGenerator.java | 6 +-- .../RepositoryCsrfTokenValidator.java | 10 ++--- src/main/docs/guide/csrf/csrfApis.adoc | 1 + 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java new file mode 100644 index 0000000000..eeb9f33627 --- /dev/null +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2024 original authors + * + * 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 + * + * https://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 License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.security.csrf.generator; + +import io.micronaut.core.annotation.NonNull; + +/** + * CSRF token Generation with HMAC. + * @author Sergio del Amo + * @since 4.11.0 + * @param request + */ +public interface CsrfHmacTokenGenerator extends CsrfTokenGenerator { + /** + * Dot is used as separator between the HMAC and the random value. As the random value and hmac are base64 encoded, they will not contain a dot. + */ + String HMAC_RANDOM_SEPARATOR = "."; + + /** + * Generates an HMAC. + * @param request Request + * @param randomValue Cryptographic random value + * @return HMAC hash + */ + @NonNull + String hmac(@NonNull T request, String randomValue); +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index 1f8de3f560..b4f394f7ef 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -38,11 +38,10 @@ */ @Requires(classes = CookieConfiguration.class) @Singleton -public final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator { +final class DefaultCsrfTokenGenerator implements CsrfHmacTokenGenerator { /** * hmac random value separator. */ - public static final String HMAC_RANDOM_SEPARATOR = "."; private static final String SESSION_RANDOM_SEPARATOR = "!"; private final SecureRandom secureRandom = new SecureRandom(); private final CsrfConfiguration csrfConfiguration; @@ -53,7 +52,7 @@ public final class DefaultCsrfTokenGenerator implements CsrfTokenGenerator * @param csrfConfiguration CSRF Configuration * @param sessionIdResolver SessionID Resolver */ - public DefaultCsrfTokenGenerator(CsrfConfiguration csrfConfiguration, + DefaultCsrfTokenGenerator(CsrfConfiguration csrfConfiguration, SessionIdResolver sessionIdResolver) { this.csrfConfiguration = csrfConfiguration; this.sessionIdResolver = sessionIdResolver; @@ -77,6 +76,7 @@ public String generateCsrfToken(@NonNull T request) { * @param randomValue Cryptographic random value * @return HMAC hash */ + @Override @NonNull public String hmac(@NonNull T request, String randomValue) { // Gather the values diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java index e01fc0d9b0..3407a7d481 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java @@ -16,7 +16,7 @@ package io.micronaut.security.csrf.validator; import io.micronaut.context.annotation.Requires; -import io.micronaut.security.csrf.generator.DefaultCsrfTokenGenerator; +import io.micronaut.security.csrf.generator.CsrfHmacTokenGenerator; import io.micronaut.security.csrf.repository.CsrfTokenRepository; import jakarta.inject.Singleton; import org.slf4j.Logger; @@ -32,12 +32,12 @@ * @since 4.11.0 * @author Sergio del Amo */ -@Requires(beans = { CsrfTokenRepository.class, DefaultCsrfTokenGenerator.class}) +@Requires(beans = { CsrfTokenRepository.class, CsrfHmacTokenGenerator.class}) @Singleton public class RepositoryCsrfTokenValidator implements CsrfTokenValidator { private static final Logger LOG = LoggerFactory.getLogger(RepositoryCsrfTokenValidator.class); private final List> repositories; - private final DefaultCsrfTokenGenerator defaultCsrfTokenGenerator; + private final CsrfHmacTokenGenerator defaultCsrfTokenGenerator; /** * @@ -45,7 +45,7 @@ public class RepositoryCsrfTokenValidator implements CsrfTokenValidator { * @param defaultCsrfTokenGenerator Default CSRF Token Generator */ public RepositoryCsrfTokenValidator(List> repositories, - DefaultCsrfTokenGenerator defaultCsrfTokenGenerator) { + CsrfHmacTokenGenerator defaultCsrfTokenGenerator) { this.repositories = repositories; this.defaultCsrfTokenGenerator = defaultCsrfTokenGenerator; } @@ -65,7 +65,7 @@ public boolean validateCsrfToken(T request, String csrfTokenInRequest) { } private boolean validateHmac(T request, String csrfTokenInRequest) { - String[] arr = csrfTokenInRequest.split("\\" + DefaultCsrfTokenGenerator.HMAC_RANDOM_SEPARATOR); + String[] arr = csrfTokenInRequest.split("\\" + CsrfHmacTokenGenerator.HMAC_RANDOM_SEPARATOR); if (arr.length != 2) { if (LOG.isWarnEnabled()) { LOG.warn("Invalid CSRF token: {}", csrfTokenInRequest); diff --git a/src/main/docs/guide/csrf/csrfApis.adoc b/src/main/docs/guide/csrf/csrfApis.adoc index 533b981edb..8906bfd7d5 100644 --- a/src/main/docs/guide/csrf/csrfApis.adoc +++ b/src/main/docs/guide/csrf/csrfApis.adoc @@ -4,5 +4,6 @@ The main APIs for CSRF protection are: * api:security.csrf.filter.CsrfFilterConfiguration[] * api:security.csrf.resolver.CsrfTokenResolver[] * api:security.csrf.generator.CsrfTokenGenerator[] +* api:security.csrf.generator.CsrfHmacTokenGenerator[] * api:security.csrf.validator.CsrfTokenValidator[] * api:security.csrf.repository.CsrfTokenRepository[] From 54228f6017a62ef3e84378cb2f2179bb2cc2ea7b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 12:13:07 +0200 Subject: [PATCH 060/108] base64 encoded sessionid --- .../security/csrf/generator/DefaultCsrfTokenGenerator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index b4f394f7ef..6971d2ddf2 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -97,8 +97,9 @@ public String hmac(@NonNull T request, String randomValue) { } static String hmacMessagePayload(String sessionId, String randomValue) { - return sessionId.replace(SESSION_RANDOM_SEPARATOR, "") + // this ensures session ID, does not have the separator as a substring. + // both session id and randomValue will be base64 encoded strings to ensure they don't contain the separator ! as a substring. + return Base64.getEncoder().encodeToString(sessionId.getBytes()) + SESSION_RANDOM_SEPARATOR + - randomValue; // randomValue cannot have the separator, as ! is not a base64 character. + randomValue; // } } From d8d5020af2080ccb7a45bcc857751164dcde65c0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 12:54:23 +0200 Subject: [PATCH 061/108] add length --- .../csrf/generator/DefaultCsrfTokenGenerator.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index 6971d2ddf2..bf29343ef7 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -98,8 +98,13 @@ public String hmac(@NonNull T request, String randomValue) { static String hmacMessagePayload(String sessionId, String randomValue) { // both session id and randomValue will be base64 encoded strings to ensure they don't contain the separator ! as a substring. - return Base64.getEncoder().encodeToString(sessionId.getBytes()) + + final String base64SessionId = Base64.getEncoder().encodeToString(sessionId.getBytes()); + return base64SessionId.length() + SESSION_RANDOM_SEPARATOR + - randomValue; // + base64SessionId + + SESSION_RANDOM_SEPARATOR + + randomValue.length() + + SESSION_RANDOM_SEPARATOR + + randomValue; } } From 0b091eca439534bf19153c25d36013ff01d15a1f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 12:59:14 +0200 Subject: [PATCH 062/108] Update security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java Co-authored-by: Jonas Konrad --- .../security/csrf/validator/RepositoryCsrfTokenValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java index 3407a7d481..92fb8092a6 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java @@ -75,6 +75,6 @@ private boolean validateHmac(T request, String csrfTokenInRequest) { String hmac = arr[0]; String randomValue = arr[1]; String expectedHmac = defaultCsrfTokenGenerator.hmac(request, randomValue); - return hmac.equals(expectedHmac); + return MessageDigest.isEquals(expectedHmac.getBytes(StandardCharsets.UTF_8), hmac.getBytes(StandardCharsets.UTF_8)); } } From c12365cecdbb34c42c44746224a5dd51e072fa17 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 13:09:24 +0200 Subject: [PATCH 063/108] Update src/main/docs/guide/toc.yml --- src/main/docs/guide/toc.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index ab1bfcf8a1..b93c7018b3 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -36,9 +36,9 @@ endpoints: builtInHandlers: title: Built-in Login and Logout Handlers micronautSecurityAuthenticationBearer: Micronaut Security Authentication Bearer - micronautSecurityAuthenticationSession: Micronaut Security Session Login Handler - micronautSecurityAuthenticationCookie: Micronaut Security Authentication Cookie - micronautSecurityAuthenticationIdToken: Micronaut Security Authentication ID Token + micronautSecurityAuthenticationSession: Authentication Mode Session + micronautSecurityAuthenticationCookie: Authentication Mode Cookie + micronautSecurityAuthenticationIdToken: Authentication Mode ID Token securityConfiguration: title: Security Configuration rejectNotFound: Reject Not Found Routes From 3db52fa3d24c5edf2be8f44d7b62d0b7b0c344d2 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 13:09:30 +0200 Subject: [PATCH 064/108] Update src/main/docs/guide/toc.yml --- src/main/docs/guide/toc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index b93c7018b3..41399b5421 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -35,7 +35,7 @@ endpoints: logoutHandler: Logout Handler builtInHandlers: title: Built-in Login and Logout Handlers - micronautSecurityAuthenticationBearer: Micronaut Security Authentication Bearer + micronautSecurityAuthenticationBearer: Authentication Mode Bearer micronautSecurityAuthenticationSession: Authentication Mode Session micronautSecurityAuthenticationCookie: Authentication Mode Cookie micronautSecurityAuthenticationIdToken: Authentication Mode ID Token From 94920866171c781f8c22467eeea6668073261127 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 13:11:32 +0200 Subject: [PATCH 065/108] Update security/src/main/java/io/micronaut/security/utils/HMacUtils.java Co-authored-by: Jonas Konrad --- .../src/main/java/io/micronaut/security/utils/HMacUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/src/main/java/io/micronaut/security/utils/HMacUtils.java b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java index f0d1a95ecc..3e9c05430b 100644 --- a/security/src/main/java/io/micronaut/security/utils/HMacUtils.java +++ b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java @@ -59,7 +59,7 @@ public static String base64EncodedHmacSha256(@NonNull String data, @NonNull Stri */ public static String base64EncodedHmac(@NonNull String algorithm, @NonNull String data, @NonNull String key) throws NoSuchAlgorithmException, InvalidKeyException { - SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm); + SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm); Mac mac = Mac.getInstance(algorithm); mac.init(secretKeySpec); return Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(data.getBytes())); From 0c38e0bb8c0e9feabcb91d33e1829889c313f77b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 15:09:50 +0200 Subject: [PATCH 066/108] add import --- .../src/main/java/io/micronaut/security/utils/HMacUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/security/src/main/java/io/micronaut/security/utils/HMacUtils.java b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java index 3e9c05430b..5302df366a 100644 --- a/security/src/main/java/io/micronaut/security/utils/HMacUtils.java +++ b/security/src/main/java/io/micronaut/security/utils/HMacUtils.java @@ -20,6 +20,7 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; From 02b058992788ff164ef2a1c7d20360bf6134abc7 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 24 Oct 2024 15:11:59 +0200 Subject: [PATCH 067/108] add missing imports --- .../csrf/validator/RepositoryCsrfTokenValidator.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java index 92fb8092a6..11cff3e574 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java @@ -21,9 +21,11 @@ import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; - +import java.security.MessageDigest; /** * {@link CsrfTokenValidator} implementation that uses a {@link CsrfTokenRepository}. @@ -75,6 +77,6 @@ private boolean validateHmac(T request, String csrfTokenInRequest) { String hmac = arr[0]; String randomValue = arr[1]; String expectedHmac = defaultCsrfTokenGenerator.hmac(request, randomValue); - return MessageDigest.isEquals(expectedHmac.getBytes(StandardCharsets.UTF_8), hmac.getBytes(StandardCharsets.UTF_8)); + return MessageDigest.isEqual(expectedHmac.getBytes(StandardCharsets.UTF_8), hmac.getBytes(StandardCharsets.UTF_8)); } } From 31fed71b9553115466110091c4f85b3aa0528f37 Mon Sep 17 00:00:00 2001 From: sdelamo Date: Fri, 25 Oct 2024 09:19:07 +0200 Subject: [PATCH 068/108] increase coverage --- security-csrf/build.gradle.kts | 28 ++- .../security/csrf/filter/CsrfFilter.java | 4 +- .../generator/DefaultCsrfTokenGenerator.java | 3 +- .../resolver/HttpHeaderCsrfTokenResolver.java | 4 +- .../CsrfConfigurationDisabledTest.java | 23 +++ .../csrf/CsrfConfigurationEnabledSetTest.java | 15 ++ .../csrf/CsrfConfigurationPropertiesTest.java | 48 +++++ ...CsrfFilterConfigurationPropertiesTest.java | 35 ++++ .../csrf/filter/CsrfFilterDisabledTest.java | 3 +- ...ubleSubmitCookiePatternWithHeaderTest.java | 186 ++++++++++++++++++ 10 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 security-csrf/src/test/java/io/micronaut/security/CsrfConfigurationDisabledTest.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationEnabledSetTest.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationPropertiesTest.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationPropertiesTest.java create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfDoubleSubmitCookiePatternWithHeaderTest.java diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index b90b5df5f3..d39694dabd 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("io.micronaut.build.internal.security-module") + jacoco } dependencies { @@ -26,4 +27,29 @@ tasks.withType { micronautBuild { binaryCompatibility.enabled = false -} \ No newline at end of file +} +tasks.test { + finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run +} +tasks.jacocoTestReport { + dependsOn(tasks.test) // tests are required to run before generating the report + reports { + xml.required = false + csv.required = false + html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml")) + } +} +tasks.jacocoTestCoverageVerification { + enabled = true + violationRules { + rule { + limit { + minimum = "0".toBigDecimal() + } + } + } +} + +tasks.check { + dependsOn(tasks.jacocoTestCoverageVerification) +} diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 3c8d32a914..57c0bf1ec4 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -113,10 +113,10 @@ private Mono>> reactiveFilter(HttpRequest req return reactiveUnauthorized(request); })); } - + private Mono>> imperativeFilter(HttpRequest request) { String csrfToken = resolveCsrfToken(request); - if (csrfToken == null) { + if (StringUtils.isEmpty(csrfToken)) { if (LOG.isDebugEnabled()) { LOG.debug("Request rejected by the {} because no CSRF Token found", this.getClass().getSimpleName()); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index bf29343ef7..a58d65ddc1 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.cookie.CookieConfiguration; import io.micronaut.security.csrf.CsrfConfiguration; import io.micronaut.security.session.SessionIdResolver; @@ -86,7 +87,7 @@ public String hmac(@NonNull T request, String randomValue) { // Create the CSRF Token String message = hmacMessagePayload(sessionID, randomValue); try { - return secret != null + return StringUtils.isNotEmpty(secret) ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash : ""; } catch (InvalidKeyException ex) { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index bad47f8845..efd1d720cf 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -44,11 +44,11 @@ final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver resolveToken(HttpRequest request) { String csrfToken = request.getHeaders().get(csrfConfiguration.getHeaderName()); - if (csrfToken != null) { + if (StringUtils.isNotEmpty(csrfToken)) { return Optional.of(csrfToken); } csrfToken = request.getHeaders().get(csrfConfiguration.getHeaderName().toLowerCase()); - if (csrfToken != null) { + if (StringUtils.isNotEmpty(csrfToken)) { return Optional.of(csrfToken); } return Optional.empty(); diff --git a/security-csrf/src/test/java/io/micronaut/security/CsrfConfigurationDisabledTest.java b/security-csrf/src/test/java/io/micronaut/security/CsrfConfigurationDisabledTest.java new file mode 100644 index 0000000000..bc264f8cad --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/CsrfConfigurationDisabledTest.java @@ -0,0 +1,23 @@ +package io.micronaut.security; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "micronaut.security.csrf.enabled", value = StringUtils.FALSE) +@MicronautTest(startApplication = false) +class CsrfConfigurationDisabledTest { + + @Inject + BeanContext beanContext; + + @Test + void disabledCsrf() { + assertFalse(beanContext.containsBean(CsrfConfiguration.class)); + } +} diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationEnabledSetTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationEnabledSetTest.java new file mode 100644 index 0000000000..388a72b542 --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationEnabledSetTest.java @@ -0,0 +1,15 @@ +package io.micronaut.security.csrf; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +class CsrfConfigurationEnabledSetTest { + + @Test + void csrfSetEnabled() { + CsrfConfigurationProperties configuration = new CsrfConfigurationProperties(); + configuration.setEnabled(false); + assertFalse(configuration.isEnabled()); + } +} diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationPropertiesTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationPropertiesTest.java new file mode 100644 index 0000000000..e12cc053a2 --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/CsrfConfigurationPropertiesTest.java @@ -0,0 +1,48 @@ +package io.micronaut.security.csrf; + +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.cookie.SameSite; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "micronaut.security.csrf.header-name", value = "header-foo") +@Property(name = "micronaut.security.csrf.field-name", value = "field-foo") +@Property(name = "micronaut.security.csrf.random-value-size", value = "5") +@Property(name = "micronaut.security.csrf.http-session-name", value = "session-foo") +@Property(name = "micronaut.security.csrf.cookie-domain", value = "cookie-domain-foo") +@Property(name = "micronaut.security.csrf.cookie-secure", value = StringUtils.FALSE) +@Property(name = "micronaut.security.csrf.cookie-path", value = "cookie-path-foo") +@Property(name = "micronaut.security.csrf.cookie-http-only", value = StringUtils.FALSE) +@Property(name = "micronaut.security.csrf.cookie-max-age", value = "5s") +@Property(name = "micronaut.security.csrf.cookie-name", value = "cookie-name-foo") +@Property(name = "micronaut.security.csrf.cookie-same-site", value = "Lax") +@Property(name = "micronaut.security.csrf.signature-key", value = "signature-key-foo") +@MicronautTest(startApplication = false) +class CsrfConfigurationPropertiesTest { + @Test + void settingCsrfConfiguration(CsrfConfiguration csrfConfiguration) { + assertEquals("header-foo", csrfConfiguration.getHeaderName()); + assertEquals("field-foo", csrfConfiguration.getFieldName()); + assertEquals(5, csrfConfiguration.getRandomValueSize()); + assertEquals("session-foo" ,csrfConfiguration.getHttpSessionName()); + assertTrue(csrfConfiguration.getCookieDomain().isPresent()); + assertEquals("cookie-domain-foo", csrfConfiguration.getCookieDomain().get()); + assertTrue(csrfConfiguration.isCookieSecure().isPresent()); + assertFalse(csrfConfiguration.isCookieSecure().get()); + assertTrue(csrfConfiguration.getCookiePath().isPresent()); + assertEquals("cookie-path-foo", csrfConfiguration.getCookiePath().get()); + assertTrue(csrfConfiguration.isCookieHttpOnly().isPresent()); + assertFalse(csrfConfiguration.isCookieHttpOnly().get()); + assertTrue(csrfConfiguration.getCookieMaxAge().isPresent()); + assertEquals(Duration.ofSeconds(5), csrfConfiguration.getCookieMaxAge().get()); + assertEquals("cookie-name-foo", csrfConfiguration.getCookieName()); + assertTrue(csrfConfiguration.getCookieSameSite().isPresent()); + assertEquals(SameSite.Lax, csrfConfiguration.getCookieSameSite().get()); + assertEquals("signature-key-foo", csrfConfiguration.getSecretKey()); + } +} diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationPropertiesTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationPropertiesTest.java new file mode 100644 index 0000000000..52381f0214 --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationPropertiesTest.java @@ -0,0 +1,35 @@ +package io.micronaut.security.csrf.filter; + +import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.MediaType; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; +import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$") +@Property(name = "micronaut.security.csrf.filter.methods[0]", value = "TRACE") +@Property(name = "micronaut.security.csrf.filter.methods[1]", value = "HEAD") +@Property(name = "micronaut.security.csrf.filter.content-types[0]", value = "application/xml") +@Property(name = "micronaut.security.csrf.filter.content-types[1]", value = "application/graphql") +@MicronautTest(startApplication = false) +class CsrfFilterConfigurationPropertiesTest { + @Test + void testFilterConfigurationSetting(CsrfFilterConfiguration configuration) { + assertEquals("^(?!\\/login).*$", + configuration.getRegexPattern()); + assertEquals(Set.of(HttpMethod.TRACE, HttpMethod.HEAD), + configuration.getMethods()); + assertEquals(Set.of(MediaType.APPLICATION_XML_TYPE, MediaType.APPLICATION_GRAPHQL_TYPE), + configuration.getContentTypes()); + } + + @Test + void csrfFilterSetEnabled() { + CsrfFilterConfigurationProperties configuration = new CsrfFilterConfigurationProperties(); + configuration.setEnabled(false); + assertFalse(configuration.isEnabled()); + } +} diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java index 456fbc8e4a..4d2151061b 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterDisabledTest.java @@ -20,5 +20,4 @@ class CsrfFilterDisabledTest { void testFieldCsrfTokenResolverDisabled() { assertFalse(beanContext.containsBean(CsrfFilter.class)); } - -} \ No newline at end of file +} diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfDoubleSubmitCookiePatternWithHeaderTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfDoubleSubmitCookiePatternWithHeaderTest.java new file mode 100644 index 0000000000..9f7f6ba664 --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/generator/CsrfDoubleSubmitCookiePatternWithHeaderTest.java @@ -0,0 +1,186 @@ +package io.micronaut.security.csrf.generator; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.*; +import io.micronaut.http.annotation.*; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.csrf.CsrfConfiguration; +import io.micronaut.security.csrf.repository.CsrfTokenRepository; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.session.SessionIdResolver; +import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider; +import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario; +import io.micronaut.security.token.cookie.TokenCookieConfigurationProperties; +import io.micronaut.security.utils.HMacUtils; +import io.micronaut.serde.annotation.Serdeable; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.micronaut.security.csrf.generator.DefaultCsrfTokenGenerator.hmacMessagePayload; +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "micronaut.security.authentication", value = "cookie") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "pleaseChangeThisSecretForANewOne") +@Property(name = "micronaut.security.csrf.signature-key", value = "pleaseChangeThisSecretForANewOnekoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") +@Property(name = "micronaut.security.redirect.enabled", value = StringUtils.FALSE) +@Property(name = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.FALSE) +@Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$") +@Property(name = "spec.name", value = "CsrfDoubleSubmitCookiePatternWithHeaderTest") +@MicronautTest +class CsrfDoubleSubmitCookiePatternWithHeaderTest { + public static final String FIX_SESSION_ID = "123456789"; + + @Test + void loginSavesACsrfTokenInCookie(@Client("/") HttpClient httpClient, + CsrfConfiguration csrfConfiguration) throws NoSuchAlgorithmException, InvalidKeyException { + BlockingHttpClient client = httpClient.toBlocking(); + + HttpRequest loginRequest = HttpRequest.POST("/login",Map.of("username", "sherlock", "password", "password")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + + HttpResponse loginRsp = assertDoesNotThrow(() -> client.exchange(loginRequest)); + assertEquals(HttpStatus.OK, loginRsp.getStatus()); + Optional cookieJwtOptional = loginRsp.getCookie("JWT"); + assertTrue(cookieJwtOptional.isPresent()); + Cookie cookieJwt = cookieJwtOptional.get(); + String csrfTokenCookieName = "__Host-csrfToken"; + Optional cookieCsrfTokenOptional = loginRsp.getCookie(csrfTokenCookieName); + assertTrue(cookieCsrfTokenOptional.isPresent()); + Cookie cookieCsrfToken = cookieCsrfTokenOptional.get(); + + // CSRF Only in the cookie, not in the request headers or field, request is denied + String csrfTokenHeader = ""; + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, new PasswordChange("sherlock", "evil"), cookieCsrfToken.getValue(), csrfTokenHeader); + + // CSRF Token in request and in cookie don't match, request is unauthorized + String csrfToken = "abcdefg"; + assertNotEquals(cookieCsrfToken.getValue(), csrfToken); + PasswordChange formWithCsrfToken = new PasswordChange("sherlock", "evil"); + csrfTokenHeader = csrfToken; + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, formWithCsrfToken, cookieCsrfToken.getValue(), csrfTokenHeader); + + // CSRF Token with HMAC but not session id feed into HMAC calculation, request is unauthorized + String randomValue = "abcdefg"; + String hmac = HMacUtils.base64EncodedHmacSha256(randomValue, csrfConfiguration.getSecretKey()); + String csrfTokenCalculatedWithoutSessionId = hmac + "." + randomValue; + PasswordChange body = new PasswordChange("sherlock", "evil"); + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, body, csrfTokenCalculatedWithoutSessionId, csrfTokenCalculatedWithoutSessionId); + + String message = hmacMessagePayload(FIX_SESSION_ID, randomValue); + hmac = HMacUtils.base64EncodedHmacSha256(message, csrfConfiguration.getSecretKey()); + csrfToken = hmac + "." + randomValue; + assertOk(client, cookieJwt.getValue(), csrfTokenCookieName, csrfToken); + + // Even if you have the same session id and random value, the attacker cannot generate the same hmac as he does not have the same secret key + String evilSignatureKey = "evilAyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAowevil"; + csrfToken = HMacUtils.base64EncodedHmacSha256(message, evilSignatureKey) + "." + randomValue; + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, new PasswordChange("sherlock", "evil"), csrfToken, csrfToken); + // invalid csrf but content type not intercepted by the filter. + assertOk(client, cookieJwt.getValue(), csrfTokenCookieName, csrfToken, MediaType.APPLICATION_JSON_TYPE); + + // CSRF Token in request match token in cookie and hmac signature is valid. + csrfToken = cookieCsrfToken.getValue(); + assertOk(client, cookieJwt.getValue(), csrfTokenCookieName, csrfToken); + + // Default CSRF Token validator expects the CSRF token to have a dot + csrfToken= csrfToken.replace(".", ""); + assertDenied(client, cookieJwt.getValue(), csrfTokenCookieName, new PasswordChange("sherlock", "evil"), csrfToken, csrfToken); + } + + private void assertDenied(BlockingHttpClient client, String cookieJwt, String csrfTokenCookieName, PasswordChange body, String csrfToken, String csrfTokenHeader) { + HttpRequest request = HttpRequest.POST("/password/change", body) + .header("X-CSRF-TOKEN", csrfTokenHeader) + .cookie(Cookie.of(TokenCookieConfigurationProperties.DEFAULT_COOKIENAME, cookieJwt)) + .cookie(Cookie.of(csrfTokenCookieName, csrfToken)) + .accept(MediaType.TEXT_HTML) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE); + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.retrieve(request)); + assertEquals(HttpStatus.FORBIDDEN, ex.getStatus()); + } + + private void assertOk(BlockingHttpClient client, String cookieJwt, String csrfTokenCookieName, String csrfToken) { + assertOk(client, cookieJwt, csrfTokenCookieName, csrfToken, MediaType.APPLICATION_FORM_URLENCODED_TYPE); + } + + private void assertOk(BlockingHttpClient client, String cookieJwt, String csrfTokenCookieName, String csrfToken, MediaType contentType) { + PasswordChange body = new PasswordChange("sherlock", "evil"); + HttpRequest request = HttpRequest.POST("/password/change", body) + .header("X-CSRF-TOKEN", csrfToken) + .cookie(Cookie.of(TokenCookieConfigurationProperties.DEFAULT_COOKIENAME, cookieJwt)) + .cookie(Cookie.of(csrfTokenCookieName, csrfToken)) + .accept(MediaType.TEXT_HTML) + .contentType(contentType); + + HttpResponse response = assertDoesNotThrow(() -> client.exchange(request, String.class)); + assertEquals(HttpStatus.OK, response.getStatus()); + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternWithHeaderTest") + @Singleton + static class MockSessionIdResolver implements SessionIdResolver> { + @Override + @NonNull + public Optional findSessionId(@NonNull HttpRequest request) { + return Optional.of(FIX_SESSION_ID); + } + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternWithHeaderTest") + @Singleton + static class AuthenticationProviderUserPassword extends MockAuthenticationProvider { + AuthenticationProviderUserPassword() { + super(List.of(new SuccessAuthenticationScenario("sherlock"))); + } + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternWithHeaderTest") + @Controller + static class PasswordChangeController { + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_HTML) + @Consumes({ MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON }) + @Post("/password/change") + String changePassword(@Body PasswordChange passwordChangeForm) { + return passwordChangeForm.username; + } + } + + @Serdeable + record PasswordChange( + String username, + String password) { + } + + @Requires(property = "spec.name", value = "CsrfDoubleSubmitCookiePatternWithHeaderTest") + @Controller("/csrf") + static class CsrfTokenEchoController { + + private final CsrfTokenRepository> csrfTokenRepository; + + CsrfTokenEchoController(CsrfTokenRepository> csrfTokenRepository) { + this.csrfTokenRepository = csrfTokenRepository; + } + + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_PLAIN) + @Get("/echo") + Optional echo(HttpRequest request) { + return csrfTokenRepository.findCsrfToken(request); + } + } +} From 73266ba5d153718ea87b3d342df36651cd181ab4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 09:48:34 +0200 Subject: [PATCH 069/108] core 4.7.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2936c4453..e7e9a32921 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ managed-nimbus-jose-jwt = "9.41.2" managed-jjwt = "0.12.6" -micronaut = "4.7.0" +micronaut = "4.7.1" micronaut-platform = "4.6.3" micronaut-docs = "2.0.0" From 94beaf6f0061ab55ba53bfcab13c8f99c47fe207 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 09:52:05 +0200 Subject: [PATCH 070/108] use FilterBodyParser API --- .../csrf/resolver/FieldCsrfTokenResolver.java | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index b15c2a843a..728009c7be 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -20,14 +20,12 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.ServerHttpRequest; -import io.micronaut.http.body.ByteBody; +import io.micronaut.http.server.filter.FilterBodyParser; import io.micronaut.security.csrf.CsrfConfiguration; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import java.util.Optional; - /** * Resolves a CSRF token from a form-urlencoded body using the {@link ServerHttpRequest#byteBody()} API. * @@ -38,9 +36,16 @@ @Singleton class FieldCsrfTokenResolver implements ReactiveCsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; + private final FilterBodyParser filterBodyParser; - FieldCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { + /** + * + * @param csrfConfiguration CSRF Configuration + * @param filterBodyParser Filter Body Parser + */ + FieldCsrfTokenResolver(CsrfConfiguration csrfConfiguration, FilterBodyParser filterBodyParser) { this.csrfConfiguration = csrfConfiguration; + this.filterBodyParser = filterBodyParser; } @Override @@ -53,20 +58,12 @@ public Publisher resolveToken(HttpRequest request) { } private Publisher resolveToken(ServerHttpRequest request) { - return Mono.fromFuture(request.byteBody().split(ByteBody.SplitBackpressureMode.FASTEST).buffer()) - .map(bb -> bb.toString(request.getCharacterEncoding())) - .map(this::extractCsrfTokenFromAFormUrlEncodedString) - .flatMap(opt -> opt.map(Mono::just).orElseGet(Mono::empty)); - } - - private Optional extractCsrfTokenFromAFormUrlEncodedString(String body) { - final String[] arr = body.split("&"); - final String prefix = csrfConfiguration.getFieldName() + "="; - for (String s : arr) { - if (s.startsWith(prefix)) { - return Optional.of(s.substring(prefix.length())); - } - } - return Optional.empty(); + return Mono.fromFuture(filterBodyParser.parseBody(request)) + .flatMap(m -> { + Object csrfToken = m.get(csrfConfiguration.getFieldName()); + return csrfToken == null || StringUtils.isEmpty(csrfToken.toString()) + ? Mono.empty() + : Mono.just(csrfToken.toString()); + }); } } From 7f708037a5abd490a057f2c3b49fca63ddbe7503 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 09:57:26 +0200 Subject: [PATCH 071/108] Use CompletableFuture instead of Publisher --- .../security/csrf/filter/CsrfFilter.java | 18 +++++++-------- .../csrf/resolver/FieldCsrfTokenResolver.java | 20 ++++++++--------- ...lver.java => FutureCsrfTokenResolver.java} | 22 +++++++++---------- ...va => FutureCsrfTokenResolverAdapter.java} | 15 +++++-------- .../csrf/resolver/CsrfTokenResolverTest.java | 13 ++++------- 5 files changed, 39 insertions(+), 49 deletions(-) rename security-csrf/src/main/java/io/micronaut/security/csrf/resolver/{ReactiveCsrfTokenResolver.java => FutureCsrfTokenResolver.java} (59%) rename security-csrf/src/main/java/io/micronaut/security/csrf/resolver/{ReactiveCsrfTokenResolverAdapter.java => FutureCsrfTokenResolverAdapter.java} (64%) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 57c0bf1ec4..a12c4becc0 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -30,7 +30,7 @@ import io.micronaut.security.authentication.Authentication; import io.micronaut.security.authentication.AuthorizationException; import io.micronaut.security.csrf.resolver.CsrfTokenResolver; -import io.micronaut.security.csrf.resolver.ReactiveCsrfTokenResolver; +import io.micronaut.security.csrf.resolver.FutureCsrfTokenResolver; import io.micronaut.security.csrf.validator.CsrfTokenValidator; import io.micronaut.security.filters.SecurityFilter; import org.reactivestreams.Publisher; @@ -56,21 +56,21 @@ value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") final class CsrfFilter implements Ordered { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); - private final List>> reactiveCsrfTokenResolvers; + private final List>> futureCsrfTokenResolvers; private final List>> csrfTokenResolvers; private final CsrfTokenValidator> csrfTokenValidator; private final ExceptionHandler> exceptionHandler; private final CsrfFilterConfiguration csrfFilterConfiguration; CsrfFilter(CsrfFilterConfiguration csrfFilterConfiguration, - List>> reactiveCsrfTokenResolvers, + List>> futureCsrfTokenResolvers, List>> csrfTokenResolvers, CsrfTokenValidator> csrfTokenValidator, ExceptionHandler> exceptionHandler) { this.csrfTokenResolvers = csrfTokenResolvers; - this.reactiveCsrfTokenResolvers = reactiveCsrfTokenResolvers.isEmpty() - ? reactiveCsrfTokenResolvers - : ReactiveCsrfTokenResolver.of(csrfTokenResolvers, reactiveCsrfTokenResolvers); + this.futureCsrfTokenResolvers = futureCsrfTokenResolvers.isEmpty() + ? futureCsrfTokenResolvers + : FutureCsrfTokenResolver.of(csrfTokenResolvers, futureCsrfTokenResolvers); this.csrfTokenValidator = csrfTokenValidator; this.exceptionHandler = exceptionHandler; this.csrfFilterConfiguration = csrfFilterConfiguration; @@ -85,7 +85,7 @@ public Publisher>> csrfFilter(@NonNull HttpReque if (!shouldTheFilterProcessTheRequestAccordingToTheContentType(request)) { return proceedRequest(); } - return reactiveCsrfTokenResolvers.isEmpty() + return futureCsrfTokenResolvers.isEmpty() ? imperativeFilter(request) : reactiveFilter(request); } @@ -95,8 +95,8 @@ private static Mono>> proceedRequest() { } private Mono>> reactiveFilter(HttpRequest request) { - return Flux.fromIterable(this.reactiveCsrfTokenResolvers) - .concatMap(resolver -> Mono.from(resolver.resolveToken(request)) + return Flux.fromIterable(this.futureCsrfTokenResolvers) + .concatMap(resolver -> Mono.fromFuture(resolver.resolveToken(request)) .filter(csrfToken -> { LOG.debug("CSRF Token resolved"); if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 728009c7be..2bfc488ca2 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -16,15 +16,13 @@ package io.micronaut.security.csrf.resolver; import io.micronaut.context.annotation.Requires; -import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.ServerHttpRequest; import io.micronaut.http.server.filter.FilterBodyParser; import io.micronaut.security.csrf.CsrfConfiguration; import jakarta.inject.Singleton; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; +import java.util.concurrent.CompletableFuture; /** * Resolves a CSRF token from a form-urlencoded body using the {@link ServerHttpRequest#byteBody()} API. @@ -34,7 +32,7 @@ @Requires(classes = HttpRequest.class) @Requires(property = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton -class FieldCsrfTokenResolver implements ReactiveCsrfTokenResolver> { +class FieldCsrfTokenResolver implements FutureCsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; private final FilterBodyParser filterBodyParser; @@ -50,20 +48,20 @@ class FieldCsrfTokenResolver implements ReactiveCsrfTokenResolver @Override @Singleton - public Publisher resolveToken(HttpRequest request) { + public CompletableFuture resolveToken(HttpRequest request) { if (request instanceof ServerHttpRequest serverHttpRequest) { return resolveToken(serverHttpRequest); } - return Publishers.empty(); + return CompletableFuture.completedFuture(null); } - private Publisher resolveToken(ServerHttpRequest request) { - return Mono.fromFuture(filterBodyParser.parseBody(request)) - .flatMap(m -> { + private CompletableFuture resolveToken(ServerHttpRequest request) { + return filterBodyParser.parseBody(request) + .thenApply(m -> { Object csrfToken = m.get(csrfConfiguration.getFieldName()); return csrfToken == null || StringUtils.isEmpty(csrfToken.toString()) - ? Mono.empty() - : Mono.just(csrfToken.toString()); + ? null + : csrfToken.toString(); }); } } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java similarity index 59% rename from security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolver.java rename to security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java index fd3e1e7817..d8f189cefb 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java @@ -19,20 +19,20 @@ import io.micronaut.core.async.annotation.SingleResult; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.order.Ordered; -import org.reactivestreams.Publisher; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * Attempts to resolve a CSRF token from the provided request. - * {@link ReactiveCsrfTokenResolver} is an {@link Ordered} api. Override the {@link #getOrder()} method to provide a custom order. + * {@link FutureCsrfTokenResolver} is an {@link Ordered} api. Override the {@link #getOrder()} method to provide a custom order. * * @author Sergio del Amo * @since 1.1.0 * @param request */ -public interface ReactiveCsrfTokenResolver extends Ordered { +public interface FutureCsrfTokenResolver extends Ordered { /** * @@ -41,20 +41,20 @@ public interface ReactiveCsrfTokenResolver extends Ordered { */ @SingleResult @NonNull - Publisher resolveToken(T request); + CompletableFuture resolveToken(T request); /** * * @param resolvers Imperative CSRF Token Resolvers - * @param reactiveCsrfTokenResolvers Reactive CSRF Token Resolvers - * @return Returns a List of {@link ReactiveCsrfTokenResolver} instances containing every reactive resolver plus the imperative resolvers adapted to imperative. + * @param futureCsrfTokenResolvers Reactive CSRF Token Resolvers + * @return Returns a List of {@link FutureCsrfTokenResolver} instances containing every reactive resolver plus the imperative resolvers adapted to imperative. * @param */ - static List> of(List> resolvers, - List> reactiveCsrfTokenResolvers) { - List> result = new ArrayList<>(); - result.addAll(reactiveCsrfTokenResolvers); - result.addAll(resolvers.stream().map(ReactiveCsrfTokenResolverAdapter::new).toList()); + static List> of(List> resolvers, + List> futureCsrfTokenResolvers) { + List> result = new ArrayList<>(); + result.addAll(futureCsrfTokenResolvers); + result.addAll(resolvers.stream().map(FutureCsrfTokenResolverAdapter::new).toList()); OrderUtil.sort(result); return result; } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolverAdapter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java similarity index 64% rename from security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolverAdapter.java rename to security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java index fbef03675a..be32e9c0bb 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/ReactiveCsrfTokenResolverAdapter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java @@ -15,14 +15,13 @@ */ package io.micronaut.security.csrf.resolver; -import io.micronaut.core.async.publisher.Publishers; -import org.reactivestreams.Publisher; +import java.util.concurrent.CompletableFuture; /** - * Adapter from {@link CsrfTokenResolver} to {@link ReactiveCsrfTokenResolver}. + * Adapter from {@link CsrfTokenResolver} to {@link FutureCsrfTokenResolver}. * @param Request */ -public class ReactiveCsrfTokenResolverAdapter implements ReactiveCsrfTokenResolver { +public class FutureCsrfTokenResolverAdapter implements FutureCsrfTokenResolver { private final CsrfTokenResolver csrfTokenResolver; @@ -30,15 +29,13 @@ public class ReactiveCsrfTokenResolverAdapter implements ReactiveCsrfTokenRes * * @param csrfTokenResolver CSRF Token resolver */ - public ReactiveCsrfTokenResolverAdapter(CsrfTokenResolver csrfTokenResolver) { + public FutureCsrfTokenResolverAdapter(CsrfTokenResolver csrfTokenResolver) { this.csrfTokenResolver = csrfTokenResolver; } @Override - public Publisher resolveToken(T request) { - return csrfTokenResolver.resolveToken(request) - .map(Publishers::just) - .orElseGet(Publishers::empty); + public CompletableFuture resolveToken(T request) { + return CompletableFuture.completedFuture(csrfTokenResolver.resolveToken(request).orElse(null)); } @Override diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java index e78272af0e..34cb25be7b 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/CsrfTokenResolverTest.java @@ -1,15 +1,10 @@ package io.micronaut.security.csrf.resolver; -import io.micronaut.context.BeanContext; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -21,15 +16,15 @@ class CsrfTokenResolverTest { List>> csrfTokenResolvers; @Inject - List>> reactiveCsrfTokenResolvers; + List>> futureCsrfTokenResolvers; @Test void csrfTokenResolversOrder() { assertEquals(1, csrfTokenResolvers.size()); - assertEquals(1, reactiveCsrfTokenResolvers.size()); - List>> all = ReactiveCsrfTokenResolver.of(csrfTokenResolvers, reactiveCsrfTokenResolvers); + assertEquals(1, futureCsrfTokenResolvers.size()); + List>> all = FutureCsrfTokenResolver.of(csrfTokenResolvers, futureCsrfTokenResolvers); assertEquals(2, all.size()); // It is important for HTTP Header to be the first one. FieldCsrfTokenResolver requires Netty. Moreover, it is more secure to supply the CSRF token via custom HTTP Header instead of a form field as it is more difficult to exploit. - assertInstanceOf(ReactiveCsrfTokenResolverAdapter.class, all.get(0)); // with HttpHeaderCsrfTokenResolver inside + assertInstanceOf(FutureCsrfTokenResolverAdapter.class, all.get(0)); // with HttpHeaderCsrfTokenResolver inside assertInstanceOf(FieldCsrfTokenResolver.class, all.get(1)); } From 18b8afe8e4eef4e299e53764c073e4a8c5b3e32b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 10:20:28 +0200 Subject: [PATCH 072/108] javadoc: add missing @param --- .../micronaut/security/token/cookie/TokenCookieLoginHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java b/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java index 84076dce72..c3ab3e15c6 100644 --- a/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java +++ b/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java @@ -62,6 +62,7 @@ public class TokenCookieLoginHandler extends CookieLoginHandler { * @param accessTokenConfiguration JWT Generator Configuration * @param accessRefreshTokenGenerator Access Refresh Token Generator * @param priorToLoginPersistence Prior To Login Persistence Mechanism + * @param loginCookieProviders Login Cookie Providers */ @Inject public TokenCookieLoginHandler(RedirectService redirectService, From 13c9cfe7898bb0d246da96be36bf2e597fbd577c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 10:32:21 +0200 Subject: [PATCH 073/108] extra check of uri path with regex --- .../security/csrf/filter/CsrfFilter.java | 29 +++++++++++++++++++ .../security/csrf/filter/CsrfFilterTest.java | 18 ++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index a12c4becc0..2389680f7f 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.PathMatcher; import io.micronaut.core.util.StringUtils; import io.micronaut.http.*; import io.micronaut.http.annotation.RequestFilter; @@ -33,6 +34,8 @@ import io.micronaut.security.csrf.resolver.FutureCsrfTokenResolver; import io.micronaut.security.csrf.validator.CsrfTokenValidator; import io.micronaut.security.filters.SecurityFilter; +import io.micronaut.web.router.RouteMatch; +import io.micronaut.web.router.UriRouteMatch; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,6 +82,9 @@ final class CsrfFilter implements Ordered { @RequestFilter @Nullable public Publisher>> csrfFilter(@NonNull HttpRequest request) { + if (!shouldTheFilterProcessTheRequestAccordingToTheUriMatch(request)) { + return proceedRequest(); + } if (!shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(request)) { return proceedRequest(); } @@ -90,6 +96,29 @@ public Publisher>> csrfFilter(@NonNull HttpReque : reactiveFilter(request); } + private boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(HttpRequest request) { + RouteMatch routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class).orElse(null); + if (routeMatch instanceof UriRouteMatch uriRouteMatch) { + return shouldTheFilterProcessTheRequestAccordingToTheUriMatch(uriRouteMatch); + } + return true; + } + + private boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(UriRouteMatch uriRouteMatch) { + return shouldTheFilterProcessTheRequestAccordingToTheUriMatch(uriRouteMatch.getUri()); + } + + boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(String uri) { + boolean matches = PathMatcher.REGEX.matches(csrfFilterConfiguration.getRegexPattern(), uri); + if (!matches) { + if (LOG.isDebugEnabled()) { + LOG.debug("Request uri {} does not match fitler regex pattern {}", uri, csrfFilterConfiguration.getRegexPattern()); + } + return false; + } + return true; + } + private static Mono>> proceedRequest() { return Mono.just(Optional.empty()); } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java new file mode 100644 index 0000000000..58a794cbed --- /dev/null +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java @@ -0,0 +1,18 @@ +package io.micronaut.security.csrf.filter; + +import io.micronaut.context.annotation.Property; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$") +@MicronautTest(startApplication = false) +class CsrfFilterTest { + + @Test + void csrfFilterUriMatch(CsrfFilter csrfFilter) { + assertFalse(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch("/login")); + assertTrue(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch("/todo/list")); + } +} \ No newline at end of file From b6031ed2f3d871a671be7246d298bce03ccda580 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 10:40:36 +0200 Subject: [PATCH 074/108] increase coverage --- .../security/csrf/filter/CsrfFilter.java | 4 +- .../security/csrf/filter/CsrfFilterTest.java | 134 ++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 2389680f7f..49a660160e 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -96,7 +96,7 @@ public Publisher>> csrfFilter(@NonNull HttpReque : reactiveFilter(request); } - private boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(HttpRequest request) { + boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(HttpRequest request) { RouteMatch routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class).orElse(null); if (routeMatch instanceof UriRouteMatch uriRouteMatch) { return shouldTheFilterProcessTheRequestAccordingToTheUriMatch(uriRouteMatch); @@ -104,7 +104,7 @@ private boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(HttpReque return true; } - private boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(UriRouteMatch uriRouteMatch) { + boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(UriRouteMatch uriRouteMatch) { return shouldTheFilterProcessTheRequestAccordingToTheUriMatch(uriRouteMatch.getUri()); } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java index 58a794cbed..6418cbe563 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java @@ -1,9 +1,26 @@ package io.micronaut.security.csrf.filter; import io.micronaut.context.annotation.Property; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.ReturnType; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.bind.RequestBinderRegistry; +import io.micronaut.http.simple.SimpleHttpRequest; +import io.micronaut.http.uri.UriMatchVariable; +import io.micronaut.inject.ExecutableMethod; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.web.router.UriRouteInfo; +import io.micronaut.web.router.UriRouteMatch; import org.junit.jupiter.api.Test; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.*; @Property(name = "micronaut.security.csrf.filter.regex-pattern", value = "^(?!\\/login).*$") @@ -14,5 +31,122 @@ class CsrfFilterTest { void csrfFilterUriMatch(CsrfFilter csrfFilter) { assertFalse(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch("/login")); assertTrue(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch("/todo/list")); + + HttpRequest request = new SimpleHttpRequest<>(HttpMethod.POST, "/login", Collections.emptyMap()); + assertTrue(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch(request)); + request = new SimpleHttpRequest<>(HttpMethod.POST, "/todo/list", Collections.emptyMap()); + assertTrue(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch(request)); + + assertFalse(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch(createUriRouteMatch("/login"))); + assertTrue(csrfFilter.shouldTheFilterProcessTheRequestAccordingToTheUriMatch(createUriRouteMatch("/todo/list"))); + } + + private static UriRouteMatch createUriRouteMatch(String uri) { + return new UriRouteMatch() { + @Override + public UriRouteInfo getRouteInfo() { + throw new UnsupportedOperationException(); + } + + @Override + public HttpMethod getHttpMethod() { + throw new UnsupportedOperationException(); + } + + @Override + public @NonNull ExecutableMethod getExecutableMethod() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getTarget() { + throw new UnsupportedOperationException(); + } + + @Override + public Class getDeclaringType() { + throw new UnsupportedOperationException(); + } + + @Override + public Argument[] getArguments() { + throw new UnsupportedOperationException(); + } + + @Override + public Object invoke(Object... arguments) { + throw new UnsupportedOperationException(); + } + + @Override + public Method getTargetMethod() { + throw new UnsupportedOperationException(); + } + + @Override + public ReturnType getReturnType() { + throw new UnsupportedOperationException(); + } + + @Override + public String getMethodName() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUri() { + return uri; + } + + @Override + public Map getVariableValues() { + throw new UnsupportedOperationException(); + } + + @Override + public List getVariables() { + throw new UnsupportedOperationException(); + } + + @Override + public Map getVariableMap() { + throw new UnsupportedOperationException(); + } + + @Override + public void fulfill(Map argumentValues) { + + } + + @Override + public void fulfillBeforeFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) { + + } + + @Override + public void fulfillAfterFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) { + + } + + @Override + public boolean isFulfilled() { + return false; + } + + @Override + public Optional> getRequiredInput(String name) { + return Optional.empty(); + } + + @Override + public Object execute() { + return null; + } + + @Override + public boolean isSatisfied(String name) { + return false; + } + }; } } \ No newline at end of file From dee17cf0e85bed034667e36c79a6a8c4dfc31253 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:00:55 +0200 Subject: [PATCH 075/108] extract request.getHeaders() into a local variable to avoid repeated calls --- .../security/csrf/resolver/HttpHeaderCsrfTokenResolver.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index efd1d720cf..ae4290aa19 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpRequest; import io.micronaut.security.csrf.CsrfConfiguration; import jakarta.inject.Singleton; @@ -43,11 +44,12 @@ final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver resolveToken(HttpRequest request) { - String csrfToken = request.getHeaders().get(csrfConfiguration.getHeaderName()); + final HttpHeaders httpHeaders = request.getHeaders(); + String csrfToken = httpHeaders.get(csrfConfiguration.getHeaderName()); if (StringUtils.isNotEmpty(csrfToken)) { return Optional.of(csrfToken); } - csrfToken = request.getHeaders().get(csrfConfiguration.getHeaderName().toLowerCase()); + csrfToken = httpHeaders.get(csrfConfiguration.getHeaderName().toLowerCase()); if (StringUtils.isNotEmpty(csrfToken)) { return Optional.of(csrfToken); } From 3730f591edad43e4208ef22025af5d3e32e8ec97 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:03:24 +0200 Subject: [PATCH 076/108] split the method calls onto multiple lines to improve readability --- .../security/csrf/session/CsrfSessionPopulator.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java index f60ea54a8b..2c95a036d0 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java @@ -33,13 +33,21 @@ public class CsrfSessionPopulator implements SessionPopulator { private final CsrfConfiguration csrfConfiguration; private final CsrfTokenGenerator csrfTokenGenerator; - public CsrfSessionPopulator(CsrfConfiguration csrfConfiguration, CsrfTokenGenerator csrfTokenGenerator) { + /** + * + * @param csrfConfiguration CSRF Configuration + * @param csrfTokenGenerator CSRF Token Generator + */ + public CsrfSessionPopulator(CsrfConfiguration csrfConfiguration, + CsrfTokenGenerator csrfTokenGenerator) { this.csrfConfiguration = csrfConfiguration; this.csrfTokenGenerator = csrfTokenGenerator; } @Override - public void populateSession(T request, Authentication authentication, Session session) { + public void populateSession(T request, + Authentication authentication, + Session session) { session.put(csrfConfiguration.getHttpSessionName(), csrfTokenGenerator.generateCsrfToken(request)); } } From 96347417a157da5af9293e4fa9e89b631d31bad0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:05:16 +0200 Subject: [PATCH 077/108] stored in a field toLowerCase() be to avoid repeated calls --- .../csrf/resolver/HttpHeaderCsrfTokenResolver.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index ae4290aa19..7774f12826 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -36,20 +36,22 @@ @Internal final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver> { private static final int ORDER = -100; - private final CsrfConfiguration csrfConfiguration; + private final String lowerHeaderName; + private final String headerName; HttpHeaderCsrfTokenResolver(CsrfConfiguration csrfConfiguration) { - this.csrfConfiguration = csrfConfiguration; + headerName = csrfConfiguration.getHeaderName(); + lowerHeaderName = headerName.toLowerCase(); } @Override public Optional resolveToken(HttpRequest request) { final HttpHeaders httpHeaders = request.getHeaders(); - String csrfToken = httpHeaders.get(csrfConfiguration.getHeaderName()); + String csrfToken = httpHeaders.get(headerName); if (StringUtils.isNotEmpty(csrfToken)) { return Optional.of(csrfToken); } - csrfToken = httpHeaders.get(csrfConfiguration.getHeaderName().toLowerCase()); + csrfToken = httpHeaders.get(lowerHeaderName); if (StringUtils.isNotEmpty(csrfToken)) { return Optional.of(csrfToken); } From 0e26b76c5a4cacf4bb5f0498a35f7ddd32643ac0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:05:55 +0200 Subject: [PATCH 078/108] size the array according to the passed arguments --- .../security/csrf/resolver/FutureCsrfTokenResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java index d8f189cefb..19fd613f4b 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java @@ -52,7 +52,7 @@ public interface FutureCsrfTokenResolver extends Ordered { */ static List> of(List> resolvers, List> futureCsrfTokenResolvers) { - List> result = new ArrayList<>(); + List> result = new ArrayList<>(futureCsrfTokenResolvers.size() + resolvers.size()); result.addAll(futureCsrfTokenResolvers); result.addAll(resolvers.stream().map(FutureCsrfTokenResolverAdapter::new).toList()); OrderUtil.sort(result); From 76d404ed60e85b7cb3f86a3b8e603bc9d92432b3 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:07:29 +0200 Subject: [PATCH 079/108] add nullability annotations to return type and arguments --- .../security/csrf/resolver/FieldCsrfTokenResolver.java | 5 +++-- .../csrf/resolver/FutureCsrfTokenResolver.java | 10 +++++----- .../csrf/resolver/FutureCsrfTokenResolverAdapter.java | 5 ++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 2bfc488ca2..5ea432c99d 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.resolver; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.ServerHttpRequest; @@ -47,8 +48,8 @@ class FieldCsrfTokenResolver implements FutureCsrfTokenResolver> } @Override - @Singleton - public CompletableFuture resolveToken(HttpRequest request) { + @NonNull + public CompletableFuture resolveToken(@NonNull HttpRequest request) { if (request instanceof ServerHttpRequest serverHttpRequest) { return resolveToken(serverHttpRequest); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java index 19fd613f4b..3b5dfd873e 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java @@ -16,7 +16,6 @@ package io.micronaut.security.csrf.resolver; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.async.annotation.SingleResult; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.order.Ordered; @@ -39,9 +38,8 @@ public interface FutureCsrfTokenResolver extends Ordered { * @param request The Request. Maybe an HTTP Request. * @return A CSRF token or an empty Optional if the token cannot be resolved. */ - @SingleResult @NonNull - CompletableFuture resolveToken(T request); + CompletableFuture resolveToken(@NonNull T request); /** * @@ -50,8 +48,10 @@ public interface FutureCsrfTokenResolver extends Ordered { * @return Returns a List of {@link FutureCsrfTokenResolver} instances containing every reactive resolver plus the imperative resolvers adapted to imperative. * @param */ - static List> of(List> resolvers, - List> futureCsrfTokenResolvers) { + @NonNull + static List> of( + @NonNull List> resolvers, + @NonNull List> futureCsrfTokenResolvers) { List> result = new ArrayList<>(futureCsrfTokenResolvers.size() + resolvers.size()); result.addAll(futureCsrfTokenResolvers); result.addAll(resolvers.stream().map(FutureCsrfTokenResolverAdapter::new).toList()); diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java index be32e9c0bb..8bf1b6f080 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java @@ -15,6 +15,8 @@ */ package io.micronaut.security.csrf.resolver; +import io.micronaut.core.annotation.NonNull; + import java.util.concurrent.CompletableFuture; /** @@ -34,7 +36,8 @@ public FutureCsrfTokenResolverAdapter(CsrfTokenResolver csrfTokenResolver) { } @Override - public CompletableFuture resolveToken(T request) { + @NonNull + public CompletableFuture resolveToken(@NonNull T request) { return CompletableFuture.completedFuture(csrfTokenResolver.resolveToken(request).orElse(null)); } From 95bc79b62c4a4b0f1054bface8b6a063299c6f2b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:08:39 +0200 Subject: [PATCH 080/108] checkstyle: comman is not followed by a white space --- .../main/java/io/micronaut/security/csrf/filter/CsrfFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 49a660160e..d1283261fb 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -98,7 +98,7 @@ public Publisher>> csrfFilter(@NonNull HttpReque boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(HttpRequest request) { RouteMatch routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class).orElse(null); - if (routeMatch instanceof UriRouteMatch uriRouteMatch) { + if (routeMatch instanceof UriRouteMatch uriRouteMatch) { return shouldTheFilterProcessTheRequestAccordingToTheUriMatch(uriRouteMatch); } return true; From fdd4e43713eec77524ee730e3b662be95daf434a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:10:03 +0200 Subject: [PATCH 081/108] add nullability annotation to the argument --- .../csrf/repository/CompositeCsrfTokenRepository.java | 4 +++- .../security/csrf/repository/CookieCsrfTokenRepository.java | 4 +++- .../security/csrf/repository/CsrfTokenRepository.java | 4 +++- .../security/csrf/session/SessionCsrfTokenRepository.java | 4 +++- .../security/csrf/resolver/FieldCsrfTokenResolverTest.java | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java index 7195f519c2..626ef3657d 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.repository; import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; import java.util.List; @@ -40,7 +41,8 @@ public CompositeCsrfTokenRepository(List> repositories) { } @Override - public Optional findCsrfToken(T request) { + @NonNull + public Optional findCsrfToken(@NonNull T request) { return repositories.stream() .flatMap(r -> r.findCsrfToken(request).stream()) .findFirst(); diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java index 6440f1b959..b6c07d129f 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.repository; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.cookie.Cookie; @@ -44,7 +45,8 @@ public CookieCsrfTokenRepository(CsrfConfiguration csrfConfiguration) { } @Override - public Optional findCsrfToken(HttpRequest request) { + @NonNull + public Optional findCsrfToken(@NonNull HttpRequest request) { return request.getCookies() .findCookie(csrfConfiguration.getCookieName()) .map(Cookie::getValue); diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java index acc7af8c87..15a38aeebe 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.csrf.repository; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.Ordered; import java.util.Optional; @@ -30,5 +31,6 @@ public interface CsrfTokenRepository extends Ordered { * @param request Request * @return A CSRF token or an empty optional. */ - Optional findCsrfToken(T request); + @NonNull + Optional findCsrfToken(@NonNull T request); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java index 27d3286db7..3db703fd97 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.session; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.security.csrf.CsrfConfiguration; @@ -42,7 +43,8 @@ public SessionCsrfTokenRepository(CsrfConfiguration csrfConfiguration) { } @Override - public Optional findCsrfToken(HttpRequest request) { + @NonNull + public Optional findCsrfToken(@NonNull HttpRequest request) { return SessionForRequest.find(request) .flatMap(session -> session.get(csrfConfiguration.getHttpSessionName(), String.class)); } diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java index fb8f102e89..d0f64a03d3 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java @@ -4,6 +4,7 @@ import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; @@ -54,7 +55,8 @@ static class CsrfTokenRepositoryReplacement implements CsrfTokenRepository findCsrfToken(HttpRequest request) { + @NonNull + public Optional findCsrfToken(@NonNull HttpRequest request) { return Optional.of(csrfToken); } } From b83e55b93b7f06be246d53b4533b24f6b6a6a040 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:11:00 +0200 Subject: [PATCH 082/108] add nullability annotation to the second argument --- .../security/csrf/generator/CsrfHmacTokenGenerator.java | 4 ++-- .../security/csrf/generator/DefaultCsrfTokenGenerator.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java index eeb9f33627..e2e0bdaef4 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java @@ -32,9 +32,9 @@ public interface CsrfHmacTokenGenerator extends CsrfTokenGenerator { /** * Generates an HMAC. * @param request Request - * @param randomValue Cryptographic random value + * @param base64EncodedRandomValue Cryptographic random value encoded as Base64 * @return HMAC hash */ @NonNull - String hmac(@NonNull T request, String randomValue); + String hmac(@NonNull T request, @NonNull String base64EncodedRandomValue); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java index a58d65ddc1..a1ba260346 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/DefaultCsrfTokenGenerator.java @@ -74,18 +74,18 @@ public String generateCsrfToken(@NonNull T request) { /** * * @param request Request - * @param randomValue Cryptographic random value + * @param base64EncodedRandomValue Cryptographic random value encoded as Base64 * @return HMAC hash */ @Override @NonNull - public String hmac(@NonNull T request, String randomValue) { + public String hmac(@NonNull T request, @NonNull String base64EncodedRandomValue) { // Gather the values String secret = csrfConfiguration.getSecretKey(); String sessionID = sessionIdResolver.findSessionId(request).orElse(""); // Current authenticated user session // Create the CSRF Token - String message = hmacMessagePayload(sessionID, randomValue); + String message = hmacMessagePayload(sessionID, base64EncodedRandomValue); try { return StringUtils.isNotEmpty(secret) ? HMacUtils.base64EncodedHmacSha256(message, secret) // Generate the HMAC hash From 3dd3357a7a038c9fc444dd47dced95404bd86fba Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:12:28 +0200 Subject: [PATCH 083/108] extra mono optional empty to constant --- .../security/csrf/filter/CsrfFilter.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index d1283261fb..5370f50e1a 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -59,6 +59,7 @@ value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") final class CsrfFilter implements Ordered { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); + private static final Mono>> PROCEED = Mono.just(Optional.empty()); private final List>> futureCsrfTokenResolvers; private final List>> csrfTokenResolvers; private final CsrfTokenValidator> csrfTokenValidator; @@ -83,13 +84,13 @@ final class CsrfFilter implements Ordered { @Nullable public Publisher>> csrfFilter(@NonNull HttpRequest request) { if (!shouldTheFilterProcessTheRequestAccordingToTheUriMatch(request)) { - return proceedRequest(); + return PROCEED; } if (!shouldTheFilterProcessTheRequestAccordingToTheHttpMethod(request)) { - return proceedRequest(); + return PROCEED; } if (!shouldTheFilterProcessTheRequestAccordingToTheContentType(request)) { - return proceedRequest(); + return PROCEED; } return futureCsrfTokenResolvers.isEmpty() ? imperativeFilter(request) @@ -119,10 +120,6 @@ boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(String uri) { return true; } - private static Mono>> proceedRequest() { - return Mono.just(Optional.empty()); - } - private Mono>> reactiveFilter(HttpRequest request) { return Flux.fromIterable(this.futureCsrfTokenResolvers) .concatMap(resolver -> Mono.fromFuture(resolver.resolveToken(request)) @@ -136,7 +133,7 @@ private Mono>> reactiveFilter(HttpRequest req } })) .next() - .flatMap(validToken -> proceedRequest()) + .flatMap(validToken -> PROCEED) .switchIfEmpty(Mono.defer(() -> { LOG.debug("Request rejected by the CsrfFilter"); return reactiveUnauthorized(request); @@ -152,7 +149,7 @@ private Mono>> imperativeFilter(HttpRequest r return reactiveUnauthorized(request); } if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { - return proceedRequest(); + return PROCEED; } LOG.debug("Request rejected by the CSRF Filter because the CSRF Token validation failed"); return reactiveUnauthorized(request); From 6c7bd99617059194f881a0aaf7dc81ae45d07c89 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:14:20 +0200 Subject: [PATCH 084/108] add javadoc --- .../security/csrf/repository/CsrfLoginCookieProvider.java | 5 +++++ .../security/csrf/resolver/FutureCsrfTokenResolver.java | 2 +- .../security/csrf/session/SessionCsrfTokenRepository.java | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java index e90fb762ce..a4116f7451 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfLoginCookieProvider.java @@ -35,6 +35,11 @@ public class CsrfLoginCookieProvider implements LoginCookieProvider> csrfTokenGenerator; private final CsrfConfiguration csrfConfiguration; + /** + * + * @param csrfTokenGenerator CSRF Token Generator + * @param csrfConfiguration CSRF Configuration + */ public CsrfLoginCookieProvider(CsrfTokenGenerator> csrfTokenGenerator, CsrfConfiguration csrfConfiguration) { this.csrfTokenGenerator = csrfTokenGenerator; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java index 3b5dfd873e..3ee1ddafa7 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java @@ -46,7 +46,7 @@ public interface FutureCsrfTokenResolver extends Ordered { * @param resolvers Imperative CSRF Token Resolvers * @param futureCsrfTokenResolvers Reactive CSRF Token Resolvers * @return Returns a List of {@link FutureCsrfTokenResolver} instances containing every reactive resolver plus the imperative resolvers adapted to imperative. - * @param + * @param request type */ @NonNull static List> of( diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java index 3db703fd97..a77fb6a280 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java @@ -38,6 +38,10 @@ public class SessionCsrfTokenRepository implements CsrfTokenRepository> { private final CsrfConfiguration csrfConfiguration; + /** + * + * @param csrfConfiguration CSRF Configuration + */ public SessionCsrfTokenRepository(CsrfConfiguration csrfConfiguration) { this.csrfConfiguration = csrfConfiguration; } From a68d9c1f4b376048bb4fcfe59fa1b1ae03dd5415 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:14:34 +0200 Subject: [PATCH 085/108] Use HttpResponse instead of MutableHttpResponse --- .../io/micronaut/security/csrf/filter/CsrfFilter.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 5370f50e1a..bcc9f27100 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -59,7 +59,7 @@ value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") final class CsrfFilter implements Ordered { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); - private static final Mono>> PROCEED = Mono.just(Optional.empty()); + private static final Mono>> PROCEED = Mono.just(Optional.empty()); private final List>> futureCsrfTokenResolvers; private final List>> csrfTokenResolvers; private final CsrfTokenValidator> csrfTokenValidator; @@ -82,7 +82,7 @@ final class CsrfFilter implements Ordered { @RequestFilter @Nullable - public Publisher>> csrfFilter(@NonNull HttpRequest request) { + public Publisher>> csrfFilter(@NonNull HttpRequest request) { if (!shouldTheFilterProcessTheRequestAccordingToTheUriMatch(request)) { return PROCEED; } @@ -120,7 +120,7 @@ boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(String uri) { return true; } - private Mono>> reactiveFilter(HttpRequest request) { + private Mono>> reactiveFilter(HttpRequest request) { return Flux.fromIterable(this.futureCsrfTokenResolvers) .concatMap(resolver -> Mono.fromFuture(resolver.resolveToken(request)) .filter(csrfToken -> { @@ -140,7 +140,7 @@ private Mono>> reactiveFilter(HttpRequest req })); } - private Mono>> imperativeFilter(HttpRequest request) { + private Mono>> imperativeFilter(HttpRequest request) { String csrfToken = resolveCsrfToken(request); if (StringUtils.isEmpty(csrfToken)) { if (LOG.isDebugEnabled()) { @@ -201,7 +201,7 @@ private String resolveCsrfToken(@NonNull HttpRequest request) { } @NonNull - private Mono>> reactiveUnauthorized(@NonNull HttpRequest request) { + private Mono>> reactiveUnauthorized(@NonNull HttpRequest request) { return Mono.just(Optional.of(unauthorized(request))); } From 4cf98a24962c1acd9072fe691cf0b3c2b107f009 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:17:13 +0200 Subject: [PATCH 086/108] wrap these debug statements in if statements --- .../security/csrf/filter/CsrfFilter.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index bcc9f27100..8d3f2bf632 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -112,8 +112,8 @@ boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(UriRouteMatch>> reactiveFilter(HttpRequest request) { return Flux.fromIterable(this.futureCsrfTokenResolvers) .concatMap(resolver -> Mono.fromFuture(resolver.resolveToken(request)) .filter(csrfToken -> { - LOG.debug("CSRF Token resolved"); + LOG.trace("CSRF Token resolved"); if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { return true; } else { - LOG.debug("CSRF Token validation failed"); + LOG.trace("CSRF Token validation failed"); return false; } })) .next() .flatMap(validToken -> PROCEED) .switchIfEmpty(Mono.defer(() -> { - LOG.debug("Request rejected by the CsrfFilter"); + if (LOG.isDebugEnabled()) { + LOG.debug("Request rejected by the CsrfFilter"); + } return reactiveUnauthorized(request); })); } @@ -143,15 +145,17 @@ private Mono>> reactiveFilter(HttpRequest request) { private Mono>> imperativeFilter(HttpRequest request) { String csrfToken = resolveCsrfToken(request); if (StringUtils.isEmpty(csrfToken)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Request rejected by the {} because no CSRF Token found", this.getClass().getSimpleName()); + if (LOG.isTraceEnabled()) { + LOG.trace("Request rejected by the {} because no CSRF Token found", this.getClass().getSimpleName()); } return reactiveUnauthorized(request); } if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { return PROCEED; } - LOG.debug("Request rejected by the CSRF Filter because the CSRF Token validation failed"); + if (LOG.isDebugEnabled()) { + LOG.debug("Request rejected by the CSRF Filter because the CSRF Token validation failed"); + } return reactiveUnauthorized(request); } @@ -194,7 +198,7 @@ private String resolveCsrfToken(@NonNull HttpRequest request) { return tokenOptional.get(); } } - if (LOG.isDebugEnabled()) { + if (LOG.isTraceEnabled()) { LOG.trace("No CSRF token found in request"); } return null; From 533ab549d56ebbe8b859bbf8fa1de8887e3b35d1 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:18:08 +0200 Subject: [PATCH 087/108] add nullability annotation here --- .../micronaut/security/csrf/CsrfConfiguration.java | 2 ++ .../security/csrf/CsrfConfigurationProperties.java | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index 763f3dda6b..043b2ba9e0 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.Toggleable; import io.micronaut.http.cookie.CookieConfiguration; @@ -35,6 +36,7 @@ public interface CsrfConfiguration extends CookieConfiguration, Toggleable { * * @return The Secret Key that is used to calculate an HMAC as part of a CSRF token generation. */ + @Nullable String getSecretKey(); /** diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index d28b42ea08..f30bd826c4 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -77,16 +77,22 @@ final class CsrfConfigurationProperties implements CsrfConfiguration { private String fieldName = DEFAULT_FIELD_NAME; private int randomValueSize = DEFAULT_RANDOM_VALUE_SIZE; private String httpSessionName = DEFAULT_HTTP_SESSION_NAME; + + @Nullable private String cookieDomain; + private Boolean cookieSecure = DEFAULT_SECURE; private String cookiePath = DEFAULT_COOKIEPATH; private Boolean cookieHttpOnly = DEFAULT_HTTPONLY; private Duration cookieMaxAge = DEFAULT_MAX_AGE; private String cookieName = DEFAULT_COOKIE_NAME; private SameSite sameSite = DEFAULT_SAME_SITE; + + @Nullable private String signatureKey; @Override + @Nullable public String getSecretKey() { return signatureKey; } @@ -95,11 +101,12 @@ public String getSecretKey() { * The Secret Key that is used to calculate an HMAC as part of a CSRF token generation. Default Value `null`. * @param signatureKey The Secret Key that is used to calculate an HMAC as part of a CSRF token generation. */ - public void setSignatureKey(String signatureKey) { + public void setSignatureKey(@Nullable String signatureKey) { this.signatureKey = signatureKey; } @Override + @NonNull public String getHttpSessionName() { return httpSessionName; } @@ -108,7 +115,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. */ - public void setHttpSessionName(String httpSessionName) { + public void setHttpSessionName(@NonNull String httpSessionName) { this.httpSessionName = httpSessionName; } @@ -140,6 +147,7 @@ public void setHeaderName(@NonNull String headerName) { } @Override + @NonNull public String getFieldName() { return fieldName; } @@ -148,7 +156,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. */ - public void setFieldName(String fieldName) { + public void setFieldName(@NonNull String fieldName) { this.fieldName = fieldName; } From 1d756e077119e590dd0d11e5ce17fc8274314912 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:20:27 +0200 Subject: [PATCH 088/108] add nullability annotation to the argument --- .../micronaut/security/csrf/resolver/CsrfTokenResolver.java | 2 +- .../security/csrf/resolver/HttpHeaderCsrfTokenResolver.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java index 9f7652a4b6..b4c69f10cb 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/CsrfTokenResolver.java @@ -36,5 +36,5 @@ public interface CsrfTokenResolver extends Ordered { * @return A CSRF token or an empty Optional if the token cannot be resolved. */ @NonNull - Optional resolveToken(T request); + Optional resolveToken(@NonNull T request); } diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index 7774f12826..8bf47a42c2 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -17,6 +17,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpHeaders; import io.micronaut.http.HttpRequest; @@ -45,7 +46,8 @@ final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver resolveToken(HttpRequest request) { + @NonNull + public Optional resolveToken(@NonNull HttpRequest request) { final HttpHeaders httpHeaders = request.getHeaders(); String csrfToken = httpHeaders.get(headerName); if (StringUtils.isNotEmpty(csrfToken)) { From 8b81613714ce6758decac40bf52d79d7d21ae033 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:26:02 +0200 Subject: [PATCH 089/108] extract this property into a constant --- .../java/io/micronaut/security/csrf/CsrfConfiguration.java | 3 +++ .../micronaut/security/csrf/CsrfConfigurationProperties.java | 3 --- .../csrf/filter/CsrfFilterConfigurationProperties.java | 5 +++-- .../security/csrf/repository/CookieCsrfTokenRepository.java | 2 +- .../security/csrf/resolver/FieldCsrfTokenResolver.java | 2 +- .../security/csrf/resolver/HttpHeaderCsrfTokenResolver.java | 2 +- .../security/csrf/session/SessionCsrfTokenRepository.java | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java index 043b2ba9e0..7896e27517 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfiguration.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.Toggleable; import io.micronaut.http.cookie.CookieConfiguration; +import io.micronaut.security.config.SecurityConfigurationProperties; /** * CSRF Configuration. @@ -26,6 +27,8 @@ * @since 4.11.0 */ public interface CsrfConfiguration extends CookieConfiguration, Toggleable { + String PREFIX = SecurityConfigurationProperties.PREFIX + ".csrf"; + /** * * @return Random value's size in bytes. The random value used is used to build a CSRF Token. diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index f30bd826c4..88316f9984 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -20,7 +20,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.cookie.SameSite; -import io.micronaut.security.config.SecurityConfigurationProperties; import io.micronaut.security.token.generator.AccessTokenConfigurationProperties; import java.time.Duration; @@ -30,8 +29,6 @@ @Internal @ConfigurationProperties(CsrfConfigurationProperties.PREFIX) final class CsrfConfigurationProperties implements CsrfConfiguration { - public static final String PREFIX = SecurityConfigurationProperties.PREFIX + ".csrf"; - /** * The default HTTP Header name. */ diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java index b1708e7523..e06b05e3a5 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilterConfigurationProperties.java @@ -21,14 +21,15 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpMethod; import io.micronaut.http.MediaType; -import io.micronaut.security.config.SecurityConfigurationProperties; +import io.micronaut.security.csrf.CsrfConfiguration; + import java.util.Set; @Requires(classes = { HttpMethod.class, MediaType.class }) @Internal @ConfigurationProperties(CsrfFilterConfigurationProperties.PREFIX) final class CsrfFilterConfigurationProperties implements CsrfFilterConfiguration { - public static final String PREFIX = SecurityConfigurationProperties.PREFIX + ".csrf.filter"; + public static final String PREFIX = CsrfConfiguration.PREFIX + ".filter"; /** * The default enable value. diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java index b6c07d129f..4d38066ac1 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CookieCsrfTokenRepository.java @@ -31,7 +31,7 @@ * @since 4.11.0 */ @Requires(classes = HttpRequest.class) -@Requires(property = "micronaut.security.csrf.repository.cookie.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Requires(property = CsrfConfiguration.PREFIX + ".repository.cookie.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton public class CookieCsrfTokenRepository implements CsrfTokenRepository> { private final CsrfConfiguration csrfConfiguration; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 5ea432c99d..898385a570 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -31,7 +31,7 @@ * @since 2.0.0 */ @Requires(classes = HttpRequest.class) -@Requires(property = "micronaut.security.csrf.token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Requires(property = CsrfConfiguration.PREFIX + ".token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton class FieldCsrfTokenResolver implements FutureCsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java index 8bf47a42c2..3f665b62d0 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/HttpHeaderCsrfTokenResolver.java @@ -32,7 +32,7 @@ * @since 4.11.0 */ @Requires(classes = HttpRequest.class) -@Requires(property = "micronaut.security.csrf.token-resolvers.http-header.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Requires(property = CsrfConfiguration.PREFIX + ".token-resolvers.http-header.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton @Internal final class HttpHeaderCsrfTokenResolver implements CsrfTokenResolver> { diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java index a77fb6a280..047d910726 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/SessionCsrfTokenRepository.java @@ -33,7 +33,7 @@ */ @Requires(classes = HttpRequest.class) @Requires(beans = CsrfConfiguration.class) -@Requires(property = "micronaut.security.csrf.repositories.session.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Requires(property = CsrfConfiguration.PREFIX + ".repositories.session.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton public class SessionCsrfTokenRepository implements CsrfTokenRepository> { private final CsrfConfiguration csrfConfiguration; From 828b96b2e41bcbd50d54c6e13ca91a13edeaf9cf Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:26:18 +0200 Subject: [PATCH 090/108] make FieldCsrfTokenResolver --- .../security/csrf/resolver/FieldCsrfTokenResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java index 898385a570..bac3e0977d 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolver.java @@ -33,7 +33,7 @@ @Requires(classes = HttpRequest.class) @Requires(property = CsrfConfiguration.PREFIX + ".token-resolvers.field.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Singleton -class FieldCsrfTokenResolver implements FutureCsrfTokenResolver> { +final class FieldCsrfTokenResolver implements FutureCsrfTokenResolver> { private final CsrfConfiguration csrfConfiguration; private final FilterBodyParser filterBodyParser; From f8161bd7a636e2a736566b2b065a39e1e3190163 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 13:32:00 +0200 Subject: [PATCH 091/108] remove jacoco config --- security-csrf/build.gradle.kts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index d39694dabd..f4ef7999cb 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("io.micronaut.build.internal.security-module") - jacoco } dependencies { @@ -28,28 +27,3 @@ tasks.withType { micronautBuild { binaryCompatibility.enabled = false } -tasks.test { - finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run -} -tasks.jacocoTestReport { - dependsOn(tasks.test) // tests are required to run before generating the report - reports { - xml.required = false - csv.required = false - html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml")) - } -} -tasks.jacocoTestCoverageVerification { - enabled = true - violationRules { - rule { - limit { - minimum = "0".toBigDecimal() - } - } - } -} - -tasks.check { - dependsOn(tasks.jacocoTestCoverageVerification) -} From 18ff8908af6763cce2cb081d5e9f59372637b4b4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 17:27:25 +0200 Subject: [PATCH 092/108] Use static access with "io.micronaut.security.csrf.CsrfConfiguration" for "PREFIX" --- .../io/micronaut/security/csrf/CsrfConfigurationProperties.java | 2 +- .../src/main/java/io/micronaut/security/csrf/package-info.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java index 88316f9984..4dc974b3ef 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/CsrfConfigurationProperties.java @@ -27,7 +27,7 @@ import java.util.Optional; @Internal -@ConfigurationProperties(CsrfConfigurationProperties.PREFIX) +@ConfigurationProperties(CsrfConfiguration.PREFIX) final class CsrfConfigurationProperties implements CsrfConfiguration { /** * The default HTTP Header name. diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java index 28844d4a6c..33516fbc13 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/package-info.java @@ -20,7 +20,7 @@ * @author Sergio del Amo * @since 4.11.0 */ -@Requires(property = CsrfConfigurationProperties.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) +@Requires(property = CsrfConfiguration.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Configuration package io.micronaut.security.csrf; From 4f2c1180f9296b9d7a8424f57a75d63e4182a9dc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 17:28:37 +0200 Subject: [PATCH 093/108] add UnsupportedOperationException --- .../security/csrf/filter/CsrfFilterTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java index 6418cbe563..134af5625c 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/filter/CsrfFilterTest.java @@ -115,17 +115,18 @@ public Map getVariableMap() { @Override public void fulfill(Map argumentValues) { - + throw new UnsupportedOperationException(); } @Override public void fulfillBeforeFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) { + throw new UnsupportedOperationException(); } @Override public void fulfillAfterFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) { - + throw new UnsupportedOperationException(); } @Override @@ -135,17 +136,17 @@ public boolean isFulfilled() { @Override public Optional> getRequiredInput(String name) { - return Optional.empty(); + throw new UnsupportedOperationException(); } @Override public Object execute() { - return null; + throw new UnsupportedOperationException(); } @Override public boolean isSatisfied(String name) { - return false; + throw new UnsupportedOperationException(); } }; } From 3f89d926acfa767fc2af39723d96231e16b3be66 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 25 Oct 2024 17:33:20 +0200 Subject: [PATCH 094/108] sonnar issues --- .../csrf/resolver/FieldCsrfTokenResolverTest.java | 2 -- .../endpoint/token/response/IdTokenLoginHandler.java | 2 +- .../security/session/DefaultSessionPopulator.java | 10 ++++++++++ .../security/token/cookie/TokenCookieLoginHandler.java | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java index d0f64a03d3..471f8dab61 100644 --- a/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java +++ b/security-csrf/src/test/java/io/micronaut/security/csrf/resolver/FieldCsrfTokenResolverTest.java @@ -2,7 +2,6 @@ import io.micronaut.context.BeanContext; import io.micronaut.context.annotation.Property; -import io.micronaut.context.annotation.Replaces; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpMethod; @@ -20,7 +19,6 @@ import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; -import jakarta.inject.Singleton; import org.junit.jupiter.api.Test; import java.util.Optional; diff --git a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java index 87b01bcac1..777847b168 100644 --- a/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java +++ b/security-oauth2/src/main/java/io/micronaut/security/oauth2/endpoint/token/response/IdTokenLoginHandler.java @@ -86,7 +86,7 @@ public IdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfi * @param priorToLoginPersistence The prior to login persistence strategy * @deprecated Use {@link #IdTokenLoginHandler(AccessTokenCookieConfiguration, RedirectConfiguration, RedirectService, PriorToLoginPersistence, List)} instead. */ - @Deprecated + @Deprecated(forRemoval = true, since = "4.11.0") public IdTokenLoginHandler(AccessTokenCookieConfiguration accessTokenCookieConfiguration, RedirectConfiguration redirectConfiguration, RedirectService redirectService, diff --git a/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java b/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java index 315ad4c667..f5c7949624 100644 --- a/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java +++ b/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java @@ -21,8 +21,18 @@ import io.micronaut.session.Session; import jakarta.inject.Singleton; +/** + * Default Implementation of {@link SessionPopulator}. It adds the {@link Authentication} object to the session with the key {@link SecurityFilter#AUTHENTICATION}. + * @param Request + */ @Singleton public class DefaultSessionPopulator implements SessionPopulator { + /** + * Adds the {@link Authentication} object to the session with the key {@link SecurityFilter#AUTHENTICATION}. + * @param request The request + * @param authentication The authenticated user. + * @param session The session + */ @Override public void populateSession(T request, @NonNull Authentication authentication, @NonNull Session session) { session.put(SecurityFilter.AUTHENTICATION, authentication); diff --git a/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java b/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java index c3ab3e15c6..31370997cf 100644 --- a/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java +++ b/security/src/main/java/io/micronaut/security/token/cookie/TokenCookieLoginHandler.java @@ -90,7 +90,7 @@ public TokenCookieLoginHandler(RedirectService redirectService, * @param priorToLoginPersistence Prior To Login Persistence Mechanism * @deprecated Use {@link TokenCookieLoginHandler#TokenCookieLoginHandler(RedirectService, RedirectConfiguration, AccessTokenCookieConfiguration, RefreshTokenCookieConfiguration, AccessTokenConfiguration, AccessRefreshTokenGenerator, PriorToLoginPersistence, List)} instead. */ - @Deprecated + @Deprecated(forRemoval = true, since = "4.11.0") public TokenCookieLoginHandler(RedirectService redirectService, RedirectConfiguration redirectConfiguration, AccessTokenCookieConfiguration accessTokenCookieConfiguration, From aba5267a2e31ba97c5c788c966cc8356aeff7a2c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:10:07 +0100 Subject: [PATCH 095/108] =?UTF-8?q?don=E2=80=99t=20use=20reactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use CompletableFuture<@Nullable HttpResponse> --- security-csrf/build.gradle.kts | 1 - .../security/csrf/filter/CsrfFilter.java | 54 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/security-csrf/build.gradle.kts b/security-csrf/build.gradle.kts index f4ef7999cb..ba286f32ac 100644 --- a/security-csrf/build.gradle.kts +++ b/security-csrf/build.gradle.kts @@ -6,7 +6,6 @@ dependencies { api(projects.micronautSecurity) compileOnly(mn.micronaut.http.server) compileOnly(projects.micronautSecuritySession) - implementation(mnReactor.micronaut.reactor) testAnnotationProcessor(mn.micronaut.inject.java) testImplementation(mnTest.micronaut.test.junit5) testRuntimeOnly(libs.junit.jupiter.engine) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java index 8d3f2bf632..d864a01b09 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/filter/CsrfFilter.java @@ -36,14 +36,11 @@ import io.micronaut.security.filters.SecurityFilter; import io.micronaut.web.router.RouteMatch; import io.micronaut.web.router.UriRouteMatch; -import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * {@link RequestFilter} which validates CSRF tokens and rejects a request if the token is invalid. @@ -59,7 +56,7 @@ value = "${" + CsrfFilterConfigurationProperties.PREFIX + ".regex-pattern:" + CsrfFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}") final class CsrfFilter implements Ordered { private static final Logger LOG = LoggerFactory.getLogger(CsrfFilter.class); - private static final Mono>> PROCEED = Mono.just(Optional.empty()); + private static final CompletableFuture<@Nullable HttpResponse> PROCEED = CompletableFuture.completedFuture(null); private final List>> futureCsrfTokenResolvers; private final List>> csrfTokenResolvers; private final CsrfTokenValidator> csrfTokenValidator; @@ -82,7 +79,8 @@ final class CsrfFilter implements Ordered { @RequestFilter @Nullable - public Publisher>> csrfFilter(@NonNull HttpRequest request) { + + public CompletableFuture<@Nullable HttpResponse> csrfFilter(@NonNull HttpRequest request) { if (!shouldTheFilterProcessTheRequestAccordingToTheUriMatch(request)) { return PROCEED; } @@ -120,29 +118,31 @@ boolean shouldTheFilterProcessTheRequestAccordingToTheUriMatch(String uri) { return true; } - private Mono>> reactiveFilter(HttpRequest request) { - return Flux.fromIterable(this.futureCsrfTokenResolvers) - .concatMap(resolver -> Mono.fromFuture(resolver.resolveToken(request)) - .filter(csrfToken -> { - LOG.trace("CSRF Token resolved"); - if (csrfTokenValidator.validateCsrfToken(request, csrfToken)) { - return true; - } else { - LOG.trace("CSRF Token validation failed"); - return false; + private CompletableFuture<@Nullable HttpResponse> reactiveFilter(HttpRequest request) { + List> futures = futureCsrfTokenResolvers.stream() + .map(resolver -> resolver.resolveToken(request) + .thenApply(csrfToken -> { + if (LOG.isTraceEnabled()) { + LOG.trace("CSRF Token resolved"); } - })) - .next() - .flatMap(validToken -> PROCEED) - .switchIfEmpty(Mono.defer(() -> { - if (LOG.isDebugEnabled()) { - LOG.debug("Request rejected by the CsrfFilter"); + return csrfTokenValidator.validateCsrfToken(request, csrfToken); + }) + ) + .toList(); + CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[0]); + return CompletableFuture.allOf(futuresArray) + .thenApply(v -> futures.stream().map(CompletableFuture::join).toList()) + .thenApply(validations -> { + if (validations.stream().anyMatch(Boolean::booleanValue)) { + return null; + } else if (LOG.isTraceEnabled()) { + LOG.trace("CSRF Token validation failed"); } - return reactiveUnauthorized(request); - })); + return unauthorized(request); + }); } - private Mono>> imperativeFilter(HttpRequest request) { + private CompletableFuture<@Nullable HttpResponse> imperativeFilter(HttpRequest request) { String csrfToken = resolveCsrfToken(request); if (StringUtils.isEmpty(csrfToken)) { if (LOG.isTraceEnabled()) { @@ -205,8 +205,8 @@ private String resolveCsrfToken(@NonNull HttpRequest request) { } @NonNull - private Mono>> reactiveUnauthorized(@NonNull HttpRequest request) { - return Mono.just(Optional.of(unauthorized(request))); + private CompletableFuture<@Nullable HttpResponse> reactiveUnauthorized(@NonNull HttpRequest request) { + return CompletableFuture.completedFuture(unauthorized(request)); } @NonNull From bb937ad8843cc61898803ff16ab502ecfcc6f05b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:21:00 +0100 Subject: [PATCH 096/108] Use CollectionUtils::concat --- .../csrf/resolver/FutureCsrfTokenResolver.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java index 3ee1ddafa7..d6613842eb 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolver.java @@ -18,8 +18,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.OrderUtil; import io.micronaut.core.order.Ordered; - -import java.util.ArrayList; +import io.micronaut.core.util.CollectionUtils; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -52,9 +51,10 @@ public interface FutureCsrfTokenResolver extends Ordered { static List> of( @NonNull List> resolvers, @NonNull List> futureCsrfTokenResolvers) { - List> result = new ArrayList<>(futureCsrfTokenResolvers.size() + resolvers.size()); - result.addAll(futureCsrfTokenResolvers); - result.addAll(resolvers.stream().map(FutureCsrfTokenResolverAdapter::new).toList()); + List> result = CollectionUtils.concat(futureCsrfTokenResolvers, + resolvers.stream() + .map(resolver -> (FutureCsrfTokenResolver) new FutureCsrfTokenResolverAdapter<>(resolver)) + .toList()); OrderUtil.sort(result); return result; } From 2fb98bfc6f69e57d712f67a92baa7bb0d1817838 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:22:39 +0100 Subject: [PATCH 097/108] FutureCsrfTokenResolverAdapter pck private & final --- .../security/csrf/resolver/FutureCsrfTokenResolverAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java index 8bf1b6f080..0ed16f8d14 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/resolver/FutureCsrfTokenResolverAdapter.java @@ -23,7 +23,7 @@ * Adapter from {@link CsrfTokenResolver} to {@link FutureCsrfTokenResolver}. * @param Request */ -public class FutureCsrfTokenResolverAdapter implements FutureCsrfTokenResolver { +final class FutureCsrfTokenResolverAdapter implements FutureCsrfTokenResolver { private final CsrfTokenResolver csrfTokenResolver; From c70e6e6b27e893330bf0f89b7982d3c4bbc0477f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:23:20 +0100 Subject: [PATCH 098/108] CsrfSessionPopulator pck private & final annotate with @Internal as well. --- .../micronaut/security/csrf/session/CsrfSessionPopulator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java index 2c95a036d0..c973a0b31c 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/CsrfSessionPopulator.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.csrf.session; +import io.micronaut.core.annotation.Internal; import io.micronaut.security.authentication.Authentication; import io.micronaut.security.csrf.CsrfConfiguration; import io.micronaut.security.csrf.generator.CsrfTokenGenerator; @@ -29,7 +30,8 @@ * @param Request */ @Singleton -public class CsrfSessionPopulator implements SessionPopulator { +@Internal +final class CsrfSessionPopulator implements SessionPopulator { private final CsrfConfiguration csrfConfiguration; private final CsrfTokenGenerator csrfTokenGenerator; From b6cd612e0dc43762dd6f84fb6ddda118a3eabd11 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:24:57 +0100 Subject: [PATCH 099/108] HttpSessionSessionIdResolver pck private & final annotate with @Internal --- .../security/csrf/session/HttpSessionSessionIdResolver.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java b/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java index 999537504e..febd955402 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.csrf.session; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequest; import io.micronaut.security.session.SessionIdResolver; @@ -29,8 +30,9 @@ * @author Sergio del Amo * @since 4.11.0 */ +@Internal @Singleton -public class HttpSessionSessionIdResolver implements SessionIdResolver> { +final class HttpSessionSessionIdResolver implements SessionIdResolver> { @Override @NonNull public Optional findSessionId(@NonNull HttpRequest request) { From 816466c0eaa566f1078f6682922b2cb984aaab0f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:25:38 +0100 Subject: [PATCH 100/108] RepositoryCsrfTokenValidator pck private & final annotate with @Internal --- .../security/csrf/validator/RepositoryCsrfTokenValidator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java index 11cff3e574..2da356045a 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/validator/RepositoryCsrfTokenValidator.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.validator; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; import io.micronaut.security.csrf.generator.CsrfHmacTokenGenerator; import io.micronaut.security.csrf.repository.CsrfTokenRepository; import jakarta.inject.Singleton; @@ -35,8 +36,9 @@ * @author Sergio del Amo */ @Requires(beans = { CsrfTokenRepository.class, CsrfHmacTokenGenerator.class}) +@Internal @Singleton -public class RepositoryCsrfTokenValidator implements CsrfTokenValidator { +class RepositoryCsrfTokenValidator implements CsrfTokenValidator { private static final Logger LOG = LoggerFactory.getLogger(RepositoryCsrfTokenValidator.class); private final List> repositories; private final CsrfHmacTokenGenerator defaultCsrfTokenGenerator; From 37517d30649548afe82a673f58222520b289707a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:26:44 +0100 Subject: [PATCH 101/108] JsonWebTokenIdSessionIdResolver pkg private and final --- .../token/jwt/validator/JsonWebTokenIdSessionIdResolver.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java index 537164aaeb..800ab3b565 100644 --- a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java +++ b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java @@ -16,6 +16,7 @@ package io.micronaut.security.token.jwt.validator; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequest; import io.micronaut.security.session.SessionIdResolver; @@ -35,7 +36,8 @@ @Requires(classes = HttpRequest.class) @Requires(bean = JsonWebTokenParser.class) @Singleton -public class JsonWebTokenIdSessionIdResolver implements SessionIdResolver> { +@Internal +final class JsonWebTokenIdSessionIdResolver implements SessionIdResolver> { private final JsonWebTokenParser jsonWebTokenParser; public JsonWebTokenIdSessionIdResolver(JsonWebTokenParser jsonWebTokenParser) { From a89d437121df06ddad0ecceb7f7f09e8fbfa149c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:27:32 +0100 Subject: [PATCH 102/108] DefaultSessionPopulator package private and final annotate also with @Internal --- .../micronaut/security/session/DefaultSessionPopulator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java b/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java index f5c7949624..467b08a789 100644 --- a/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java +++ b/security-session/src/main/java/io/micronaut/security/session/DefaultSessionPopulator.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.session; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.security.authentication.Authentication; import io.micronaut.security.filters.SecurityFilter; @@ -26,7 +27,8 @@ * @param Request */ @Singleton -public class DefaultSessionPopulator implements SessionPopulator { +@Internal +final class DefaultSessionPopulator implements SessionPopulator { /** * Adds the {@link Authentication} object to the session with the key {@link SecurityFilter#AUTHENTICATION}. * @param request The request From f2e202523ebe734213e7c6f7a8aa62eae0ba60f9 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:28:10 +0100 Subject: [PATCH 103/108] CompositeSessionIdResolver final and @Internal --- .../security/session/CompositeSessionIdResolver.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java index b4aa6a9cf0..4304e045fa 100644 --- a/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java +++ b/security/src/main/java/io/micronaut/security/session/CompositeSessionIdResolver.java @@ -16,6 +16,7 @@ package io.micronaut.security.session; import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; import java.util.List; @@ -26,9 +27,10 @@ * @see Composite Pattern * @param Request */ +@Internal @Primary @Singleton -public class CompositeSessionIdResolver implements SessionIdResolver { +final class CompositeSessionIdResolver implements SessionIdResolver { private final List> sessionIdResolvers; From 37a8bc0f1a5c3219ac50fc70ffc3596dca695e75 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:28:58 +0100 Subject: [PATCH 104/108] CompositeCsrfTokenRepository final and internal --- .../csrf/repository/CompositeCsrfTokenRepository.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java index 626ef3657d..35761a2749 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CompositeCsrfTokenRepository.java @@ -16,6 +16,7 @@ package io.micronaut.security.csrf.repository; import io.micronaut.context.annotation.Primary; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; @@ -27,9 +28,10 @@ * @see Composite Pattern * @param Request */ +@Internal @Primary @Singleton -public class CompositeCsrfTokenRepository implements CsrfTokenRepository { +final class CompositeCsrfTokenRepository implements CsrfTokenRepository { private final List> repositories; /** From 7d4ecda7a35b536ced1219888c96ea9c644e60b3 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:36:16 +0100 Subject: [PATCH 105/108] make possible to disable sessionidresolvers --- .../JsonWebTokenIdSessionIdResolver.java | 2 ++ ...JsonWebTokenIdSessionIdResolverSpec.groovy | 21 ++++++++++++++++++ .../session/HttpSessionSessionIdResolver.java | 6 +++-- .../HttpSessionSessionIdResolverSpec.groovy | 22 +++++++++++++++++++ .../security/session/SessionIdResolver.java | 5 +++++ 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 security-jwt/src/test/groovy/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolverSpec.groovy rename {security-csrf/src/main/java/io/micronaut/security/csrf => security-session/src/main/java/io/micronaut/security}/session/HttpSessionSessionIdResolver.java (83%) create mode 100644 security-session/src/test/groovy/io/micronaut/docs/security/session/HttpSessionSessionIdResolverSpec.groovy diff --git a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java index 800ab3b565..ea01e168fb 100644 --- a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java +++ b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolver.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.security.session.SessionIdResolver; import jakarta.inject.Singleton; @@ -33,6 +34,7 @@ * @since 4.11.0 * @author Sergio del Amo */ +@Requires(property = SessionIdResolver.PREFIX + ".jwt-id.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Requires(classes = HttpRequest.class) @Requires(bean = JsonWebTokenParser.class) @Singleton diff --git a/security-jwt/src/test/groovy/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolverSpec.groovy b/security-jwt/src/test/groovy/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolverSpec.groovy new file mode 100644 index 0000000000..c99a2542ca --- /dev/null +++ b/security-jwt/src/test/groovy/io/micronaut/security/token/jwt/validator/JsonWebTokenIdSessionIdResolverSpec.groovy @@ -0,0 +1,21 @@ +package io.micronaut.security.token.jwt.validator + +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.Property +import io.micronaut.core.util.StringUtils +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@Property(name = "micronaut.security.sessionid-resolver.jwt-id.enabled", value = StringUtils.FALSE) +@MicronautTest(startApplication = false) +class JsonWebTokenIdSessionIdResolverSpec extends Specification { + + @Inject + BeanContext beanContext + + void "it is possible to disable JsonWebTokenIdSessionIdResolver"() { + expect: + !beanContext.containsBean(JsonWebTokenIdSessionIdResolver) + } +} \ No newline at end of file diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java b/security-session/src/main/java/io/micronaut/security/session/HttpSessionSessionIdResolver.java similarity index 83% rename from security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java rename to security-session/src/main/java/io/micronaut/security/session/HttpSessionSessionIdResolver.java index febd955402..c84bb8497a 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/session/HttpSessionSessionIdResolver.java +++ b/security-session/src/main/java/io/micronaut/security/session/HttpSessionSessionIdResolver.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.security.csrf.session; +package io.micronaut.security.session; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; -import io.micronaut.security.session.SessionIdResolver; import io.micronaut.session.Session; import io.micronaut.session.http.SessionForRequest; import jakarta.inject.Singleton; @@ -30,6 +31,7 @@ * @author Sergio del Amo * @since 4.11.0 */ +@Requires(property = SessionIdResolver.PREFIX + ".httpsession-id.enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Internal @Singleton final class HttpSessionSessionIdResolver implements SessionIdResolver> { diff --git a/security-session/src/test/groovy/io/micronaut/docs/security/session/HttpSessionSessionIdResolverSpec.groovy b/security-session/src/test/groovy/io/micronaut/docs/security/session/HttpSessionSessionIdResolverSpec.groovy new file mode 100644 index 0000000000..54cee18173 --- /dev/null +++ b/security-session/src/test/groovy/io/micronaut/docs/security/session/HttpSessionSessionIdResolverSpec.groovy @@ -0,0 +1,22 @@ +package io.micronaut.docs.security.session + +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.Property +import io.micronaut.core.util.StringUtils +import io.micronaut.security.session.HttpSessionSessionIdResolver +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@Property(name = "micronaut.security.sessionid-resolver.httpsession-id.enabled", value = StringUtils.FALSE) +@MicronautTest(startApplication = false) +class HttpSessionSessionIdResolverSpec extends Specification { + + @Inject + BeanContext beanContext + + void "it is possible to disable JsonWebTokenIdSessionIdResolver"() { + expect: + !beanContext.containsBean(HttpSessionSessionIdResolver) + } +} \ No newline at end of file diff --git a/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java index d9563f2783..697a4a21f0 100644 --- a/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java +++ b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.Ordered; +import io.micronaut.security.config.SecurityConfigurationProperties; import java.util.Optional; @@ -28,6 +29,10 @@ * @param Request */ public interface SessionIdResolver extends Ordered { + /** + * Prefix used in SessionID resolver implementation.s. + */ + String PREFIX = SecurityConfigurationProperties.PREFIX + ".sessionid-resolver"; /** * From 41cffe0b5592d194d3003e79ea30fa5a944dafa6 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 11:46:18 +0100 Subject: [PATCH 106/108] test HttpSessionSessionIdResolver --- ...SessionAuthenticationNoRedirectSpec.groovy | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy index 270d8c7455..049c790e21 100644 --- a/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy +++ b/security-session/src/test/groovy/io/micronaut/docs/security/session/SessionAuthenticationNoRedirectSpec.groovy @@ -4,12 +4,16 @@ import io.micronaut.context.annotation.Requires import io.micronaut.core.annotation.Nullable import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.cookie.Cookie import io.micronaut.security.annotation.Secured +import io.micronaut.security.rules.SecurityRule +import io.micronaut.security.session.SessionIdResolver import io.micronaut.security.testutils.EmbeddedServerSpecification import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario @@ -77,6 +81,23 @@ class SessionAuthenticationNoRedirectSpec extends EmbeddedServerSpecification { rsp.body() rsp.body().contains('sherlock') + when: + String sessionIdInResolver = client.retrieve(HttpRequest.GET('/session/id') + .accept(MediaType.TEXT_PLAIN) + .cookie(Cookie.of('SESSION', sessionId))) + + then: + noExceptionThrown() + sessionIdInResolver + + when: + client.exchange(HttpRequest.GET('/session/id') + .accept(MediaType.TEXT_PLAIN)) + + then: + HttpClientResponseException ex = thrown() + HttpStatus.NOT_FOUND == ex.status + when: HttpRequest logoutRequest = HttpRequest.POST('/logout', "").cookie(Cookie.of('SESSION', sessionId)) HttpResponse logoutRsp = client.exchange(logoutRequest, String) @@ -106,10 +127,23 @@ class SessionAuthenticationNoRedirectSpec extends EmbeddedServerSpecification { @Secured("isAnonymous()") @Controller("/") static class HomeController { + private final SessionIdResolver> sessionIdResolver + + HomeController(SessionIdResolver> sessionIdResolver) { + this.sessionIdResolver = sessionIdResolver + } + @Produces(MediaType.TEXT_PLAIN) @Get String index(@Nullable Principal principal) { return principal?.name ?: 'You are not logged in' } + + @Secured(SecurityRule.IS_ANONYMOUS) + @Produces(MediaType.TEXT_PLAIN) + @Get("/session/id") + Optional index(HttpRequest request) { + return sessionIdResolver.findSessionId(request) + } } } From 3c6632611be7c2d4c19f9ea96e3e8d3a43929f29 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 15:04:51 +0100 Subject: [PATCH 107/108] annotate it with @Internal --- .../security/csrf/generator/CsrfHmacTokenGenerator.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java index e2e0bdaef4..346fea95d4 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/generator/CsrfHmacTokenGenerator.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.csrf.generator; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; /** @@ -23,6 +24,7 @@ * @since 4.11.0 * @param request */ +@Internal public interface CsrfHmacTokenGenerator extends CsrfTokenGenerator { /** * Dot is used as separator between the HMAC and the random value. As the random value and hmac are base64 encoded, they will not contain a dot. From aafcefb23e3ee3aaceb60a961b7bd22e9bfb8c75 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 28 Oct 2024 15:07:37 +0100 Subject: [PATCH 108/108] ann interfaces with composite pattern with @Indexed --- .../security/csrf/repository/CsrfTokenRepository.java | 2 ++ .../java/io/micronaut/security/session/SessionIdResolver.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java index 15a38aeebe..01faa96bed 100644 --- a/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java +++ b/security-csrf/src/main/java/io/micronaut/security/csrf/repository/CsrfTokenRepository.java @@ -15,6 +15,7 @@ */ package io.micronaut.security.csrf.repository; +import io.micronaut.core.annotation.Indexed; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.Ordered; @@ -24,6 +25,7 @@ * Repository API for CSRF Tokens. * @param Request */ +@Indexed(CsrfTokenRepository.class) @FunctionalInterface public interface CsrfTokenRepository extends Ordered { /** diff --git a/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java index 697a4a21f0..0a6ea05189 100644 --- a/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java +++ b/security/src/main/java/io/micronaut/security/session/SessionIdResolver.java @@ -16,8 +16,10 @@ package io.micronaut.security.session; +import io.micronaut.core.annotation.Indexed; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.order.Ordered; +import io.micronaut.data.annotation.Index; import io.micronaut.security.config.SecurityConfigurationProperties; import java.util.Optional; @@ -28,6 +30,7 @@ * @since 4.11.0 * @param Request */ +@Indexed(SessionIdResolver.class) public interface SessionIdResolver extends Ordered { /** * Prefix used in SessionID resolver implementation.s.