From e528923878094e4ee58c25d9b82e67821fe3e515 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 03:08:32 +0000 Subject: [PATCH 01/42] Bump io.projectreactor:reactor-bom from 2023.0.12 to 2023.0.13 Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.12 to 2023.0.13. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2023.0.12...2023.0.13) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 1f4477c3364..a4dc106d863 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11" commons-collections = "commons-collections:commons-collections:3.2.2" io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.13" io-mockk = "io.mockk:mockk:1.13.13" -io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.12" +io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.13" io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" } io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" } io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" } From d9e9e3cdeb2569b825cae2dddc1b8fe7e8bd4835 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 03:18:56 +0000 Subject: [PATCH 02/42] Bump org.springframework.data:spring-data-bom from 2024.0.6 to 2024.0.7 Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.0.6 to 2024.0.7. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.0.6...2024.0.7) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 400c7aed5c5..c2be1c30ec8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -84,7 +84,7 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3" org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36" org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16" -org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.6" +org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.7" org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.10" org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" } org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0" From 57a06ead66eb81984a885cfa1597184f0c3778da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 03:34:54 +0000 Subject: [PATCH 03/42] Bump org.springframework.data:spring-data-bom from 2024.1.0 to 2024.1.1 Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.1.0 to 2024.1.1. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.1.0...2024.1.1) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 50aa9d233e5..8e8404600aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -88,7 +88,7 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3" org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36" org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16" -org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.1.0" +org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.1.1" org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.10" org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" } org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0" From 85ca9e9a578808a2106ada77de154bc87b027278 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Dec 2024 15:23:29 +0000 Subject: [PATCH 04/42] Release 6.3.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2b893bd4417..0af50ab2e5c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ springBootVersion=3.1.1 -version=6.3.6-SNAPSHOT +version=6.3.6 samplesBranch=6.3.x org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From 87888d42fc7fd633accb745cdd8119c7679ca603 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Dec 2024 15:50:41 +0000 Subject: [PATCH 05/42] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0af50ab2e5c..cd27f59f3ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ springBootVersion=3.1.1 -version=6.3.6 +version=6.3.7-SNAPSHOT samplesBranch=6.3.x org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From 5fe6d9259fbee532d402a801527b7aed4d937e98 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Dec 2024 15:58:35 +0000 Subject: [PATCH 06/42] Release 6.4.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9aebea48a2e..463d88a1019 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # springBootVersion=3.3.3 -version=6.4.2-SNAPSHOT +version=6.4.2 samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From ac0ca0cafcbb1f5e9230b2aadb83119103eb3b1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Dec 2024 16:36:38 +0000 Subject: [PATCH 07/42] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 463d88a1019..7ea67c6b3ac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # springBootVersion=3.3.3 -version=6.4.2 +version=6.4.3-SNAPSHOT samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From 5610b35a2f7ecff6231d6c96baa20d98db67254c Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:10:38 -0600 Subject: [PATCH 08/42] Ignore updates to org.apache.directory.shared Related gh-16277 [skip ci] --- .github/dependabot.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 23736cf2c83..4082d7441c8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,6 +19,7 @@ updates: - dependency-name: com.nimbusds:nimbus-jose-jwt - dependency-name: org.python:jython - dependency-name: org.apache.directory.server:* + - dependency-name: org.apache.directory.shared:* - dependency-name: org.junit:junit-bom update-types: - version-update:semver-major @@ -44,6 +45,7 @@ updates: - dependency-name: com.nimbusds:nimbus-jose-jwt - dependency-name: org.python:jython - dependency-name: org.apache.directory.server:* + - dependency-name: org.apache.directory.shared:* - dependency-name: org.junit:junit-bom update-types: - version-update:semver-major @@ -69,6 +71,7 @@ updates: - dependency-name: com.nimbusds:nimbus-jose-jwt - dependency-name: org.python:jython - dependency-name: org.apache.directory.server:* + - dependency-name: org.apache.directory.shared:* - dependency-name: org.junit:junit-bom update-types: - version-update:semver-major From bf6a2fab01d7f1e7622ddef424a456dc98d640e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:12:57 +0000 Subject: [PATCH 09/42] Bump org.junit:junit-bom from 5.11.3 to 5.11.4 Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.3 to 5.11.4. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.3...r5.11.4) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 8e8404600aa..e79f42a258c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,7 +75,7 @@ org-hsqldb = "org.hsqldb:hsqldb:2.7.4" org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25" org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" } -org-junit-junit-bom = "org.junit:junit-bom:5.11.3" +org-junit-junit-bom = "org.junit:junit-bom:5.11.4" org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" } org-opensaml-opensaml-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml" } org-opensaml-opensaml-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml" } From 036f6f27bfbf627bde2ac7d34f4f17fe7cf97aae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:38:18 +0000 Subject: [PATCH 10/42] Bump org.springframework.data:spring-data-bom from 2024.1.0 to 2024.1.1 Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.1.0 to 2024.1.1. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.1.0...2024.1.1) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 50aa9d233e5..8e8404600aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -88,7 +88,7 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3" org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36" org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16" -org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.1.0" +org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.1.1" org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.10" org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" } org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0" From 12a6431496cf940642744d73d10ccc8963a9595a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 00:25:49 +0000 Subject: [PATCH 11/42] Bump org.junit:junit-bom from 5.11.3 to 5.11.4 Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.3 to 5.11.4. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.3...r5.11.4) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 8e8404600aa..e79f42a258c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,7 +75,7 @@ org-hsqldb = "org.hsqldb:hsqldb:2.7.4" org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25" org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" } -org-junit-junit-bom = "org.junit:junit-bom:5.11.3" +org-junit-junit-bom = "org.junit:junit-bom:5.11.4" org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" } org-opensaml-opensaml-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml" } org-opensaml-opensaml-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml" } From f45cc22e11e1a8e35f4717ad0055197642ff5371 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Wed, 20 Nov 2024 06:09:14 +0200 Subject: [PATCH 12/42] Allow configuring custom ServerHttpHeadersWriter for Kotlin DSL Closes gh-16009 --- .../config/web/server/ServerHeadersDsl.kt | 17 ++++++- .../web/server/ServerHeadersDslTests.kt | 47 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt index 37bd1f177a9..86289744439 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.security.config.web.server import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter +import org.springframework.security.web.server.header.ServerHttpHeadersWriter import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter @@ -43,6 +44,7 @@ class ServerHeadersDsl { private var crossOriginOpenerPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginOpenerPolicySpec) -> Unit)? = null private var crossOriginEmbedderPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginEmbedderPolicySpec) -> Unit)? = null private var crossOriginResourcePolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginResourcePolicySpec) -> Unit)? = null + private var writers = mutableListOf() private var disabled = false @@ -198,6 +200,16 @@ class ServerHeadersDsl { this.crossOriginResourcePolicy = ServerCrossOriginResourcePolicyDsl().apply(crossOriginResourcePolicyConfig).get() } + /** + * Configures custom headers writer + * + * @since 6.5 + * @param writer the [ServerHttpHeadersWriter] to provide custom headers writer + */ + fun writer(writer: ServerHttpHeadersWriter) { + this.writers.add(writer) + } + /** * Disables HTTP response headers. */ @@ -244,6 +256,9 @@ class ServerHeadersDsl { crossOriginResourcePolicy?.also { headers.crossOriginResourcePolicy(crossOriginResourcePolicy) } + writers.also { + writers.forEach { writer -> headers.writer(writer) } + } if (disabled) { headers.disable() } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt index dfa78726cac..78bfd4d57d4 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.springframework.security.web.server.header.XFrameOptionsServerHttpHea import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono /** * Tests for [ServerHeadersDsl] @@ -198,4 +199,48 @@ class ServerHeadersDslTests { } } } + + @Test + fun `request when custom server http headers writer configured then custom http headers added`() { + this.spring.register(ServerHttpHeadersWriterCustomConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("CUSTOM-HEADER-1", "CUSTOM-VALUE-1") + .expectHeader().valueEquals("CUSTOM-HEADER-2", "CUSTOM-VALUE-2") + } + + @Configuration + @EnableWebFluxSecurity + @EnableWebFlux + open class ServerHttpHeadersWriterCustomConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + writer { exchange -> + Mono.just(exchange) + .doOnNext { + it.response.headers.add( + "CUSTOM-HEADER-1", + "CUSTOM-VALUE-1" + ) + } + .then() + } + writer { exchange -> + Mono.just(exchange) + .doOnNext { + it.response.headers.add( + "CUSTOM-HEADER-2", + "CUSTOM-VALUE-2" + ) + } + .then() + } + } + } + } + } } From b9f3a28678af525e70cd5aa00abc816163cdb4fe Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 23 Oct 2024 18:06:48 -0600 Subject: [PATCH 13/42] Add UserDetailsService Constructor Closes gh-15973 --- ...alizeUserDetailsBeanManagerConfigurer.java | 8 +--- .../AbstractDaoAuthenticationConfigurer.java | 4 +- .../AuthenticationManagerFactoryBean.java | 3 +- .../dao/DaoAuthenticationProvider.java | 37 ++++++++++++++----- .../authentication/passwords/index.adoc | 9 ++--- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index 7320840d9b3..b23810c48e9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -95,14 +95,10 @@ else if (beanNames.length > 1) { PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class); CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class); - DaoAuthenticationProvider provider; + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); if (passwordEncoder != null) { - provider = new DaoAuthenticationProvider(passwordEncoder); + provider.setPasswordEncoder(passwordEncoder); } - else { - provider = new DaoAuthenticationProvider(); - } - provider.setUserDetailsService(userDetailsService); if (passwordManager != null) { provider.setUserDetailsPasswordService(passwordManager); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java index 519c2bd53f1..35adfe2f351 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java @@ -36,7 +36,7 @@ public abstract class AbstractDaoAuthenticationConfigurer, C extends AbstractDaoAuthenticationConfigurer, U extends UserDetailsService> extends UserDetailsAwareConfigurer { - private DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + private DaoAuthenticationProvider provider; private final U userDetailsService; @@ -46,7 +46,7 @@ public abstract class AbstractDaoAuthenticationConfigurer passwordEncoder = SingletonSupplier + .of(PasswordEncoderFactories::createDelegatingPasswordEncoder); /** * The password used to perform {@link PasswordEncoder#matches(CharSequence, String)} @@ -64,15 +68,25 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication private CompromisedPasswordChecker compromisedPasswordChecker; + /** + * @deprecated Please provide the {@link UserDetailsService} in the constructor + */ + @Deprecated public DaoAuthenticationProvider() { - this(PasswordEncoderFactories.createDelegatingPasswordEncoder()); + } + + public DaoAuthenticationProvider(UserDetailsService userDetailsService) { + setUserDetailsService(userDetailsService); } /** * Creates a new instance using the provided {@link PasswordEncoder} * @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null. * @since 6.0.3 + * @deprecated Please provide the {@link UserDetailsService} in the constructor + * followed by {@link #setPasswordEncoder(PasswordEncoder)} instead */ + @Deprecated public DaoAuthenticationProvider(PasswordEncoder passwordEncoder) { setPasswordEncoder(passwordEncoder); } @@ -87,7 +101,7 @@ protected void additionalAuthenticationChecks(UserDetails userDetails, .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); - if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { + if (!this.passwordEncoder.get().matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); @@ -133,9 +147,9 @@ protected Authentication createSuccessAuthentication(Object principal, Authentic throw new CompromisedPasswordException("The provided password is compromised, please change your password"); } boolean upgradeEncoding = this.userDetailsPasswordService != null - && this.passwordEncoder.upgradeEncoding(user.getPassword()); + && this.passwordEncoder.get().upgradeEncoding(user.getPassword()); if (upgradeEncoding) { - String newPassword = this.passwordEncoder.encode(presentedPassword); + String newPassword = this.passwordEncoder.get().encode(presentedPassword); user = this.userDetailsPasswordService.updatePassword(user, newPassword); } return super.createSuccessAuthentication(principal, authentication, user); @@ -143,14 +157,14 @@ protected Authentication createSuccessAuthentication(Object principal, Authentic private void prepareTimingAttackProtection() { if (this.userNotFoundEncodedPassword == null) { - this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); + this.userNotFoundEncodedPassword = this.passwordEncoder.get().encode(USER_NOT_FOUND_PASSWORD); } } private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); - this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword); + this.passwordEncoder.get().matches(presentedPassword, this.userNotFoundEncodedPassword); } } @@ -163,14 +177,19 @@ private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken aut */ public void setPasswordEncoder(PasswordEncoder passwordEncoder) { Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); - this.passwordEncoder = passwordEncoder; + this.passwordEncoder = () -> passwordEncoder; this.userNotFoundEncodedPassword = null; } protected PasswordEncoder getPasswordEncoder() { - return this.passwordEncoder; + return this.passwordEncoder.get(); } + /** + * @param userDetailsService + * @deprecated Please provide the {@link UserDetailsService} in the constructor + */ + @Deprecated public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/index.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/index.adoc index b13cdb615d8..f52e95c4c58 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/index.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/index.adoc @@ -148,8 +148,7 @@ public class SecurityConfig { public AuthenticationManager authenticationManager( UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { - DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); - authenticationProvider.setUserDetailsService(userDetailsService); + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(userDetailsService); authenticationProvider.setPasswordEncoder(passwordEncoder); return new ProviderManager(authenticationProvider); @@ -229,8 +228,7 @@ class SecurityConfig { fun authenticationManager( userDetailsService: UserDetailsService, passwordEncoder: PasswordEncoder): AuthenticationManager { - val authenticationProvider = DaoAuthenticationProvider() - authenticationProvider.setUserDetailsService(userDetailsService) + val authenticationProvider = DaoAuthenticationProvider(userDetailsService) authenticationProvider.setPasswordEncoder(passwordEncoder) return ProviderManager(authenticationProvider) @@ -501,8 +499,7 @@ class SecurityConfig { @Bean fun authenticationManager(): AuthenticationManager { - val authenticationProvider = DaoAuthenticationProvider() - authenticationProvider.setUserDetailsService(userDetailsService()) + val authenticationProvider = DaoAuthenticationProvider(userDetailsService()) authenticationProvider.setPasswordEncoder(passwordEncoder()) val providerManager = ProviderManager(authenticationProvider) From 841c03fe3bdb9a50742c7265e12ddbf5234dfee1 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:36:29 -0700 Subject: [PATCH 14/42] Add Serializable Compatilibity to Saml 2.0 Exceptions Issue gh-16276 --- ...ingSecurityCoreVersionSerializableTests.java | 7 +++++++ ...ork.security.saml2.Saml2Exception.serialized | Bin 0 -> 16297 bytes ...tion.Saml2AuthenticationException.serialized | Bin 0 -> 16678 bytes .../security/saml2/Saml2Exception.java | 7 ++++++- .../Saml2AuthenticationException.java | 7 ++++++- 5 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.saml2.Saml2Exception.serialized create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException.serialized diff --git a/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java b/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java index cff442fffe8..ecc30bd446a 100644 --- a/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java +++ b/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java @@ -116,8 +116,11 @@ import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications; @@ -301,6 +304,10 @@ class SpringSecurityCoreVersionSerializableTests { (r) -> new LdapAuthority("USER", "username", Map.of("attribute", List.of("value1", "value2")))); // saml2-service-provider + generatorByClassName.put(Saml2AuthenticationException.class, + (r) -> new Saml2AuthenticationException(new Saml2Error("code", "descirption"), "message", + new IOException("fail"))); + generatorByClassName.put(Saml2Exception.class, (r) -> new Saml2Exception("message", new IOException("fail"))); generatorByClassName.put(DefaultSaml2AuthenticatedPrincipal.class, (r) -> TestSaml2Authentications.authentication().getPrincipal()); generatorByClassName.put(Saml2Authentication.class, diff --git a/config/src/test/resources/serialized/6.4.x/org.springframework.security.saml2.Saml2Exception.serialized b/config/src/test/resources/serialized/6.4.x/org.springframework.security.saml2.Saml2Exception.serialized new file mode 100644 index 0000000000000000000000000000000000000000..4fd752b76ff32c61363d4a88deaec496110dd86d GIT binary patch literal 16297 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 literal 0 HcmV?d00001 diff --git a/config/src/test/resources/serialized/6.4.x/org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException.serialized b/config/src/test/resources/serialized/6.4.x/org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException.serialized new file mode 100644 index 0000000000000000000000000000000000000000..f771882b3de39663ba2cce10cb968e40abb8bba0 GIT binary patch literal 16678 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 W1l%!3`Fw_Qeg3-zl({a^*uMY}4DY@G literal 0 HcmV?d00001 diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/Saml2Exception.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/Saml2Exception.java index dc4e6bb770d..3595dec00ae 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/Saml2Exception.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/Saml2Exception.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,16 @@ package org.springframework.security.saml2; +import java.io.Serial; + /** * @since 5.2 */ public class Saml2Exception extends RuntimeException { + @Serial + private static final long serialVersionUID = 6076252564189633016L; + public Saml2Exception(String message) { super(message); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java index 6ee38c6d60a..36075ba0df1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.io.Serial; + import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.saml2.core.Saml2Error; @@ -40,6 +42,9 @@ */ public class Saml2AuthenticationException extends AuthenticationException { + @Serial + private static final long serialVersionUID = -2996886630890949105L; + private final Saml2Error error; /** From 27c2a8ad11afa3ac9ae88a67d03d32ec6b3c1e9d Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:05:23 -0700 Subject: [PATCH 15/42] Add Serializable Compatibility to Web Authentication Exceptions Issue gh-16276 --- ...ingSecurityCoreVersionSerializableTests.java | 16 ++++++++++++++++ ...catedCredentialsNotFoundException.serialized | Bin 0 -> 16420 bytes ...n.rememberme.CookieTheftException.serialized | Bin 0 -> 10859 bytes ...rememberme.InvalidCookieException.serialized | Bin 0 -> 10861 bytes ...RememberMeAuthenticationException.serialized | Bin 0 -> 16357 bytes ...on.SessionAuthenticationException.serialized | Bin 0 -> 10753 bytes ...ication.www.NonceExpiredException.serialized | Bin 0 -> 16338 bytes ...thenticatedCredentialsNotFoundException.java | 7 ++++++- .../rememberme/CookieTheftException.java | 7 ++++++- .../rememberme/InvalidCookieException.java | 7 ++++++- .../RememberMeAuthenticationException.java | 7 ++++++- .../session/SessionAuthenticationException.java | 7 ++++++- .../www/NonceExpiredException.java | 5 +++++ 13 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException.serialized create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.rememberme.CookieTheftException.serialized create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.rememberme.InvalidCookieException.serialized create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException.serialized create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.session.SessionAuthenticationException.serialized create mode 100644 config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.www.NonceExpiredException.serialized diff --git a/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java b/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java index ecc30bd446a..5072da9f5c4 100644 --- a/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java +++ b/config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java @@ -128,6 +128,12 @@ import org.springframework.security.saml2.provider.service.authentication.TestSaml2RedirectAuthenticationRequests; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException; +import org.springframework.security.web.authentication.rememberme.CookieTheftException; +import org.springframework.security.web.authentication.rememberme.InvalidCookieException; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.www.NonceExpiredException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -328,6 +334,16 @@ class SpringSecurityCoreVersionSerializableTests { token.setDetails(details); return token; }); + generatorByClassName.put(PreAuthenticatedCredentialsNotFoundException.class, + (r) -> new PreAuthenticatedCredentialsNotFoundException("message", new IOException("fail"))); + generatorByClassName.put(CookieTheftException.class, (r) -> new CookieTheftException("message")); + generatorByClassName.put(InvalidCookieException.class, (r) -> new InvalidCookieException("message")); + generatorByClassName.put(RememberMeAuthenticationException.class, + (r) -> new RememberMeAuthenticationException("message", new IOException("fail"))); + generatorByClassName.put(SessionAuthenticationException.class, + (r) -> new SessionAuthenticationException("message")); + generatorByClassName.put(NonceExpiredException.class, + (r) -> new NonceExpiredException("message", new IOException("fail"))); } @ParameterizedTest diff --git a/config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException.serialized b/config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException.serialized new file mode 100644 index 0000000000000000000000000000000000000000..6d7a94c295056eecba83fa32183d38fbf96d17f0 GIT binary patch literal 16420 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 literal 0 HcmV?d00001 diff --git a/config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.rememberme.InvalidCookieException.serialized b/config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.rememberme.InvalidCookieException.serialized new file mode 100644 index 0000000000000000000000000000000000000000..b4f3a5f6acc8e3b98177c470d27a6a4f79d5f1ea GIT binary patch literal 10861 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 RSScoVY9So1OMXX2{|6t~g*E^H literal 0 HcmV?d00001 diff --git a/config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.session.SessionAuthenticationException.serialized b/config/src/test/resources/serialized/6.4.x/org.springframework.security.web.authentication.session.SessionAuthenticationException.serialized new file mode 100644 index 0000000000000000000000000000000000000000..5b627fb9c7a5b8ec33d00805b3af38c331b5982b GIT binary patch literal 10753 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 literal 0 HcmV?d00001 diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedCredentialsNotFoundException.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedCredentialsNotFoundException.java index d18e7c0cc2b..57767d815c5 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedCredentialsNotFoundException.java +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedCredentialsNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,15 @@ package org.springframework.security.web.authentication.preauth; +import java.io.Serial; + import org.springframework.security.core.AuthenticationException; public class PreAuthenticatedCredentialsNotFoundException extends AuthenticationException { + @Serial + private static final long serialVersionUID = 2026209817833032728L; + public PreAuthenticatedCredentialsNotFoundException(String msg) { super(msg); } diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/CookieTheftException.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/CookieTheftException.java index dabaedd8954..3a477e0c504 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/CookieTheftException.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/CookieTheftException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,16 @@ package org.springframework.security.web.authentication.rememberme; +import java.io.Serial; + /** * @author Luke Taylor */ public class CookieTheftException extends RememberMeAuthenticationException { + @Serial + private static final long serialVersionUID = -7215039140728554850L; + public CookieTheftException(String message) { super(message); } diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/InvalidCookieException.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/InvalidCookieException.java index 00668e06d88..d434bbc47b4 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/InvalidCookieException.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/InvalidCookieException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.authentication.rememberme; +import java.io.Serial; + /** * Exception thrown by a RememberMeServices implementation to indicate that a submitted * cookie is of an invalid format or has expired. @@ -24,6 +26,9 @@ */ public class InvalidCookieException extends RememberMeAuthenticationException { + @Serial + private static final long serialVersionUID = -7952247791921087125L; + public InvalidCookieException(String message) { super(message); } diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationException.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationException.java index dc727efa921..a1fc8c4ee89 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationException.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.authentication.rememberme; +import java.io.Serial; + import org.springframework.security.core.AuthenticationException; /** @@ -27,6 +29,9 @@ */ public class RememberMeAuthenticationException extends AuthenticationException { + @Serial + private static final long serialVersionUID = 7028526952590057426L; + /** * Constructs a {@code RememberMeAuthenticationException} with the specified message * and root cause. diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/SessionAuthenticationException.java b/web/src/main/java/org/springframework/security/web/authentication/session/SessionAuthenticationException.java index db1650b3a9e..6ec0835f758 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/session/SessionAuthenticationException.java +++ b/web/src/main/java/org/springframework/security/web/authentication/session/SessionAuthenticationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.authentication.session; +import java.io.Serial; + import org.springframework.security.core.AuthenticationException; /** @@ -31,6 +33,9 @@ */ public class SessionAuthenticationException extends AuthenticationException { + @Serial + private static final long serialVersionUID = -2359914603911936474L; + public SessionAuthenticationException(String msg) { super(msg); } diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/NonceExpiredException.java b/web/src/main/java/org/springframework/security/web/authentication/www/NonceExpiredException.java index 8ac38137d03..6628a9e27a2 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/NonceExpiredException.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/NonceExpiredException.java @@ -16,6 +16,8 @@ package org.springframework.security.web.authentication.www; +import java.io.Serial; + import org.springframework.security.core.AuthenticationException; /** @@ -25,6 +27,9 @@ */ public class NonceExpiredException extends AuthenticationException { + @Serial + private static final long serialVersionUID = -3487244679050681257L; + /** * Constructs a NonceExpiredException with the specified message. * @param msg the detail message From 7848b959daaeb4134c3b95f3377677a846c6c8cc Mon Sep 17 00:00:00 2001 From: Michal Okosy Date: Sun, 10 Mar 2024 12:53:16 +0100 Subject: [PATCH 16/42] Use relative URLs in /login redirects Closes gh-7273 --- .../web/builders/NamespaceHttpTests.java | 6 ++--- .../DefaultLoginPageConfigurerTests.java | 2 +- .../ExceptionHandlingConfigurerTests.java | 5 ++-- .../configurers/FormLoginConfigurerTests.java | 8 +++--- .../NamespaceHttpFormLoginTests.java | 8 +++--- .../configurers/NamespaceRememberMeTests.java | 6 ++--- .../RememberMeConfigurerTests.java | 4 +-- .../RequestCacheConfigurerTests.java | 20 +++++++------- .../client/OAuth2LoginConfigurerTests.java | 14 +++++----- .../OAuth2ResourceServerConfigurerTests.java | 4 +-- .../saml2/Saml2LoginConfigurerTests.java | 7 ++--- .../security/config/http/CsrfConfigTests.java | 8 +++--- .../config/http/FormLoginConfigTests.java | 6 ++--- .../security/config/http/HttpConfigTests.java | 8 +++--- .../config/http/MiscHttpConfigTests.java | 6 ++--- .../OAuth2LoginBeanDefinitionParserTests.java | 14 +++++----- .../http/PlaceHolderAndELConfigTests.java | 6 ++--- .../Saml2LoginBeanDefinitionParserTests.java | 6 ++--- ...yContextHolderAwareRequestConfigTests.java | 16 ++++++------ .../web/ExceptionHandlingDslTests.kt | 8 +++--- .../annotation/web/FormLoginDslTests.kt | 4 +-- .../annotation/web/RememberMeDslTests.kt | 6 ++--- .../LoginUrlAuthenticationEntryPoint.java | 26 +++++++------------ ...LoginUrlAuthenticationEntryPointTests.java | 14 +++++++--- 24 files changed, 105 insertions(+), 107 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java index e24b8d8f101..7299e3ebc8b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -167,7 +167,7 @@ public void configureWhenAuthenticationEntryPointSetAndRequestUnauthorizedThenRe // @formatter:off this.mockMvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrlPattern("**/entry-point")); + .andExpect(redirectedUrl("/entry-point")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java index 6d683c4899a..b519769bdb5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java @@ -82,7 +82,7 @@ public class DefaultLoginPageConfigurerTests { @Test public void getWhenFormLoginEnabledThenRedirectsToLoginPage() throws Exception { this.spring.register(DefaultLoginPageConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("/login")); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java index d89526127e2..cd57c7bd1ba 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -213,8 +213,7 @@ public void getWhenCustomSecurityContextHolderStrategyThenUsed() throws Exceptio @Test public void getWhenUsingDefaultsAndUnauthenticatedThenRedirectsToLogin() throws Exception { this.spring.register(DefaultHttpConfig.class).autowire(); - this.mvc.perform(get("/").header(HttpHeaders.ACCEPT, "bogus/type")) - .andExpect(redirectedUrl("http://localhost/login")); + this.mvc.perform(get("/").header(HttpHeaders.ACCEPT, "bogus/type")).andExpect(redirectedUrl("/login")); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index 49b8ed2a1af..663b67bcbfa 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -162,7 +162,7 @@ public void requestProtectedWhenFormLoginConfiguredThenRedirectsToLogin() throws // @formatter:off this.mockMvc.perform(get("/private")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -217,7 +217,7 @@ public void requestProtectedWhenFormLoginDefaultsInLambdaThenRedirectsToLogin() // @formatter:off this.mockMvc.perform(get("/private")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -331,7 +331,7 @@ public void failureUrlWhenPermitAllAndFailureHandlerThenSecured() throws Excepti // @formatter:off this.mockMvc.perform(get("/login?error")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java index c1bd55dbdbd..2d46951de1b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ public class NamespaceHttpFormLoginTests { @Test public void formLoginWhenDefaultConfigurationThenMatchesNamespace() throws Exception { this.spring.register(FormLoginConfig.class, UserDetailsServiceConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("/login")); this.mvc.perform(post("/login").with(csrf())).andExpect(redirectedUrl("/login?error")); // @formatter:off MockHttpServletRequestBuilder loginRequest = post("/login") @@ -79,7 +79,7 @@ public void formLoginWhenDefaultConfigurationThenMatchesNamespace() throws Excep @Test public void formLoginWithCustomEndpointsThenBehaviorMatchesNamespace() throws Exception { this.spring.register(FormLoginCustomConfig.class, UserDetailsServiceConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/authentication/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("/authentication/login")); this.mvc.perform(post("/authentication/login/process").with(csrf())) .andExpect(redirectedUrl("/authentication/login?failed")); // @formatter:off @@ -94,7 +94,7 @@ public void formLoginWithCustomEndpointsThenBehaviorMatchesNamespace() throws Ex @Test public void formLoginWithCustomHandlersThenBehaviorMatchesNamespace() throws Exception { this.spring.register(FormLoginCustomRefsConfig.class, UserDetailsServiceConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("/login")); this.mvc.perform(post("/login").with(csrf())).andExpect(redirectedUrl("/custom/failure")); verifyBean(WebAuthenticationDetailsSource.class).buildDetails(any(HttpServletRequest.class)); // @formatter:off diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java index c55f865db8d..2c448484e50 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,7 +104,7 @@ public void rememberMeLoginWhenUsingDefaultsThenMatchesNamespace() throws Except .with(csrf()) .cookie(rememberMe); this.mvc.perform(authenticationClassRequest) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn(); // @formatter:on } @@ -150,7 +150,7 @@ public void rememberMeLoginWhenKeyDeclaredThenMatchesNamespace() throws Exceptio // @formatter:off this.mvc.perform(somewhereRequest) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); MockHttpServletRequestBuilder loginWithRememberme = post("/login").with(rememberMeLogin()); Cookie withKey = this.mvc.perform(loginWithRememberme) .andReturn() diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java index e3cb83f76fd..9277158ba22 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -240,7 +240,7 @@ public void getWhenRememberMeCookieAndLoggedOutThenRedirectsToLogin() throws Exc .with(csrf()) .cookie(expiredRememberMeCookie); // @formatter:on - this.mvc.perform(expiredRequest).andExpect(redirectedUrl("http://localhost/login")); + this.mvc.perform(expiredRequest).andExpect(redirectedUrl("/login")); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index f22e55043d9..09ada968b68 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ public void getWhenBookmarkedUrlIsFaviconIcoThenPostAuthenticationRedirectsToRoo this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire(); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/favicon.ico")) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -104,7 +104,7 @@ public void getWhenBookmarkedUrlIsFaviconPngThenPostAuthenticationRedirectsToRoo this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire(); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/favicon.png")) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -120,7 +120,7 @@ public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedir MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -140,7 +140,7 @@ public void getWhenBookmarkedRequestIsXRequestedWithThenPostAuthenticationRedire .header("X-Requested-With", "XMLHttpRequest"); MockHttpSession session = (MockHttpSession) this.mvc .perform(xRequestedWith) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -157,7 +157,7 @@ public void getWhenBookmarkedRequestIsTextEventStreamThenPostAuthenticationRedir MediaType.TEXT_EVENT_STREAM); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -174,7 +174,7 @@ public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemember MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.ALL); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -188,7 +188,7 @@ public void getWhenBookmarkedRequestIsTextHtmlThenPostAuthenticationRemembers() MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -203,7 +203,7 @@ public void getWhenBookmarkedRequestIsChromeThenPostAuthenticationRemembers() th MockHttpServletRequestBuilder request = get("/messages") .header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); @@ -218,7 +218,7 @@ public void getWhenBookmarkedRequestIsRequestedWithAndroidThenPostAuthentication MockHttpServletRequestBuilder request = get("/messages") .header("X-Requested-With", "com.android"); MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index b56d047a5f7..65a56cdb7aa 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -437,7 +437,7 @@ public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() thr this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("http://localhost/oauth2/authorization/google"); + assertThat(this.response.getRedirectedUrl()).matches("/oauth2/authorization/google"); } // gh-6802 @@ -448,7 +448,7 @@ public void oauth2LoginWithOneClientConfiguredAndFormLoginThenRedirectDefaultLog this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + assertThat(this.response.getRedirectedUrl()).matches("/login"); } // gh-5347 @@ -461,7 +461,7 @@ public void oauth2LoginWithOneClientConfiguredAndRequestFaviconNotAuthenticatedT this.request.setServletPath(requestUri); this.request.addHeader(HttpHeaders.ACCEPT, new MediaType("image", "*").toString()); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + assertThat(this.response.getRedirectedUrl()).matches("/login"); } // gh-5347 @@ -472,7 +472,7 @@ public void oauth2LoginWithMultipleClientsConfiguredThenRedirectDefaultLoginPage this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + assertThat(this.response.getRedirectedUrl()).matches("/login"); } // gh-6812 @@ -521,7 +521,7 @@ public void oauth2LoginWithOneAuthorizationCodeClientAndOtherClientsConfiguredTh this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("http://localhost/oauth2/authorization/google"); + assertThat(this.response.getRedirectedUrl()).matches("/oauth2/authorization/google"); } @Test @@ -531,7 +531,7 @@ public void oauth2LoginWithCustomLoginPageThenRedirectCustomLoginPage() throws E this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); + assertThat(this.response.getRedirectedUrl()).matches("/custom-login"); } @Test @@ -541,7 +541,7 @@ public void requestWhenOauth2LoginWithCustomLoginPageInLambdaThenRedirectCustomL this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); + assertThat(this.response.getRedirectedUrl()).matches("/custom-login"); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index c247a6d7fed..2dad6916fe6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1210,7 +1210,7 @@ public void requestWhenFormLoginAndResourceServerEntryPointsThenSessionCreatedBy MvcResult result = this.mvc.perform(get("/authenticated") .header("Accept", "text/html")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn(); // @formatter:on assertThat(result.getRequest().getSession(false)).isNotNull(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 6d874a583d6..3a912072908 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -412,10 +412,11 @@ public void getFaviconWhenDefaultConfigurationThenDoesNotSaveAuthnRequest() thro this.spring.register(Saml2LoginConfig.class).autowire(); this.mvc.perform(get("/favicon.ico").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); this.mvc.perform(get("/").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()) - .andExpect(header().string("Location", startsWith("http://localhost/saml2/authenticate"))); + .andExpect(header().string("Location", startsWith("/saml2/authenticate"))); + } @Test diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java index 901945e73aa..781cce82e92 100644 --- a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -342,7 +342,7 @@ public void postWhenHasCsrfTokenButSessionExpiresThenRequestIsCancelledAfterSucc this.spring.configLocations(this.xml("CsrfEnabled")).autowire(); // simulates a request that has no authentication (e.g. session time-out) MvcResult result = this.mvc.perform(post("/authenticated").with(csrf())) - .andExpect(redirectedUrl("http://localhost/login")) + .andExpect(redirectedUrl("/login")) .andReturn(); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); // if the request cache is consulted, then it will redirect back to /some-url, @@ -363,9 +363,7 @@ public void getWhenHasCsrfTokenButSessionExpiresThenRequestIsRememeberedAfterSuc throws Exception { this.spring.configLocations(this.xml("CsrfEnabled")).autowire(); // simulates a request that has no authentication (e.g. session time-out) - MvcResult result = this.mvc.perform(get("/authenticated")) - .andExpect(redirectedUrl("http://localhost/login")) - .andReturn(); + MvcResult result = this.mvc.perform(get("/authenticated")).andExpect(redirectedUrl("/login")).andReturn(); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); // if the request cache is consulted, then it will redirect back to /some-url, // which we do want diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java index 52237273df3..b054762aac3 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ public void getProtectedPageWhenFormLoginConfiguredThenRedirectsToDefaultLoginPa this.spring.configLocations(this.xml("WithAntRequestMatcher")).autowire(); // @formatter:off this.mvc.perform(get("/")) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -107,7 +107,7 @@ public void authenticateWhenConfiguredWithSpelThenRedirectsAccordingly() throws this.mvc.perform(invalidPassword) .andExpect(redirectedUrl(WebConfigUtilsTests.URL + "/failure")); this.mvc.perform(get("/")) - .andExpect(redirectedUrl("http://localhost" + WebConfigUtilsTests.URL + "/login")); + .andExpect(redirectedUrl(WebConfigUtilsTests.URL + "/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java index 9a4e3b041e1..62b4ab99dd1 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java @@ -71,7 +71,7 @@ public void getWhenUsingMinimalConfigurationThenRedirectsToLogin() throws Except // @formatter:off this.mvc.perform(get("/")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -81,7 +81,7 @@ public void getWhenUsingMinimalAuthorizationManagerThenRedirectsToLogin() throws // @formatter:off this.mvc.perform(get("/")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -95,7 +95,7 @@ public void getWhenUsingAuthorizationManagerThenRedirectsToLogin() throws Except // @formatter:off this.mvc.perform(get("/")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on verify(authorizationManager).check(any(), any()); } @@ -109,7 +109,7 @@ public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter() proxy.doFilter(request, new EncodeUrlDenyingHttpServletResponseWrapper(response), (req, resp) -> { }); assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); - assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login"); + assertThat(response.getRedirectedUrl()).isEqualTo("/login"); } @Test diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index 180bd2ec532..b08a3c868a0 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -573,7 +573,7 @@ public void configureWhenUsingDisableUrlRewritingThenRedirectIsNotEncodedByRespo proxy.doFilter(request, new EncodeUrlDenyingHttpServletResponseWrapper(response), (req, resp) -> { }); assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); - assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login"); + assertThat(response.getRedirectedUrl()).isEqualTo("/login"); } @Test @@ -802,7 +802,7 @@ public void authenticateWhenUsingPortMapperThenRedirectsAppropriately() throws E this.spring.configLocations(xml("PortsMappedRequiresHttps")).autowire(); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(get("https://localhost:9080/protected")) - .andExpect(redirectedUrl("https://localhost:9443/login")) + .andExpect(redirectedUrl("/login")) .andReturn() .getRequest() .getSession(false); diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java index b632f834de2..6c1e24dcee9 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -174,7 +174,7 @@ public void requestWhenSingleClientRegistrationThenAutoRedirect() throws Excepti // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/oauth2/authorization/google-login")); + .andExpect(redirectedUrl("/oauth2/authorization/google-login")); // @formatter:on verify(this.requestCache).saveRequest(any(), any()); } @@ -187,7 +187,7 @@ public void requestWhenSingleClientRegistrationAndRequestFaviconNotAuthenticated // @formatter:off this.mvc.perform(get("/favicon.ico").accept(new MediaType("image", "*"))) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -199,7 +199,7 @@ public void requestWhenSingleClientRegistrationAndRequestXHRNotAuthenticatedThen // @formatter:off this.mvc.perform(get("/").header("X-Requested-With", "XMLHttpRequest")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -411,7 +411,7 @@ public void requestWhenMultiClientRegistrationThenRedirectDefaultLoginPage() thr // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -421,7 +421,7 @@ public void requestWhenCustomLoginPageThenRedirectCustomLoginPage() throws Excep // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/custom-login")); + .andExpect(redirectedUrl("/custom-login")); // @formatter:on } @@ -433,7 +433,7 @@ public void requestWhenSingleClientRegistrationAndFormLoginConfiguredThenRedirec // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java index ffe686efc16..0c1710f98b9 100644 --- a/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ public void loginWhenUsingPlaceholderThenInterceptUrlsAndFormLoginWorks() throws // login-page setting // @formatter:off this.mvc.perform(get("/secured")) - .andExpect(redirectedUrl("http://localhost/loginPage")); + .andExpect(redirectedUrl("/loginPage")); // login-processing-url setting // default-target-url setting this.mvc.perform(post("/loginPage").param("username", "user").param("password", "password")) @@ -98,7 +98,7 @@ public void loginWhenUsingSpELThenInterceptUrlsAndFormLoginWorks() throws Except // login-page setting // @formatter:off this.mvc.perform(get("/secured")) - .andExpect(redirectedUrl("http://localhost/loginPage")); + .andExpect(redirectedUrl("/loginPage")); // login-processing-url setting // default-target-url setting this.mvc.perform(post("/loginPage").param("username", "user").param("password", "password")) diff --git a/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java index 9eb168233ab..fad02a9ae0c 100644 --- a/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -161,7 +161,7 @@ public void requestWhenSingleRelyingPartyRegistrationThenAutoRedirect() throws E // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/saml2/authenticate/one")); + .andExpect(redirectedUrl("/saml2/authenticate/one")); // @formatter:on verify(this.requestCache).saveRequest(any(), any()); } @@ -172,7 +172,7 @@ public void requestWhenMultiRelyingPartyRegistrationThenRedirectToLoginWithRelyi // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java index 713a03b846b..5a7d0a90382 100644 --- a/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,7 +78,7 @@ public void servletAuthenticateWhenUsingDefaultConfigurationThenUsesSpringSecuri // @formatter:off this.mvc.perform(get("/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -114,7 +114,7 @@ public void servletAuthenticateWhenUsingFormLoginThenUsesSpringSecurity() throws // @formatter:off this.mvc.perform(get("/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); // @formatter:on } @@ -137,10 +137,10 @@ public void servletAuthenticateWhenUsingMultipleHttpConfigsThenUsesSpringSecurit // @formatter:off this.mvc.perform(get("/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); + .andExpect(redirectedUrl("/login")); this.mvc.perform(get("/v2/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login2")); + .andExpect(redirectedUrl("/login2")); // @formatter:on } @@ -177,10 +177,10 @@ public void servletLogoutWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() th @Test public void servletLogoutWhenUsingCustomLogoutThenUsesSpringSecurity() throws Exception { this.spring.configLocations(this.xml("Logout")).autowire(); - this.mvc.perform(get("/authenticate")) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/signin")); // @formatter:off + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/signin")); MvcResult result = this.mvc.perform(get("/good-login")) .andReturn(); // @formatter:on diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt index ed3e409cfff..2aea44c6ffb 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,7 +217,7 @@ class ExceptionHandlingDslTests { this.mockMvc.get("/") .andExpect { status { isFound() } - redirectedUrl("http://localhost/custom-login") + redirectedUrl("/custom-login") } } @@ -246,13 +246,13 @@ class ExceptionHandlingDslTests { this.mockMvc.get("/secured1") .andExpect { status { isFound() } - redirectedUrl("http://localhost/custom-login1") + redirectedUrl("/custom-login1") } this.mockMvc.get("/secured2") .andExpect { status { isFound() } - redirectedUrl("http://localhost/custom-login2") + redirectedUrl("/custom-login2") } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt index 965c361b4a3..5b00105247c 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt @@ -143,7 +143,7 @@ class FormLoginDslTests { this.mockMvc.get("/") .andExpect { status { isFound() } - redirectedUrl("http://localhost/login") + redirectedUrl("/login") } } @@ -169,7 +169,7 @@ class FormLoginDslTests { this.mockMvc.get("/") .andExpect { status { isFound() } - redirectedUrl("http://localhost/log-in") + redirectedUrl("/log-in") } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt index b73b41f50de..a3a07cc0106 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -153,7 +153,7 @@ internal class RememberMeDslTests { cookie(expiredRememberMeCookie) }.andExpect { status { isFound() } - redirectedUrl("http://localhost/login") + redirectedUrl("/login") } } @@ -229,7 +229,7 @@ internal class RememberMeDslTests { cookie(withoutKeyRememberMeCookie) }.andExpect { status { isFound() } - redirectedUrl("http://localhost/login") + redirectedUrl("/login") } val keyMvcResult = mockMvc.post("/login") { loginRememberMeRequest() diff --git a/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java b/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java index 3bf2c6f0dbf..60cf4eef4c1 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java +++ b/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java @@ -61,6 +61,7 @@ * @author colin sampaleanu * @author Omri Spector * @author Luke Taylor + * @author Michal Okosy * @since 3.0 */ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { @@ -143,29 +144,22 @@ public void commence(HttpServletRequest request, HttpServletResponse response, protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { String loginForm = determineUrlToUseForThisRequest(request, response, authException); - if (UrlUtils.isAbsoluteUrl(loginForm)) { + if (UrlUtils.isAbsoluteUrl(loginForm) || !this.forceHttps || "https".equals(request.getScheme())) { return loginForm; } int serverPort = this.portResolver.getServerPort(request); - String scheme = request.getScheme(); + Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort); + if (httpsPort == null) { + logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s", + serverPort)); + return loginForm; + } RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); - urlBuilder.setScheme(scheme); + urlBuilder.setScheme("https"); urlBuilder.setServerName(request.getServerName()); - urlBuilder.setPort(serverPort); + urlBuilder.setPort(httpsPort); urlBuilder.setContextPath(request.getContextPath()); urlBuilder.setPathInfo(loginForm); - if (this.forceHttps && "http".equals(scheme)) { - Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort); - if (httpsPort != null) { - // Overwrite scheme and port in the redirect URL - urlBuilder.setScheme("https"); - urlBuilder.setPort(httpsPort); - } - else { - logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s", - serverPort)); - } - } return urlBuilder.getUrl(); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java b/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java index 77b49be1a16..ad699da8ea9 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java @@ -129,12 +129,18 @@ public void testHttpsOperationFromOriginalHttpsUrl() throws Exception { ep.setPortResolver(new MockPortResolver(80, 443)); ep.afterPropertiesSet(); ep.commence(request, response, null); - assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); request.setServerPort(8443); response = new MockHttpServletResponse(); ep.setPortResolver(new MockPortResolver(8080, 8443)); ep.commence(request, response, null); - assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com:8443/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + // access to https via http port + request.setServerPort(8080); + response = new MockHttpServletResponse(); + ep.setPortResolver(new MockPortResolver(8080, 8443)); + ep.commence(request, response, null); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); } @Test @@ -152,7 +158,7 @@ public void testNormalOperation() throws Exception { request.setServerPort(80); MockHttpServletResponse response = new MockHttpServletResponse(); ep.commence(request, response, null); - assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); } @Test @@ -172,7 +178,7 @@ public void testOperationWhenHttpsRequestsButHttpsPortUnknown() throws Exception ep.commence(request, response, null); // Response doesn't switch to HTTPS, as we didn't know HTTP port 8888 to HTTP port // mapping - assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost:8888/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); } @Test From 3eeb4317f62c584c0724063b7b982684fc024fec Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:12:41 -0700 Subject: [PATCH 17/42] Add setFavorRelativeUris This places the new functionality behind a setting so that we can remain passive until we can change the setting in the next major release. Issue gh-7273 --- .../web/builders/NamespaceHttpTests.java | 6 +- .../DefaultLoginPageConfigurerTests.java | 4 +- .../ExceptionHandlingConfigurerTests.java | 5 +- .../configurers/FormLoginConfigurerTests.java | 8 +- .../NamespaceHttpFormLoginTests.java | 8 +- .../configurers/NamespaceRememberMeTests.java | 6 +- .../RememberMeConfigurerTests.java | 4 +- .../RequestCacheConfigurerTests.java | 20 ++-- .../client/OAuth2LoginConfigurerTests.java | 14 +-- .../OAuth2ResourceServerConfigurerTests.java | 4 +- .../saml2/Saml2LoginConfigurerTests.java | 7 +- .../security/config/http/CsrfConfigTests.java | 8 +- .../config/http/FormLoginConfigTests.java | 6 +- .../security/config/http/HttpConfigTests.java | 10 +- .../config/http/MiscHttpConfigTests.java | 6 +- .../OAuth2LoginBeanDefinitionParserTests.java | 14 +-- .../http/PlaceHolderAndELConfigTests.java | 6 +- .../Saml2LoginBeanDefinitionParserTests.java | 6 +- ...yContextHolderAwareRequestConfigTests.java | 16 +-- .../web/ExceptionHandlingDslTests.kt | 8 +- .../annotation/web/FormLoginDslTests.kt | 4 +- .../annotation/web/RememberMeDslTests.kt | 6 +- docs/modules/ROOT/pages/migration-7/web.adoc | 104 ++++++++++++++++++ .../LoginUrlAuthenticationEntryPoint.java | 46 +++++++- ...LoginUrlAuthenticationEntryPointTests.java | 60 +++++++++- 25 files changed, 288 insertions(+), 98 deletions(-) create mode 100644 docs/modules/ROOT/pages/migration-7/web.adoc diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java index 7299e3ebc8b..e24b8d8f101 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -167,7 +167,7 @@ public void configureWhenAuthenticationEntryPointSetAndRequestUnauthorizedThenRe // @formatter:off this.mockMvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/entry-point")); + .andExpect(redirectedUrlPattern("**/entry-point")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java index b519769bdb5..f4646fe6f53 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,7 +82,7 @@ public class DefaultLoginPageConfigurerTests { @Test public void getWhenFormLoginEnabledThenRedirectsToLoginPage() throws Exception { this.spring.register(DefaultLoginPageConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/login")); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java index cd57c7bd1ba..d89526127e2 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -213,7 +213,8 @@ public void getWhenCustomSecurityContextHolderStrategyThenUsed() throws Exceptio @Test public void getWhenUsingDefaultsAndUnauthenticatedThenRedirectsToLogin() throws Exception { this.spring.register(DefaultHttpConfig.class).autowire(); - this.mvc.perform(get("/").header(HttpHeaders.ACCEPT, "bogus/type")).andExpect(redirectedUrl("/login")); + this.mvc.perform(get("/").header(HttpHeaders.ACCEPT, "bogus/type")) + .andExpect(redirectedUrl("http://localhost/login")); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index 663b67bcbfa..49b8ed2a1af 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -162,7 +162,7 @@ public void requestProtectedWhenFormLoginConfiguredThenRedirectsToLogin() throws // @formatter:off this.mockMvc.perform(get("/private")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -217,7 +217,7 @@ public void requestProtectedWhenFormLoginDefaultsInLambdaThenRedirectsToLogin() // @formatter:off this.mockMvc.perform(get("/private")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -331,7 +331,7 @@ public void failureUrlWhenPermitAllAndFailureHandlerThenSecured() throws Excepti // @formatter:off this.mockMvc.perform(get("/login?error")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java index 2d46951de1b..c1bd55dbdbd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFormLoginTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ public class NamespaceHttpFormLoginTests { @Test public void formLoginWhenDefaultConfigurationThenMatchesNamespace() throws Exception { this.spring.register(FormLoginConfig.class, UserDetailsServiceConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/login")); this.mvc.perform(post("/login").with(csrf())).andExpect(redirectedUrl("/login?error")); // @formatter:off MockHttpServletRequestBuilder loginRequest = post("/login") @@ -79,7 +79,7 @@ public void formLoginWhenDefaultConfigurationThenMatchesNamespace() throws Excep @Test public void formLoginWithCustomEndpointsThenBehaviorMatchesNamespace() throws Exception { this.spring.register(FormLoginCustomConfig.class, UserDetailsServiceConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("/authentication/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/authentication/login")); this.mvc.perform(post("/authentication/login/process").with(csrf())) .andExpect(redirectedUrl("/authentication/login?failed")); // @formatter:off @@ -94,7 +94,7 @@ public void formLoginWithCustomEndpointsThenBehaviorMatchesNamespace() throws Ex @Test public void formLoginWithCustomHandlersThenBehaviorMatchesNamespace() throws Exception { this.spring.register(FormLoginCustomRefsConfig.class, UserDetailsServiceConfig.class).autowire(); - this.mvc.perform(get("/")).andExpect(redirectedUrl("/login")); + this.mvc.perform(get("/")).andExpect(redirectedUrl("http://localhost/login")); this.mvc.perform(post("/login").with(csrf())).andExpect(redirectedUrl("/custom/failure")); verifyBean(WebAuthenticationDetailsSource.class).buildDetails(any(HttpServletRequest.class)); // @formatter:off diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java index 2c448484e50..c55f865db8d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,7 +104,7 @@ public void rememberMeLoginWhenUsingDefaultsThenMatchesNamespace() throws Except .with(csrf()) .cookie(rememberMe); this.mvc.perform(authenticationClassRequest) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn(); // @formatter:on } @@ -150,7 +150,7 @@ public void rememberMeLoginWhenKeyDeclaredThenMatchesNamespace() throws Exceptio // @formatter:off this.mvc.perform(somewhereRequest) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); MockHttpServletRequestBuilder loginWithRememberme = post("/login").with(rememberMeLogin()); Cookie withKey = this.mvc.perform(loginWithRememberme) .andReturn() diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java index 9277158ba22..e3cb83f76fd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -240,7 +240,7 @@ public void getWhenRememberMeCookieAndLoggedOutThenRedirectsToLogin() throws Exc .with(csrf()) .cookie(expiredRememberMeCookie); // @formatter:on - this.mvc.perform(expiredRequest).andExpect(redirectedUrl("/login")); + this.mvc.perform(expiredRequest).andExpect(redirectedUrl("http://localhost/login")); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index 09ada968b68..f22e55043d9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ public void getWhenBookmarkedUrlIsFaviconIcoThenPostAuthenticationRedirectsToRoo this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire(); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/favicon.ico")) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -104,7 +104,7 @@ public void getWhenBookmarkedUrlIsFaviconPngThenPostAuthenticationRedirectsToRoo this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire(); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/favicon.png")) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -120,7 +120,7 @@ public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedir MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -140,7 +140,7 @@ public void getWhenBookmarkedRequestIsXRequestedWithThenPostAuthenticationRedire .header("X-Requested-With", "XMLHttpRequest"); MockHttpSession session = (MockHttpSession) this.mvc .perform(xRequestedWith) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -157,7 +157,7 @@ public void getWhenBookmarkedRequestIsTextEventStreamThenPostAuthenticationRedir MediaType.TEXT_EVENT_STREAM); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -174,7 +174,7 @@ public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemember MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.ALL); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -188,7 +188,7 @@ public void getWhenBookmarkedRequestIsTextHtmlThenPostAuthenticationRemembers() MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -203,7 +203,7 @@ public void getWhenBookmarkedRequestIsChromeThenPostAuthenticationRemembers() th MockHttpServletRequestBuilder request = get("/messages") .header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); @@ -218,7 +218,7 @@ public void getWhenBookmarkedRequestIsRequestedWithAndroidThenPostAuthentication MockHttpServletRequestBuilder request = get("/messages") .header("X-Requested-With", "com.android"); MockHttpSession session = (MockHttpSession) this.mvc.perform(request) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn() .getRequest() .getSession(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index 65a56cdb7aa..b56d047a5f7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -437,7 +437,7 @@ public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() thr this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("/oauth2/authorization/google"); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/oauth2/authorization/google"); } // gh-6802 @@ -448,7 +448,7 @@ public void oauth2LoginWithOneClientConfiguredAndFormLoginThenRedirectDefaultLog this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("/login"); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); } // gh-5347 @@ -461,7 +461,7 @@ public void oauth2LoginWithOneClientConfiguredAndRequestFaviconNotAuthenticatedT this.request.setServletPath(requestUri); this.request.addHeader(HttpHeaders.ACCEPT, new MediaType("image", "*").toString()); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("/login"); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); } // gh-5347 @@ -472,7 +472,7 @@ public void oauth2LoginWithMultipleClientsConfiguredThenRedirectDefaultLoginPage this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("/login"); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); } // gh-6812 @@ -521,7 +521,7 @@ public void oauth2LoginWithOneAuthorizationCodeClientAndOtherClientsConfiguredTh this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("/oauth2/authorization/google"); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/oauth2/authorization/google"); } @Test @@ -531,7 +531,7 @@ public void oauth2LoginWithCustomLoginPageThenRedirectCustomLoginPage() throws E this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("/custom-login"); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); } @Test @@ -541,7 +541,7 @@ public void requestWhenOauth2LoginWithCustomLoginPageInLambdaThenRedirectCustomL this.request = new MockHttpServletRequest("GET", requestUri); this.request.setServletPath(requestUri); this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); - assertThat(this.response.getRedirectedUrl()).matches("/custom-login"); + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 2dad6916fe6..c247a6d7fed 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1210,7 +1210,7 @@ public void requestWhenFormLoginAndResourceServerEntryPointsThenSessionCreatedBy MvcResult result = this.mvc.perform(get("/authenticated") .header("Accept", "text/html")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn(); // @formatter:on assertThat(result.getRequest().getSession(false)).isNotNull(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 3a912072908..6d874a583d6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -412,11 +412,10 @@ public void getFaviconWhenDefaultConfigurationThenDoesNotSaveAuthnRequest() thro this.spring.register(Saml2LoginConfig.class).autowire(); this.mvc.perform(get("/favicon.ico").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); this.mvc.perform(get("/").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()) - .andExpect(header().string("Location", startsWith("/saml2/authenticate"))); - + .andExpect(header().string("Location", startsWith("http://localhost/saml2/authenticate"))); } @Test diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java index 781cce82e92..901945e73aa 100644 --- a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -342,7 +342,7 @@ public void postWhenHasCsrfTokenButSessionExpiresThenRequestIsCancelledAfterSucc this.spring.configLocations(this.xml("CsrfEnabled")).autowire(); // simulates a request that has no authentication (e.g. session time-out) MvcResult result = this.mvc.perform(post("/authenticated").with(csrf())) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("http://localhost/login")) .andReturn(); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); // if the request cache is consulted, then it will redirect back to /some-url, @@ -363,7 +363,9 @@ public void getWhenHasCsrfTokenButSessionExpiresThenRequestIsRememeberedAfterSuc throws Exception { this.spring.configLocations(this.xml("CsrfEnabled")).autowire(); // simulates a request that has no authentication (e.g. session time-out) - MvcResult result = this.mvc.perform(get("/authenticated")).andExpect(redirectedUrl("/login")).andReturn(); + MvcResult result = this.mvc.perform(get("/authenticated")) + .andExpect(redirectedUrl("http://localhost/login")) + .andReturn(); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); // if the request cache is consulted, then it will redirect back to /some-url, // which we do want diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java index b054762aac3..52237273df3 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ public void getProtectedPageWhenFormLoginConfiguredThenRedirectsToDefaultLoginPa this.spring.configLocations(this.xml("WithAntRequestMatcher")).autowire(); // @formatter:off this.mvc.perform(get("/")) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -107,7 +107,7 @@ public void authenticateWhenConfiguredWithSpelThenRedirectsAccordingly() throws this.mvc.perform(invalidPassword) .andExpect(redirectedUrl(WebConfigUtilsTests.URL + "/failure")); this.mvc.perform(get("/")) - .andExpect(redirectedUrl(WebConfigUtilsTests.URL + "/login")); + .andExpect(redirectedUrl("http://localhost" + WebConfigUtilsTests.URL + "/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java index 62b4ab99dd1..c7f0590bc1b 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ public void getWhenUsingMinimalConfigurationThenRedirectsToLogin() throws Except // @formatter:off this.mvc.perform(get("/")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -81,7 +81,7 @@ public void getWhenUsingMinimalAuthorizationManagerThenRedirectsToLogin() throws // @formatter:off this.mvc.perform(get("/")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -95,7 +95,7 @@ public void getWhenUsingAuthorizationManagerThenRedirectsToLogin() throws Except // @formatter:off this.mvc.perform(get("/")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on verify(authorizationManager).check(any(), any()); } @@ -109,7 +109,7 @@ public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter() proxy.doFilter(request, new EncodeUrlDenyingHttpServletResponseWrapper(response), (req, resp) -> { }); assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); - assertThat(response.getRedirectedUrl()).isEqualTo("/login"); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login"); } @Test diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index b08a3c868a0..180bd2ec532 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -573,7 +573,7 @@ public void configureWhenUsingDisableUrlRewritingThenRedirectIsNotEncodedByRespo proxy.doFilter(request, new EncodeUrlDenyingHttpServletResponseWrapper(response), (req, resp) -> { }); assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); - assertThat(response.getRedirectedUrl()).isEqualTo("/login"); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login"); } @Test @@ -802,7 +802,7 @@ public void authenticateWhenUsingPortMapperThenRedirectsAppropriately() throws E this.spring.configLocations(xml("PortsMappedRequiresHttps")).autowire(); // @formatter:off MockHttpSession session = (MockHttpSession) this.mvc.perform(get("https://localhost:9080/protected")) - .andExpect(redirectedUrl("/login")) + .andExpect(redirectedUrl("https://localhost:9443/login")) .andReturn() .getRequest() .getSession(false); diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java index 6c1e24dcee9..b632f834de2 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -174,7 +174,7 @@ public void requestWhenSingleClientRegistrationThenAutoRedirect() throws Excepti // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/oauth2/authorization/google-login")); + .andExpect(redirectedUrl("http://localhost/oauth2/authorization/google-login")); // @formatter:on verify(this.requestCache).saveRequest(any(), any()); } @@ -187,7 +187,7 @@ public void requestWhenSingleClientRegistrationAndRequestFaviconNotAuthenticated // @formatter:off this.mvc.perform(get("/favicon.ico").accept(new MediaType("image", "*"))) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -199,7 +199,7 @@ public void requestWhenSingleClientRegistrationAndRequestXHRNotAuthenticatedThen // @formatter:off this.mvc.perform(get("/").header("X-Requested-With", "XMLHttpRequest")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -411,7 +411,7 @@ public void requestWhenMultiClientRegistrationThenRedirectDefaultLoginPage() thr // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -421,7 +421,7 @@ public void requestWhenCustomLoginPageThenRedirectCustomLoginPage() throws Excep // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/custom-login")); + .andExpect(redirectedUrl("http://localhost/custom-login")); // @formatter:on } @@ -433,7 +433,7 @@ public void requestWhenSingleClientRegistrationAndFormLoginConfiguredThenRedirec // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java index 0c1710f98b9..ffe686efc16 100644 --- a/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ public void loginWhenUsingPlaceholderThenInterceptUrlsAndFormLoginWorks() throws // login-page setting // @formatter:off this.mvc.perform(get("/secured")) - .andExpect(redirectedUrl("/loginPage")); + .andExpect(redirectedUrl("http://localhost/loginPage")); // login-processing-url setting // default-target-url setting this.mvc.perform(post("/loginPage").param("username", "user").param("password", "password")) @@ -98,7 +98,7 @@ public void loginWhenUsingSpELThenInterceptUrlsAndFormLoginWorks() throws Except // login-page setting // @formatter:off this.mvc.perform(get("/secured")) - .andExpect(redirectedUrl("/loginPage")); + .andExpect(redirectedUrl("http://localhost/loginPage")); // login-processing-url setting // default-target-url setting this.mvc.perform(post("/loginPage").param("username", "user").param("password", "password")) diff --git a/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java index fad02a9ae0c..9eb168233ab 100644 --- a/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -161,7 +161,7 @@ public void requestWhenSingleRelyingPartyRegistrationThenAutoRedirect() throws E // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/saml2/authenticate/one")); + .andExpect(redirectedUrl("http://localhost/saml2/authenticate/one")); // @formatter:on verify(this.requestCache).saveRequest(any(), any()); } @@ -172,7 +172,7 @@ public void requestWhenMultiRelyingPartyRegistrationThenRedirectToLoginWithRelyi // @formatter:off this.mvc.perform(get("/")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java index 5a7d0a90382..713a03b846b 100644 --- a/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,7 +78,7 @@ public void servletAuthenticateWhenUsingDefaultConfigurationThenUsesSpringSecuri // @formatter:off this.mvc.perform(get("/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -114,7 +114,7 @@ public void servletAuthenticateWhenUsingFormLoginThenUsesSpringSecurity() throws // @formatter:off this.mvc.perform(get("/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); // @formatter:on } @@ -137,10 +137,10 @@ public void servletAuthenticateWhenUsingMultipleHttpConfigsThenUsesSpringSecurit // @formatter:off this.mvc.perform(get("/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login")); + .andExpect(redirectedUrl("http://localhost/login")); this.mvc.perform(get("/v2/authenticate")) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login2")); + .andExpect(redirectedUrl("http://localhost/login2")); // @formatter:on } @@ -177,10 +177,10 @@ public void servletLogoutWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() th @Test public void servletLogoutWhenUsingCustomLogoutThenUsesSpringSecurity() throws Exception { this.spring.configLocations(this.xml("Logout")).autowire(); - // @formatter:off this.mvc.perform(get("/authenticate")) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("/signin")); + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/signin")); + // @formatter:off MvcResult result = this.mvc.perform(get("/good-login")) .andReturn(); // @formatter:on diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt index 2aea44c6ffb..ed3e409cfff 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/ExceptionHandlingDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,7 +217,7 @@ class ExceptionHandlingDslTests { this.mockMvc.get("/") .andExpect { status { isFound() } - redirectedUrl("/custom-login") + redirectedUrl("http://localhost/custom-login") } } @@ -246,13 +246,13 @@ class ExceptionHandlingDslTests { this.mockMvc.get("/secured1") .andExpect { status { isFound() } - redirectedUrl("/custom-login1") + redirectedUrl("http://localhost/custom-login1") } this.mockMvc.get("/secured2") .andExpect { status { isFound() } - redirectedUrl("/custom-login2") + redirectedUrl("http://localhost/custom-login2") } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt index 5b00105247c..965c361b4a3 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt @@ -143,7 +143,7 @@ class FormLoginDslTests { this.mockMvc.get("/") .andExpect { status { isFound() } - redirectedUrl("/login") + redirectedUrl("http://localhost/login") } } @@ -169,7 +169,7 @@ class FormLoginDslTests { this.mockMvc.get("/") .andExpect { status { isFound() } - redirectedUrl("/log-in") + redirectedUrl("http://localhost/log-in") } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt index a3a07cc0106..b73b41f50de 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/RememberMeDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -153,7 +153,7 @@ internal class RememberMeDslTests { cookie(expiredRememberMeCookie) }.andExpect { status { isFound() } - redirectedUrl("/login") + redirectedUrl("http://localhost/login") } } @@ -229,7 +229,7 @@ internal class RememberMeDslTests { cookie(withoutKeyRememberMeCookie) }.andExpect { status { isFound() } - redirectedUrl("/login") + redirectedUrl("http://localhost/login") } val keyMvcResult = mockMvc.post("/login") { loginRememberMeRequest() diff --git a/docs/modules/ROOT/pages/migration-7/web.adoc b/docs/modules/ROOT/pages/migration-7/web.adoc new file mode 100644 index 00000000000..024d5604494 --- /dev/null +++ b/docs/modules/ROOT/pages/migration-7/web.adoc @@ -0,0 +1,104 @@ += Web Migrations + +== Favor Relative URIs + +When redirecting to a login endpoint, Spring Security has favored absolute URIs in the past. +For example, if you set your login page like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +http + // ... + .formLogin((form) -> form.loginPage("/my-login")) + // ... +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + formLogin { + loginPage = "/my-login" + } +} +---- + +Xml:: ++ +[source,kotlin,role="secondary"] +---- + + + +---- +====== + +then when redirecting to `/my-login` Spring Security would use a `Location:` like the following: + +[source] +---- +302 Found +// ... +Location: https://myapp.example.org/my-login +---- + +However, this is no longer necessary given that the RFC is was based on is now obsolete. + +In Spring Security 7, this is changed to use a relative URI like so: + +[source] +---- +302 Found +// ... +Location: /my-login +---- + +Most applications will not notice a difference. +However, in the event that this change causes problems, you can switch back to the Spring Security 6 behavior by setting the `favorRelativeUrls` value: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint("/my-login"); +entryPoint.setFavorRelativeUris(false); +http + // ... + .exceptionHandling((exceptions) -> exceptions.authenticaitonEntryPoint(entryPoint)) + // ... +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +LoginUrlAuthenticationEntryPoint entryPoint = LoginUrlAuthenticationEntryPoint("/my-login") +entryPoint.setFavorRelativeUris(false) + +http { + exceptionHandling { + authenticationEntryPoint = entryPoint + } +} +---- + +Xml:: ++ +[source,kotlin,role="secondary"] +---- + + + + + + + +---- +====== diff --git a/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java b/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java index 60cf4eef4c1..5e62d2ebeb6 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java +++ b/web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java @@ -78,6 +78,8 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin private boolean useForward = false; + private boolean favorRelativeUris = false; + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); /** @@ -144,23 +146,41 @@ public void commence(HttpServletRequest request, HttpServletResponse response, protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { String loginForm = determineUrlToUseForThisRequest(request, response, authException); - if (UrlUtils.isAbsoluteUrl(loginForm) || !this.forceHttps || "https".equals(request.getScheme())) { + if (UrlUtils.isAbsoluteUrl(loginForm)) { return loginForm; } + if (requiresRewrite(request)) { + return httpsUri(request, loginForm); + } + return this.favorRelativeUris ? loginForm : absoluteUri(request, loginForm).getUrl(); + } + + private boolean requiresRewrite(HttpServletRequest request) { + return this.forceHttps && "http".equals(request.getScheme()); + } + + private String httpsUri(HttpServletRequest request, String path) { int serverPort = this.portResolver.getServerPort(request); Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort); if (httpsPort == null) { logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s", serverPort)); - return loginForm; + return this.favorRelativeUris ? path : absoluteUri(request, path).getUrl(); } + RedirectUrlBuilder builder = absoluteUri(request, path); + builder.setScheme("https"); + builder.setPort(httpsPort); + return builder.getUrl(); + } + + private RedirectUrlBuilder absoluteUri(HttpServletRequest request, String path) { RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); - urlBuilder.setScheme("https"); + urlBuilder.setScheme(request.getScheme()); urlBuilder.setServerName(request.getServerName()); - urlBuilder.setPort(httpsPort); + urlBuilder.setPort(this.portResolver.getServerPort(request)); urlBuilder.setContextPath(request.getContextPath()); - urlBuilder.setPathInfo(loginForm); - return urlBuilder.getUrl(); + urlBuilder.setPathInfo(path); + return urlBuilder; } /** @@ -238,4 +258,18 @@ protected boolean isUseForward() { return this.useForward; } + /** + * Favor using relative URIs when formulating a redirect. + * + *

+ * Note that a relative redirect is not always possible. For example, when redirecting + * from {@code http} to {@code https}, the URL needs to be absolute. + *

+ * @param favorRelativeUris whether to favor relative URIs or not + * @since 6.5 + */ + public void setFavorRelativeUris(boolean favorRelativeUris) { + this.favorRelativeUris = favorRelativeUris; + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java b/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java index ad699da8ea9..91e2d93cdfa 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPointTests.java @@ -129,18 +129,18 @@ public void testHttpsOperationFromOriginalHttpsUrl() throws Exception { ep.setPortResolver(new MockPortResolver(80, 443)); ep.afterPropertiesSet(); ep.commence(request, response, null); - assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com/bigWebApp/hello"); request.setServerPort(8443); response = new MockHttpServletResponse(); ep.setPortResolver(new MockPortResolver(8080, 8443)); ep.commence(request, response, null); - assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com:8443/bigWebApp/hello"); // access to https via http port request.setServerPort(8080); response = new MockHttpServletResponse(); ep.setPortResolver(new MockPortResolver(8080, 8443)); ep.commence(request, response, null); - assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com:8443/bigWebApp/hello"); } @Test @@ -158,7 +158,7 @@ public void testNormalOperation() throws Exception { request.setServerPort(80); MockHttpServletResponse response = new MockHttpServletResponse(); ep.commence(request, response, null); - assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/bigWebApp/hello"); } @Test @@ -178,7 +178,7 @@ public void testOperationWhenHttpsRequestsButHttpsPortUnknown() throws Exception ep.commence(request, response, null); // Response doesn't switch to HTTPS, as we didn't know HTTP port 8888 to HTTP port // mapping - assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost:8888/bigWebApp/hello"); } @Test @@ -237,4 +237,54 @@ public void absoluteLoginFormUrlCantBeUsedWithForwarding() throws Exception { assertThatIllegalArgumentException().isThrownBy(ep::afterPropertiesSet); } + @Test + public void commenceWhenFavorRelativeUrisThenHttpsSchemeNotIncluded() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/some_path"); + request.setScheme("https"); + request.setServerName("www.example.com"); + request.setContextPath("/bigWebApp"); + request.setServerPort(443); + MockHttpServletResponse response = new MockHttpServletResponse(); + LoginUrlAuthenticationEntryPoint ep = new LoginUrlAuthenticationEntryPoint("/hello"); + ep.setFavorRelativeUris(true); + ep.setPortMapper(new PortMapperImpl()); + ep.setForceHttps(true); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(80, 443)); + ep.afterPropertiesSet(); + ep.commence(request, response, null); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + request.setServerPort(8443); + response = new MockHttpServletResponse(); + ep.setPortResolver(new MockPortResolver(8080, 8443)); + ep.commence(request, response, null); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + // access to https via http port + request.setServerPort(8080); + response = new MockHttpServletResponse(); + ep.setPortResolver(new MockPortResolver(8080, 8443)); + ep.commence(request, response, null); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + } + + @Test + public void commenceWhenFavorRelativeUrisThenHttpSchemeNotIncluded() throws Exception { + LoginUrlAuthenticationEntryPoint ep = new LoginUrlAuthenticationEntryPoint("/hello"); + ep.setFavorRelativeUris(true); + ep.setPortMapper(new PortMapperImpl()); + ep.setPortResolver(new MockPortResolver(80, 443)); + ep.afterPropertiesSet(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/some_path"); + request.setContextPath("/bigWebApp"); + request.setScheme("http"); + request.setServerName("localhost"); + request.setContextPath("/bigWebApp"); + request.setServerPort(80); + MockHttpServletResponse response = new MockHttpServletResponse(); + ep.commence(request, response, null); + assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello"); + } + } From 9404aaf01083e9d7518d0167fdffcc9dc10c5a5b Mon Sep 17 00:00:00 2001 From: ThomasKasene <6691406+ThomasKasene@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:30:18 +0100 Subject: [PATCH 18/42] Added a constant for DPOP in OAuth2AccessToken.TokenType Issue gh-14915 --- .../springframework/security/oauth2/core/OAuth2AccessToken.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java index d288503f131..66667e13745 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java @@ -107,6 +107,8 @@ public static final class TokenType implements Serializable { public static final TokenType BEARER = new TokenType("Bearer"); + public static final TokenType DPOP = new TokenType("DPoP"); + private final String value; private TokenType(String value) { From a7b6c634426ed25cc995a31f74ee64b99be72e24 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:48:25 -0600 Subject: [PATCH 19/42] Polish gh-16087 --- .../security/oauth2/core/OAuth2AccessToken.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java index 66667e13745..88fda0e6090 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java @@ -107,6 +107,9 @@ public static final class TokenType implements Serializable { public static final TokenType BEARER = new TokenType("Bearer"); + /** + * @since 6.5 + */ public static final TokenType DPOP = new TokenType("DPoP"); private final String value; From 6bc6946ad9c602b83246bec46d5899e979cbbb3e Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:51:47 -0600 Subject: [PATCH 20/42] Make TokenType constructor public Closes gh-16086 --- .../security/oauth2/core/OAuth2AccessToken.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java index 88fda0e6090..ea1124d0412 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java @@ -114,7 +114,12 @@ public static final class TokenType implements Serializable { private final String value; - private TokenType(String value) { + /** + * Constructs a {@code TokenType} using the provided value. + * @param value the value of the token type + * @since 6.5 + */ + public TokenType(String value) { Assert.hasText(value, "value cannot be empty"); this.value = value; } From 1864577e9817aa37629012d579da442bb6678bf3 Mon Sep 17 00:00:00 2001 From: Claudenir Machado Date: Thu, 5 Dec 2024 12:50:38 +0000 Subject: [PATCH 21/42] Address SessionLimitStrategy Closes gh-16206 --- .../SessionManagementConfigurer.java | 23 ++- .../config/http/HttpConfigurationBuilder.java | 18 ++- .../security/config/spring-security-6.5.rnc | 3 + .../security/config/spring-security-6.5.xsd | 7 + .../SessionManagementConfigurerTests.java | 115 ++++++++++++++- .../config/http/HttpHeadersConfigTests.java | 137 +++++++++++++++++- ...anagementConcurrencyControlMaxSessions.xml | 38 +++++ ...gementConcurrencyControlMaxSessionsRef.xml | 41 ++++++ ...ncyControlWithInvalidMaxSessionsConfig.xml | 42 ++++++ .../servlet/appendix/namespace/http.adoc | 3 + ...tSessionControlAuthenticationStrategy.java | 22 ++- .../security/web/session/SessionLimit.java | 49 +++++++ ...ionControlAuthenticationStrategyTests.java | 85 ++++++++++- .../web/session/SessionLimitTests.java | 59 ++++++++ 14 files changed, 627 insertions(+), 15 deletions(-) create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessions.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessionsRef.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig.xml create mode 100644 web/src/main/java/org/springframework/security/web/session/SessionLimit.java create mode 100644 web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index fc4a2a38804..5ff5b00e72c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -59,6 +59,7 @@ import org.springframework.security.web.session.ForceEagerSessionCreationFilter; import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.security.web.session.SessionInformationExpiredStrategy; +import org.springframework.security.web.session.SessionLimit; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy; @@ -123,7 +124,7 @@ public final class SessionManagementConfigurer> private SessionRegistry sessionRegistry; - private Integer maximumSessions; + private SessionLimit sessionLimit; private String expiredUrl; @@ -329,7 +330,7 @@ public SessionManagementConfigurer sessionFixation( * @return the {@link SessionManagementConfigurer} for further customizations */ public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) { - this.maximumSessions = maximumSessions; + this.sessionLimit = SessionLimit.of(maximumSessions); this.propertiesThatRequireImplicitAuthentication.add("maximumSessions = " + maximumSessions); return new ConcurrencyControlConfigurer(); } @@ -570,7 +571,7 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) { SessionRegistry sessionRegistry = getSessionRegistry(http); ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy( sessionRegistry); - concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions); + concurrentSessionControlStrategy.setMaximumSessions(this.sessionLimit); concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin); concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy); RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy( @@ -614,7 +615,7 @@ private void registerDelegateApplicationListener(H http, ApplicationListener * @return */ private boolean isConcurrentSessionControlEnabled() { - return this.maximumSessions != null; + return this.sessionLimit != null; } /** @@ -706,7 +707,19 @@ private ConcurrencyControlConfigurer() { * @return the {@link ConcurrencyControlConfigurer} for further customizations */ public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) { - SessionManagementConfigurer.this.maximumSessions = maximumSessions; + SessionManagementConfigurer.this.sessionLimit = SessionLimit.of(maximumSessions); + return this; + } + + /** + * Determines the behaviour when a session limit is detected. + * @param sessionLimit the {@link SessionLimit} to check the maximum number of + * sessions for a user + * @return the {@link ConcurrencyControlConfigurer} for further customizations + * @since 6.5 + */ + public ConcurrencyControlConfigurer maximumSessions(SessionLimit sessionLimit) { + SessionManagementConfigurer.this.sessionLimit = sessionLimit; return this; } diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index 53635b5aa0b..db915da8678 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -122,6 +122,10 @@ class HttpConfigurationBuilder { private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref"; + private static final String ATT_MAX_SESSIONS_REF = "max-sessions-ref"; + + private static final String ATT_MAX_SESSIONS = "max-sessions"; + private static final String ATT_SESSION_AUTH_ERROR_URL = "session-authentication-error-url"; private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY = "security-context-holder-strategy-ref"; @@ -485,10 +489,16 @@ else if (StringUtils.hasText(sessionAuthStratRef)) { concurrentSessionStrategy.addConstructorArgValue(this.sessionRegistryRef); String maxSessions = this.pc.getReaderContext() .getEnvironment() - .resolvePlaceholders(sessionCtrlElt.getAttribute("max-sessions")); + .resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS)); if (StringUtils.hasText(maxSessions)) { concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions); } + String maxSessionsRef = this.pc.getReaderContext() + .getEnvironment() + .resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS_REF)); + if (StringUtils.hasText(maxSessionsRef)) { + concurrentSessionStrategy.addPropertyReference("maximumSessions", maxSessionsRef); + } String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded"); if (StringUtils.hasText(exceptionIfMaximumExceeded)) { concurrentSessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded); @@ -591,6 +601,12 @@ private void createConcurrencyControlFilterAndSessionRegistry(Element element) { .error("Cannot use 'expired-url' attribute and 'expired-session-strategy-ref'" + " attribute together.", source); } + String maxSessions = element.getAttribute(ATT_MAX_SESSIONS); + String maxSessionsRef = element.getAttribute(ATT_MAX_SESSIONS_REF); + if (StringUtils.hasText(maxSessions) && StringUtils.hasText(maxSessionsRef)) { + this.pc.getReaderContext() + .error("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.", source); + } if (StringUtils.hasText(expiryUrl)) { BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder .rootBeanDefinition(SimpleRedirectSessionInformationExpiredStrategy.class); diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc index 9b2469aa879..9dcb7305714 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc @@ -934,6 +934,9 @@ concurrency-control = concurrency-control.attlist &= ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. attribute max-sessions {xsd:token}? +concurrency-control.attlist &= + ## Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy + attribute max-sessions-ref {xsd:token}? concurrency-control.attlist &= ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. attribute expired-url {xsd:token}? diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd index e46438d80dd..03a00f36657 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd @@ -2688,6 +2688,13 @@ + + + Allows injection of the SessionLimit instance used by the + ConcurrentSessionControlAuthenticationStrategy + + + The URL a user will be redirected to if they attempt to use a session which has been diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index fbe52459a45..cc3011a7199 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,7 @@ import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.session.ConcurrentSessionFilter; import org.springframework.security.web.session.HttpSessionDestroyedEvent; +import org.springframework.security.web.session.SessionLimit; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -249,6 +250,82 @@ public void loginWhenUserLoggedInAndMaxSessionsOneInLambdaThenLoginPrevented() t // @formatter:on } + @Test + public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginSuccessfully() throws Exception { + this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestBuilder = post("/login") + .with(csrf()) + .param("username", "admin") + .param("password", "password"); + HttpSession firstSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(firstSession).isNotNull(); + HttpSession secondSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(secondSession).isNotNull(); + // @formatter:on + assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId()); + } + + @Test + public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception { + this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestBuilder = post("/login") + .with(csrf()) + .param("username", "admin") + .param("password", "password"); + HttpSession firstSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(firstSession).isNotNull(); + HttpSession secondSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(secondSession).isNotNull(); + assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId()); + this.mvc.perform(requestBuilder) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + // @formatter:on + } + + @Test + public void loginWhenUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception { + this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestBuilder = post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password"); + HttpSession firstSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(firstSession).isNotNull(); + this.mvc.perform(requestBuilder) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + // @formatter:on + } + @Test public void requestWhenSessionCreationPolicyStateLessInLambdaThenNoSessionCreated() throws Exception { this.spring.register(SessionCreationPolicyStateLessInLambdaConfig.class).autowire(); @@ -625,6 +702,42 @@ UserDetailsService userDetailsService() { } + @Configuration + @EnableWebSecurity + static class ConcurrencyControlWithSessionLimitConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http, SessionLimit sessionLimit) throws Exception { + // @formatter:off + http + .formLogin(withDefaults()) + .sessionManagement((sessionManagement) -> sessionManagement + .sessionConcurrency((sessionConcurrency) -> sessionConcurrency + .maximumSessions(sessionLimit) + .maxSessionsPreventsLogin(true) + ) + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(PasswordEncodedUser.admin(), PasswordEncodedUser.user()); + } + + @Bean + SessionLimit SessionLimit() { + return (authentication) -> { + if ("admin".equals(authentication.getName())) { + return 2; + } + return 1; + }; + } + + } + @Configuration @EnableWebSecurity static class SessionCreationPolicyStateLessInLambdaConfig { diff --git a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java index c66933de16c..6c89be179a4 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.Set; import com.google.common.collect.ImmutableMap; +import jakarta.servlet.http.HttpSession; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,14 +34,21 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.session.SessionLimit; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -49,6 +57,7 @@ * @author Josh Cummings * @author Rafiullah Hamedy * @author Marcus Da Coregio + * @author Claudenir Freitas */ @ExtendWith(SpringTestContextExtension.class) public class HttpHeadersConfigTests { @@ -782,6 +791,120 @@ public void requestWhenCrossOriginPoliciesRespondsCrossOriginPolicies() throws E // @formatter:on } + @Test + public void requestWhenSessionManagementConcurrencyControlMaxSessionIsOne() throws Exception { + System.setProperty("security.session-management.concurrency-control.max-sessions", "1"); + this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestBuilder = post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password"); + HttpSession firstSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + // @formatter:on + assertThat(firstSession).isNotNull(); + // @formatter:off + this.mvc.perform(requestBuilder) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + // @formatter:on + } + + @Test + public void requestWhenSessionManagementConcurrencyControlMaxSessionIsUnlimited() throws Exception { + System.setProperty("security.session-management.concurrency-control.max-sessions", "-1"); + this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestBuilder = post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password"); + HttpSession firstSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(firstSession).isNotNull(); + HttpSession secondSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(secondSession).isNotNull(); + // @formatter:on + assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId()); + } + + @Test + public void requestWhenSessionManagementConcurrencyControlMaxSessionRefIsOneForNonAdminUsers() throws Exception { + this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessionsRef")).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestBuilder = post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password"); + HttpSession firstSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + // @formatter:on + assertThat(firstSession).isNotNull(); + // @formatter:off + this.mvc.perform(requestBuilder) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + // @formatter:on + } + + @Test + public void requestWhenSessionManagementConcurrencyControlMaxSessionRefIsTwoForAdminUsers() throws Exception { + this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessionsRef")).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestBuilder = post("/login") + .with(csrf()) + .param("username", "admin") + .param("password", "password"); + HttpSession firstSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(firstSession).isNotNull(); + HttpSession secondSession = this.mvc.perform(requestBuilder) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")) + .andReturn() + .getRequest() + .getSession(false); + assertThat(secondSession).isNotNull(); + // @formatter:on + assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId()); + // @formatter:off + this.mvc.perform(requestBuilder) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + // @formatter:on + } + + @Test + public void requestWhenSessionManagementConcurrencyControlWithInvalidMaxSessionConfig() { + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> this.spring + .configLocations(this.xml("DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig")) + .autowire()) + .withMessageContaining("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together."); + } + private static ResultMatcher includesDefaults() { return includes(defaultHeaders); } @@ -832,4 +955,16 @@ public String ok() { } + public static class CustomSessionLimit implements SessionLimit { + + @Override + public Integer apply(Authentication authentication) { + if ("admin".equals(authentication.getName())) { + return 2; + } + return 1; + } + + } + } diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessions.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessions.xml new file mode 100644 index 00000000000..7e8c3d12a34 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessions.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessionsRef.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessionsRef.xml new file mode 100644 index 00000000000..98215ca86cd --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessionsRef.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig.xml new file mode 100644 index 00000000000..7bf56c9a3a4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 59e48e0986d..d49c2f12db3 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -2168,6 +2168,9 @@ Allows injection of the ExpiredSessionStrategy instance used by the ConcurrentSe Maps to the `maximumSessions` property of `ConcurrentSessionControlAuthenticationStrategy`. Specify `-1` as the value to support unlimited sessions. +[[nsa-concurrency-control-max-sessions-ref]] +* **max-sessions-ref** +Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy [[nsa-concurrency-control-session-registry-alias]] * **session-registry-alias** diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java index 8d528f56212..b8f3c9e3077 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java +++ b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java @@ -33,6 +33,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionLimit; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.util.Assert; @@ -76,7 +77,7 @@ public class ConcurrentSessionControlAuthenticationStrategy private boolean exceptionIfMaximumExceeded = false; - private int maximumSessions = 1; + private SessionLimit sessionLimit = SessionLimit.of(1); /** * @param sessionRegistry the session registry which should be updated when the @@ -130,7 +131,7 @@ public void onAuthentication(Authentication authentication, HttpServletRequest r * @return either -1 meaning unlimited, or a positive integer to limit (never zero) */ protected int getMaximumSessionsForThisUser(Authentication authentication) { - return this.maximumSessions; + return this.sessionLimit.apply(authentication); } /** @@ -172,15 +173,24 @@ public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) { } /** - * Sets the maxSessions property. The default value is 1. Use -1 for + * Sets the sessionLimit property. The default value is 1. Use -1 for * unlimited sessions. * @param maximumSessions the maximum number of permitted sessions a user can have * open simultaneously. */ public void setMaximumSessions(int maximumSessions) { - Assert.isTrue(maximumSessions != 0, - "MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum"); - this.maximumSessions = maximumSessions; + this.sessionLimit = SessionLimit.of(maximumSessions); + } + + /** + * Sets the sessionLimit property. The default value is 1. Use -1 for + * unlimited sessions. + * @param sessionLimit the session limit strategy + * @since 6.5 + */ + public void setMaximumSessions(SessionLimit sessionLimit) { + Assert.notNull(sessionLimit, "sessionLimit cannot be null"); + this.sessionLimit = sessionLimit; } /** diff --git a/web/src/main/java/org/springframework/security/web/session/SessionLimit.java b/web/src/main/java/org/springframework/security/web/session/SessionLimit.java new file mode 100644 index 00000000000..385c4621377 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/session/SessionLimit.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2024 the original author or 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 org.springframework.security.web.session; + +import java.util.function.Function; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * Represents the maximum number of sessions allowed. Use {@link #UNLIMITED} to indicate + * that there is no limit. + * + * @author Claudenir Freitas + * @since 6.5 + */ +public interface SessionLimit extends Function { + + /** + * Represents unlimited sessions. + */ + SessionLimit UNLIMITED = (authentication) -> -1; + + /** + * Creates a {@link SessionLimit} that always returns the given value for any user + * @param maxSessions the maximum number of sessions allowed + * @return a {@link SessionLimit} instance that returns the given value. + */ + static SessionLimit of(int maxSessions) { + Assert.isTrue(maxSessions != 0, + "MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum"); + return (authentication) -> maxSessions; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java index ffe51cc2a0e..26d4afe3efb 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.web.session.SessionLimit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -41,9 +42,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verifyNoInteractions; /** * @author Rob Winch + * @author Claudenir Freitas * */ @ExtendWith(MockitoExtension.class) @@ -144,6 +147,86 @@ public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpired() assertThat(this.sessionInformation.isExpired()).isFalse(); } + @Test + public void setMaximumSessionsWithNullValue() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> this.strategy.setMaximumSessions(null)) + .withMessage("sessionLimit cannot be null"); + } + + @Test + public void noRegisteredSessionUsingSessionLimit() { + given(this.sessionRegistry.getAllSessions(any(), anyBoolean())).willReturn(Collections.emptyList()); + this.strategy.setMaximumSessions(SessionLimit.of(1)); + this.strategy.setExceptionIfMaximumExceeded(true); + this.strategy.onAuthentication(this.authentication, this.request, this.response); + // no exception + } + + @Test + public void maxSessionsSameSessionIdUsingSessionLimit() { + MockHttpSession session = new MockHttpSession(new MockServletContext(), this.sessionInformation.getSessionId()); + this.request.setSession(session); + given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) + .willReturn(Collections.singletonList(this.sessionInformation)); + this.strategy.setMaximumSessions(SessionLimit.of(1)); + this.strategy.setExceptionIfMaximumExceeded(true); + this.strategy.onAuthentication(this.authentication, this.request, this.response); + // no exception + } + + @Test + public void maxSessionsWithExceptionUsingSessionLimit() { + given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) + .willReturn(Collections.singletonList(this.sessionInformation)); + this.strategy.setMaximumSessions(SessionLimit.of(1)); + this.strategy.setExceptionIfMaximumExceeded(true); + assertThatExceptionOfType(SessionAuthenticationException.class) + .isThrownBy(() -> this.strategy.onAuthentication(this.authentication, this.request, this.response)); + } + + @Test + public void maxSessionsExpireExistingUserUsingSessionLimit() { + given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) + .willReturn(Collections.singletonList(this.sessionInformation)); + this.strategy.setMaximumSessions(SessionLimit.of(1)); + this.strategy.onAuthentication(this.authentication, this.request, this.response); + assertThat(this.sessionInformation.isExpired()).isTrue(); + } + + @Test + public void maxSessionsExpireLeastRecentExistingUserUsingSessionLimit() { + SessionInformation moreRecentSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique", + new Date(1374766999999L)); + given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) + .willReturn(Arrays.asList(moreRecentSessionInfo, this.sessionInformation)); + this.strategy.setMaximumSessions(SessionLimit.of(2)); + this.strategy.onAuthentication(this.authentication, this.request, this.response); + assertThat(this.sessionInformation.isExpired()).isTrue(); + } + + @Test + public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpiredUsingSessionLimit() { + SessionInformation oldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique1", + new Date(1374766134214L)); + SessionInformation secondOldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), + "unique2", new Date(1374766134215L)); + given(this.sessionRegistry.getAllSessions(any(), anyBoolean())) + .willReturn(Arrays.asList(oldestSessionInfo, secondOldestSessionInfo, this.sessionInformation)); + this.strategy.setMaximumSessions(SessionLimit.of(2)); + this.strategy.onAuthentication(this.authentication, this.request, this.response); + assertThat(oldestSessionInfo.isExpired()).isTrue(); + assertThat(secondOldestSessionInfo.isExpired()).isTrue(); + assertThat(this.sessionInformation.isExpired()).isFalse(); + } + + @Test + public void onAuthenticationWhenSessionLimitIsUnlimited() { + this.strategy.setMaximumSessions(SessionLimit.UNLIMITED); + this.strategy.onAuthentication(this.authentication, this.request, this.response); + verifyNoInteractions(this.sessionRegistry); + } + @Test public void setMessageSourceNull() { assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setMessageSource(null)); diff --git a/web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java b/web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java new file mode 100644 index 00000000000..01df1449d79 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.session; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Claudenir Freitas + * @since 6.5 + */ +class SessionLimitTests { + + private final Authentication authentication = Mockito.mock(Authentication.class); + + @Test + void testUnlimitedInstance() { + SessionLimit sessionLimit = SessionLimit.UNLIMITED; + int result = sessionLimit.apply(this.authentication); + assertThat(result).isEqualTo(-1); + } + + @ParameterizedTest + @ValueSource(ints = { -1, 1, 2, 3 }) + void testInstanceWithValidMaxSessions(int maxSessions) { + SessionLimit sessionLimit = SessionLimit.of(maxSessions); + int result = sessionLimit.apply(this.authentication); + assertThat(result).isEqualTo(maxSessions); + } + + @Test + void testInstanceWithInvalidMaxSessions() { + assertThatIllegalArgumentException().isThrownBy(() -> SessionLimit.of(0)) + .withMessage( + "MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum"); + } + +} From 1104b45832a16f6a9b5c4ad818e893350aec0633 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:43:19 -0700 Subject: [PATCH 22/42] Polish SessionLimit - Move to the web.authentication.session package since it is only needed by web.authentication.session elements and does not access any other web element itself. - Add Kotlin support - Add documentation Issue gh-16206 --- .../SessionManagementConfigurer.java | 2 +- .../web/session/SessionConcurrencyDsl.kt | 11 +++ .../SessionManagementConfigurerTests.java | 2 +- .../config/http/HttpHeadersConfigTests.java | 2 +- .../web/session/SessionConcurrencyDslTests.kt | 69 +++++++++++++++++-- .../authentication/session-management.adoc | 57 ++++++++++++++- ...tSessionControlAuthenticationStrategy.java | 1 - .../session/SessionLimit.java | 2 +- ...ionControlAuthenticationStrategyTests.java | 1 - .../session/SessionLimitTests.java | 2 +- 10 files changed, 137 insertions(+), 12 deletions(-) rename web/src/main/java/org/springframework/security/web/{ => authentication}/session/SessionLimit.java (96%) rename web/src/test/java/org/springframework/security/web/{ => authentication}/session/SessionLimitTests.java (96%) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 5ff5b00e72c..fa601b94494 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -47,6 +47,7 @@ import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; +import org.springframework.security.web.authentication.session.SessionLimit; import org.springframework.security.web.context.DelegatingSecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.NullSecurityContextRepository; @@ -59,7 +60,6 @@ import org.springframework.security.web.session.ForceEagerSessionCreationFilter; import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.security.web.session.SessionInformationExpiredStrategy; -import org.springframework.security.web.session.SessionLimit; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy; diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt index 0d33c0702a5..ce4bc54ca5a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt @@ -19,7 +19,9 @@ package org.springframework.security.config.annotation.web.session import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer import org.springframework.security.core.session.SessionRegistry +import org.springframework.security.web.authentication.session.SessionLimit import org.springframework.security.web.session.SessionInformationExpiredStrategy +import org.springframework.util.Assert /** * A Kotlin DSL to configure the behaviour of multiple sessions using idiomatic @@ -44,12 +46,21 @@ class SessionConcurrencyDsl { var expiredSessionStrategy: SessionInformationExpiredStrategy? = null var maxSessionsPreventsLogin: Boolean? = null var sessionRegistry: SessionRegistry? = null + private var sessionLimit: SessionLimit? = null + + fun maximumSessions(max: SessionLimit) { + this.sessionLimit = max + } internal fun get(): (SessionManagementConfigurer.ConcurrencyControlConfigurer) -> Unit { + Assert.isTrue(maximumSessions == null || sessionLimit == null, "You cannot specify maximumSessions as both an Int and a SessionLimit. Please use only one.") return { sessionConcurrencyControl -> maximumSessions?.also { sessionConcurrencyControl.maximumSessions(maximumSessions!!) } + sessionLimit?.also { + sessionConcurrencyControl.maximumSessions(sessionLimit!!) + } expiredUrl?.also { sessionConcurrencyControl.expiredUrl(expiredUrl) } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index cc3011a7199..bca300ec521 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -59,12 +59,12 @@ import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy; import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionLimit; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.session.ConcurrentSessionFilter; import org.springframework.security.web.session.HttpSessionDestroyedEvent; -import org.springframework.security.web.session.SessionLimit; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; diff --git a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java index 6c89be179a4..2c41d1a3688 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java @@ -35,7 +35,7 @@ import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; -import org.springframework.security.web.session.SessionLimit; +import org.springframework.security.web.authentication.session.SessionLimit; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt index 6437c54326d..9117ae757a7 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt @@ -18,18 +18,19 @@ package org.springframework.security.config.annotation.web.session import io.mockk.every import io.mockk.mockkObject -import java.util.Date import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.mock.web.MockHttpSession +import org.springframework.security.authorization.AuthorityAuthorizationManager +import org.springframework.security.authorization.AuthorizationManager import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke import org.springframework.security.config.test.SpringTestContext import org.springframework.security.config.test.SpringTestContextExtension -import org.springframework.security.config.annotation.web.invoke import org.springframework.security.core.session.SessionInformation import org.springframework.security.core.session.SessionRegistry import org.springframework.security.core.session.SessionRegistryImpl @@ -44,6 +45,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.util.* /** * Tests for [SessionConcurrencyDsl] @@ -173,16 +175,75 @@ class SessionConcurrencyDslTests { open fun sessionRegistry(): SessionRegistry = SESSION_REGISTRY } + @Test + fun `session concurrency when session limit then no more sessions allowed`() { + this.spring.register(MaximumSessionsFunctionConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.mockMvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password")) + + this.mockMvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password")) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/login?error")) + + this.mockMvc.perform(post("/login") + .with(csrf()) + .param("username", "admin") + .param("password", "password")) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/")) + + this.mockMvc.perform(post("/login") + .with(csrf()) + .param("username", "admin") + .param("password", "password")) + .andExpect(status().isFound) + .andExpect(redirectedUrl("/")) + } + + @Configuration + @EnableWebSecurity + open class MaximumSessionsFunctionConfig { + + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + val isAdmin: AuthorizationManager = AuthorityAuthorizationManager.hasRole("ADMIN") + http { + sessionManagement { + sessionConcurrency { + maximumSessions { + authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1 + } + maxSessionsPreventsLogin = true + } + } + formLogin { } + } + return http.build() + } + + } + @Configuration open class UserDetailsConfig { @Bean open fun userDetailsService(): UserDetailsService { - val userDetails = User.withDefaultPasswordEncoder() + val user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build() - return InMemoryUserDetailsManager(userDetails) + val admin = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN") + .build() + return InMemoryUserDetailsManager(user, admin) } } } diff --git a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc index fe0821a31df..c6282e2351c 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc @@ -399,7 +399,62 @@ XML:: This will prevent a user from logging in multiple times - a second login will cause the first to be invalidated. -Using Spring Boot, you can test the above configuration scenario the following way: +You can also adjust this based on who the user is. +For example, administrators may be able to have more than one session: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) { + AuthorizationManager isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN"); + http + .sessionManagement(session -> session + .maximumSessions((authentication) -> isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1) + ); + return http.build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { + val isAdmin: AuthorizationManager<*> = AuthorityAuthorizationManager.hasRole("ADMIN") + http { + sessionManagement { + sessionConcurrency { + maximumSessions { + authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1 + } + } + } + } + return http.build() +} +---- + +XML:: ++ +[source,xml,role="secondary"] +---- + +... + + + + + + +---- +====== + +Using Spring Boot, you can test the above configurations in the following way: [tabs] ====== diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java index b8f3c9e3077..51be7bd0ab4 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java +++ b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java @@ -33,7 +33,6 @@ import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.session.ConcurrentSessionFilter; -import org.springframework.security.web.session.SessionLimit; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.util.Assert; diff --git a/web/src/main/java/org/springframework/security/web/session/SessionLimit.java b/web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java similarity index 96% rename from web/src/main/java/org/springframework/security/web/session/SessionLimit.java rename to web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java index 385c4621377..362f3a7f7d7 100644 --- a/web/src/main/java/org/springframework/security/web/session/SessionLimit.java +++ b/web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.security.web.session; +package org.springframework.security.web.authentication.session; import java.util.function.Function; diff --git a/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java index 26d4afe3efb..aa1bed6d8f8 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java @@ -34,7 +34,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; -import org.springframework.security.web.session.SessionLimit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java b/web/src/test/java/org/springframework/security/web/authentication/session/SessionLimitTests.java similarity index 96% rename from web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java rename to web/src/test/java/org/springframework/security/web/authentication/session/SessionLimitTests.java index 01df1449d79..134f9f6e7ae 100644 --- a/web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/session/SessionLimitTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.security.web.session; +package org.springframework.security.web.authentication.session; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; From ce90b85945a63d0a7c840c6cba5ed4b7236d790f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:29:33 +0000 Subject: [PATCH 23/42] Bump ch.qos.logback:logback-classic from 1.5.12 to 1.5.13 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.12 to 1.5.13. - [Commits](https://github.com/qos-ch/logback/commits) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 c2be1c30ec8..39d83b9ee9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ org-opensaml = "4.3.2" org-springframework = "6.1.16" [libraries] -ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.12" +ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.13" com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.3" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" From e188552ccbc0e07f21f6473208daf3aee7c6ef06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:51:27 +0000 Subject: [PATCH 24/42] Bump org.hibernate.orm:hibernate-core from 6.6.3.Final to 6.6.4.Final Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.3.Final to 6.6.4.Final. - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.4/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.3...6.6.4) --- updated-dependencies: - dependency-name: org.hibernate.orm:hibernate-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 e79f42a258c..fc19a1a3a63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on", org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" } org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" } org-hamcrest = "org.hamcrest:hamcrest:2.2" -org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.3.Final" +org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.4.Final" org-hsqldb = "org.hsqldb:hsqldb:2.7.4" org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25" From 4bc3693669a5719cb23fc4769a15682334ae62a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:51:20 +0000 Subject: [PATCH 25/42] Bump ch.qos.logback:logback-classic from 1.5.12 to 1.5.13 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.12 to 1.5.13. - [Commits](https://github.com/qos-ch/logback/commits) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 fc19a1a3a63..c45c80b4452 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ org-opensaml5 = "5.1.2" org-springframework = "6.2.1" [libraries] -ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.12" +ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.13" com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.2" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" From a7f1a5ebc87b04067ffce2b11d34869543609ae2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:57:31 +0000 Subject: [PATCH 26/42] Bump org.hibernate.orm:hibernate-core from 6.6.3.Final to 6.6.4.Final Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.3.Final to 6.6.4.Final. - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.4/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.3...6.6.4) --- updated-dependencies: - dependency-name: org.hibernate.orm:hibernate-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 e79f42a258c..fc19a1a3a63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on", org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" } org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" } org-hamcrest = "org.hamcrest:hamcrest:2.2" -org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.3.Final" +org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.4.Final" org-hsqldb = "org.hsqldb:hsqldb:2.7.4" org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25" From 0c12677a66cda5e877d8c9a4903e6bacf6f9daca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:57:18 +0000 Subject: [PATCH 27/42] Bump ch.qos.logback:logback-classic from 1.5.12 to 1.5.13 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.12 to 1.5.13. - [Commits](https://github.com/qos-ch/logback/commits) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 fc19a1a3a63..c45c80b4452 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ org-opensaml5 = "5.1.2" org-springframework = "6.2.1" [libraries] -ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.12" +ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.13" com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.2" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" From 7aafe2ed5ab654b5136a1764bdf86a5911ac2c63 Mon Sep 17 00:00:00 2001 From: Steven Williams Date: Thu, 14 Nov 2024 16:39:12 -0500 Subject: [PATCH 28/42] Set Saml2RelyingPartyInitiatedLogoutSuccessHandler#logoutRequestRepository Closes gh-16093 --- .../security/config/http/Saml2LogoutBeanDefinitionParser.java | 1 + 1 file changed, 1 insertion(+) diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java index 5f894cf8d8f..545f0f06c67 100644 --- a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java @@ -147,6 +147,7 @@ public BeanDefinition parse(Element element, ParserContext pc) { BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder .rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class) .addConstructorArgValue(logoutRequestResolver) + .addPropertyValue("logoutRequestRepository", logoutRequestRepository) .getBeanDefinition(); this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class) .addConstructorArgValue(saml2LogoutRequestSuccessHandler) From 643a3f12064d41b9f00cb4ed0c8986e0366cad44 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Thu, 19 Dec 2024 08:49:07 -0700 Subject: [PATCH 29/42] Test Setting logoutRequestRepository Issue gh-16093 --- .../saml2/Saml2LogoutConfigurerTests.java | 1 + .../Saml2LogoutBeanDefinitionParserTests.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java index 187c0979a2a..8926f0092b7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java @@ -484,6 +484,7 @@ public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws verify(getBean(Saml2LogoutResponseValidator.class)).validate(any()); } + // gh-11363 @Test public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception { this.spring.register(Saml2LogoutComponentsConfig.class).autowire(); diff --git a/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java index 152525d4a20..d51349440a5 100644 --- a/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java @@ -63,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; @@ -380,6 +381,22 @@ public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws verify(getBean(Saml2LogoutResponseValidator.class)).validate(any()); } + // gh-11363 + @Test + public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception { + this.spring.configLocations(this.xml("CustomComponents")).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("get"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest) + .id(this.rpLogoutRequestId) + .relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)) + .build(); + given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest); + this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf())); + verify(getBean(Saml2LogoutRequestRepository.class)).saveLogoutRequest(eq(logoutRequest), any(), any()); + } + private T getBean(Class clazz) { return this.spring.getContext().getBean(clazz); } From e9bdb5b96ea9c9b7384dac93e1177b65b3cdb016 Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Tue, 17 Dec 2024 14:00:29 +0300 Subject: [PATCH 30/42] Polish SecurityFilterChain Validation Issue gh-15982 --- .../http/DefaultFilterChainValidator.java | 36 +++++++----- .../DefaultFilterChainValidatorTests.java | 23 ++++++++ ...UnreachableFilterChainException.serialized | Bin 0 -> 759 bytes .../web/UnreachableFilterChainException.java | 55 ++++++++++++++++++ .../web/util/matcher/AndRequestMatcher.java | 20 ++++++- .../web/util/matcher/OrRequestMatcher.java | 20 ++++++- 6 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 config/src/test/resources/serialized/6.5.x/org.springframework.security.web.UnreachableFilterChainException.serialized create mode 100644 web/src/main/java/org/springframework/security/web/UnreachableFilterChainException.java diff --git a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java index ce7c50be584..74b6fbb7cab 100644 --- a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java @@ -39,6 +39,7 @@ import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.UnreachableFilterChainException; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; @@ -53,7 +54,6 @@ import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.util.matcher.AnyRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator { @@ -75,25 +75,35 @@ private void checkPathOrder(List filterChains) { // Check that the universal pattern is listed at the end, if at all Iterator chains = filterChains.iterator(); while (chains.hasNext()) { - RequestMatcher matcher = ((DefaultSecurityFilterChain) chains.next()).getRequestMatcher(); - if (AnyRequestMatcher.INSTANCE.equals(matcher) && chains.hasNext()) { - throw new IllegalArgumentException("A universal match pattern ('/**') is defined " - + " before other patterns in the filter chain, causing them to be ignored. Please check the " - + "ordering in your namespace or FilterChainProxy bean configuration"); + if (chains.next() instanceof DefaultSecurityFilterChain securityFilterChain) { + if (AnyRequestMatcher.INSTANCE.equals(securityFilterChain.getRequestMatcher()) && chains.hasNext()) { + throw new UnreachableFilterChainException("A universal match pattern ('/**') is defined " + + " before other patterns in the filter chain, causing them to be ignored. Please check the " + + "ordering in your namespace or FilterChainProxy bean configuration", + securityFilterChain, chains.next()); + } } } } private void checkForDuplicateMatchers(List chains) { - while (chains.size() > 1) { - DefaultSecurityFilterChain chain = (DefaultSecurityFilterChain) chains.remove(0); - for (SecurityFilterChain test : chains) { - if (chain.getRequestMatcher().equals(((DefaultSecurityFilterChain) test).getRequestMatcher())) { - throw new IllegalArgumentException("The FilterChainProxy contains two filter chains using the" - + " matcher " + chain.getRequestMatcher() + ". If you are using multiple namespace " - + "elements, you must use a 'pattern' attribute to define the request patterns to which they apply."); + DefaultSecurityFilterChain filterChain = null; + for (SecurityFilterChain chain : chains) { + if (filterChain != null) { + if (chain instanceof DefaultSecurityFilterChain defaultChain) { + if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) { + throw new UnreachableFilterChainException( + "The FilterChainProxy contains two filter chains using the" + " matcher " + + defaultChain.getRequestMatcher() + + ". If you are using multiple namespace " + + "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.", + defaultChain, chain); + } } } + if (chain instanceof DefaultSecurityFilterChain defaultChain) { + filterChain = defaultChain; + } } } diff --git a/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java b/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java index a5b899db48c..f120b9d0581 100644 --- a/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java +++ b/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java @@ -16,7 +16,9 @@ package org.springframework.security.config.http; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -33,6 +35,8 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.UnreachableFilterChainException; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; @@ -40,9 +44,11 @@ import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; @@ -130,4 +136,21 @@ public void validateCustomMetadataSource() { verify(customMetaDataSource, atLeastOnce()).getAttributes(any()); } + @Test + void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainException() { + AnonymousAuthenticationFilter authenticationFilter = mock(AnonymousAuthenticationFilter.class); + ExceptionTranslationFilter exceptionTranslationFilter = mock(ExceptionTranslationFilter.class); + SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"), + authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor); + SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"), + authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor); + List chains = new ArrayList<>(); + chains.add(chain2); + chains.add(chain1); + FilterChainProxy proxy = new FilterChainProxy(chains); + + assertThatExceptionOfType(UnreachableFilterChainException.class) + .isThrownBy(() -> this.validator.validate(proxy)); + } + } diff --git a/config/src/test/resources/serialized/6.5.x/org.springframework.security.web.UnreachableFilterChainException.serialized b/config/src/test/resources/serialized/6.5.x/org.springframework.security.web.UnreachableFilterChainException.serialized new file mode 100644 index 0000000000000000000000000000000000000000..418c3b8ece1a2ae8506494fb0533fd7574991846 GIT binary patch literal 759 zcmZvaziSjh6vtmy^TU%E(P$AxA%%j)Zli&4A*dmdz?#B~HYsFo_ubwkv%8ZwGu~M! zf>>DDTlg2)3Ko`$jg26dmX`hr0pHx^_U?=W)4cb2-}m$5ADA=*cT37bqe&K7i$so{ zlJ-J_i6d#BhaGH&j|)PPv_(_lhEx`5tu17+-c3+jSr*WHfeIjq;cRP2SXhc#Jo-mG z8i+?M9yN|LjVRY+xa|I$b01#9M3-Q3S3DD;5=9nnDutO)_b78Y7PhMD(_8c7MeE&{ z0DuD)h5?UUVP%dL_~Gf)gWB!azx&|DAy^ULkFKvS)UO_#Ahz37cKld=e!X*Zx%2tk zDL5U&WFnkF3zuTA75OToisasAQ?x>=hiHt*n7c5-HLTx5eFtjBiezt_M8d?ioiSwK zSXby?YKdEO5)^O{5s+5+#g@OOYaN@sasOPRDRA zMRxd>*S!DOI>R@FBUCL%+b+{FOPRVcW;X9xo*)@M$(|6`{)(&DmcKX5aq~QYzea`w AHUIzs literal 0 HcmV?d00001 diff --git a/web/src/main/java/org/springframework/security/web/UnreachableFilterChainException.java b/web/src/main/java/org/springframework/security/web/UnreachableFilterChainException.java new file mode 100644 index 00000000000..ff697bd07d9 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/UnreachableFilterChainException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web; + +import org.springframework.security.core.SpringSecurityCoreVersion; + +/** + * Thrown if {@link SecurityFilterChain securityFilterChain} is not valid. + * + * @author Max Batischev + * @since 6.5 + */ +public class UnreachableFilterChainException extends IllegalArgumentException { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private final SecurityFilterChain filterChain; + + private final SecurityFilterChain unreachableFilterChain; + + /** + * Constructs an UnreachableFilterChainException with the specified + * message. + * @param message the detail message + */ + public UnreachableFilterChainException(String message, SecurityFilterChain filterChain, + SecurityFilterChain unreachableFilterChain) { + super(message); + this.filterChain = filterChain; + this.unreachableFilterChain = unreachableFilterChain; + } + + public SecurityFilterChain getFilterChain() { + return this.filterChain; + } + + public SecurityFilterChain getUnreachableFilterChain() { + return this.unreachableFilterChain; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java index b28b69bbba2..b585db8eceb 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -90,6 +91,23 @@ public MatchResult matcher(HttpServletRequest request) { return MatchResult.match(variables); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AndRequestMatcher that = (AndRequestMatcher) o; + return Objects.equals(this.requestMatchers, that.requestMatchers); + } + + @Override + public int hashCode() { + return Objects.hash(this.requestMatchers); + } + @Override public String toString() { return "And " + this.requestMatchers; diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java index e3add8edf3a..53c0af8d922 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Objects; import jakarta.servlet.http.HttpServletRequest; @@ -81,6 +82,23 @@ public MatchResult matcher(HttpServletRequest request) { return MatchResult.notMatch(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OrRequestMatcher that = (OrRequestMatcher) o; + return Objects.equals(this.requestMatchers, that.requestMatchers); + } + + @Override + public int hashCode() { + return Objects.hash(this.requestMatchers); + } + @Override public String toString() { return "Or " + this.requestMatchers; From e257af8854ef5c05a4732c95b9f0201f32860012 Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Tue, 17 Dec 2024 14:00:47 +0300 Subject: [PATCH 31/42] Add Support Same Request Matchers Checking Closes gh-15982 --- .../WebSecurityFilterChainValidator.java | 29 +++++- .../WebSecurityFilterChainValidatorTests.java | 88 +++++++++++++++++++ .../WebSecurityConfigurationTests.java | 2 +- 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java index cc11cdef400..5e069a446e4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java @@ -21,11 +21,14 @@ import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.UnreachableFilterChainException; import org.springframework.security.web.util.matcher.AnyRequestMatcher; /** * A filter chain validator for filter chains built by {@link WebSecurity} * + * @author Josh Cummings + * @author Max Batischev * @since 6.5 */ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterChainValidator { @@ -33,13 +36,18 @@ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterCh @Override public void validate(FilterChainProxy filterChainProxy) { List chains = filterChainProxy.getFilterChains(); + checkForAnyRequestRequestMatcher(chains); + checkForDuplicateMatchers(chains); + } + + private void checkForAnyRequestRequestMatcher(List chains) { DefaultSecurityFilterChain anyRequestFilterChain = null; for (SecurityFilterChain chain : chains) { if (anyRequestFilterChain != null) { String message = "A filter chain that matches any request [" + anyRequestFilterChain + "] has already been configured, which means that this filter chain [" + chain + "] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last."; - throw new IllegalArgumentException(message); + throw new UnreachableFilterChainException(message, anyRequestFilterChain, chain); } if (chain instanceof DefaultSecurityFilterChain defaultChain) { if (defaultChain.getRequestMatcher() instanceof AnyRequestMatcher) { @@ -49,4 +57,23 @@ public void validate(FilterChainProxy filterChainProxy) { } } + private void checkForDuplicateMatchers(List chains) { + DefaultSecurityFilterChain filterChain = null; + for (SecurityFilterChain chain : chains) { + if (filterChain != null) { + if (chain instanceof DefaultSecurityFilterChain defaultChain) { + if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) { + throw new UnreachableFilterChainException( + "The FilterChainProxy contains two filter chains using the" + " matcher " + + defaultChain.getRequestMatcher(), + filterChain, defaultChain); + } + } + } + if (chain instanceof DefaultSecurityFilterChain defaultChain) { + filterChain = defaultChain; + } + } + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java new file mode 100644 index 00000000000..edc0be90a6f --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.config.annotation.web.builders; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.UnreachableFilterChainException; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link WebSecurityFilterChainValidator} + * + * @author Max Batischev + */ +@ExtendWith(MockitoExtension.class) +public class WebSecurityFilterChainValidatorTests { + + private final WebSecurityFilterChainValidator validator = new WebSecurityFilterChainValidator(); + + @Mock + private AnonymousAuthenticationFilter authenticationFilter; + + @Mock + private ExceptionTranslationFilter exceptionTranslationFilter; + + @Mock + private FilterSecurityInterceptor authorizationInterceptor; + + @Test + void validateWhenAnyRequestMatcherIsPresentThenUnreachableFilterChainException() { + SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"), + this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor); + SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE, + this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor); + List chains = new ArrayList<>(); + chains.add(chain2); + chains.add(chain1); + FilterChainProxy proxy = new FilterChainProxy(chains); + + assertThatExceptionOfType(UnreachableFilterChainException.class) + .isThrownBy(() -> this.validator.validate(proxy)); + } + + @Test + void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainException() { + SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"), + this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor); + SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"), + this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor); + List chains = new ArrayList<>(); + chains.add(chain2); + chains.add(chain1); + FilterChainProxy proxy = new FilterChainProxy(chains); + + assertThatExceptionOfType(UnreachableFilterChainException.class) + .isThrownBy(() -> this.validator.validate(proxy)); + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index 326e2bda108..f6a53bff458 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -323,7 +323,7 @@ public void loadConfigWhenTwoSecurityFilterChainsPresentAndSecondWithAnyRequestT assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> this.spring.register(MultipleAnyRequestSecurityFilterChainConfig.class).autowire()) .havingRootCause() - .isExactlyInstanceOf(IllegalArgumentException.class); + .isInstanceOf(IllegalArgumentException.class); } private void assertAnotherUserPermission(WebInvocationPrivilegeEvaluator privilegeEvaluator) { From 624a8fb252f35ca939966b2c908b89fe5ac06652 Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Thu, 19 Dec 2024 18:17:16 +0300 Subject: [PATCH 32/42] Add Alerting About Deprecated Authorize Config Closes gh-16213 --- .../WebSecurityFilterChainValidator.java | 34 +++++++++++++++++++ .../http/DefaultFilterChainValidator.java | 26 ++++++++++++++ .../WebSecurityFilterChainValidatorTests.java | 10 ++++++ .../DefaultFilterChainValidatorTests.java | 6 ++++ 4 files changed, 76 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java index 5e069a446e4..b0bf43fb3b7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java @@ -18,10 +18,16 @@ import java.util.List; +import jakarta.servlet.Filter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.UnreachableFilterChainException; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.util.matcher.AnyRequestMatcher; /** @@ -33,11 +39,14 @@ */ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterChainValidator { + private final Log logger = LogFactory.getLog(getClass()); + @Override public void validate(FilterChainProxy filterChainProxy) { List chains = filterChainProxy.getFilterChains(); checkForAnyRequestRequestMatcher(chains); checkForDuplicateMatchers(chains); + checkAuthorizationFilters(chains); } private void checkForAnyRequestRequestMatcher(List chains) { @@ -76,4 +85,29 @@ private void checkForDuplicateMatchers(List chains) { } } + private void checkAuthorizationFilters(List chains) { + Filter authorizationFilter = null; + Filter filterSecurityInterceptor = null; + for (SecurityFilterChain chain : chains) { + for (Filter filter : chain.getFilters()) { + if (filter instanceof AuthorizationFilter) { + authorizationFilter = filter; + } + if (filter instanceof FilterSecurityInterceptor) { + filterSecurityInterceptor = filter; + } + } + if (authorizationFilter != null && filterSecurityInterceptor != null) { + this.logger.warn( + "It is not recommended to use authorizeRequests in the configuration. Please only use authorizeHttpRequests"); + } + if (filterSecurityInterceptor != null) { + this.logger.warn( + "Usage of authorizeRequests is deprecated. Please use authorizeHttpRequests in the configuration"); + } + authorizationFilter = null; + filterSecurityInterceptor = null; + } + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java index 74b6fbb7cab..8f2baeb4c68 100644 --- a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java @@ -69,6 +69,7 @@ public void validate(FilterChainProxy fcp) { } checkPathOrder(new ArrayList<>(fcp.getFilterChains())); checkForDuplicateMatchers(new ArrayList<>(fcp.getFilterChains())); + checkAuthorizationFilters(new ArrayList<>(fcp.getFilterChains())); } private void checkPathOrder(List filterChains) { @@ -107,6 +108,31 @@ private void checkForDuplicateMatchers(List chains) { } } + private void checkAuthorizationFilters(List chains) { + Filter authorizationFilter = null; + Filter filterSecurityInterceptor = null; + for (SecurityFilterChain chain : chains) { + for (Filter filter : chain.getFilters()) { + if (filter instanceof AuthorizationFilter) { + authorizationFilter = filter; + } + if (filter instanceof FilterSecurityInterceptor) { + filterSecurityInterceptor = filter; + } + } + if (authorizationFilter != null && filterSecurityInterceptor != null) { + this.logger.warn( + "It is not recommended to use authorizeRequests in the configuration. Please only use authorizeHttpRequests"); + } + if (filterSecurityInterceptor != null) { + this.logger.warn( + "Usage of authorizeRequests is deprecated. Please use authorizeHttpRequests in the configuration"); + } + authorizationFilter = null; + filterSecurityInterceptor = null; + } + } + @SuppressWarnings({ "unchecked" }) private F getFilter(Class type, List filters) { for (Filter f : filters) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java index edc0be90a6f..2e6984ab9b3 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java @@ -35,6 +35,7 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link WebSecurityFilterChainValidator} @@ -55,6 +56,15 @@ public class WebSecurityFilterChainValidatorTests { @Mock private FilterSecurityInterceptor authorizationInterceptor; + @Test + void validateWhenFilterSecurityInterceptorConfiguredThenValidates() { + SecurityFilterChain chain = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"), + this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor); + FilterChainProxy proxy = new FilterChainProxy(List.of(chain)); + + assertThatNoException().isThrownBy(() -> this.validator.validate(proxy)); + } + @Test void validateWhenAnyRequestMatcherIsPresentThenUnreachableFilterChainException() { SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"), diff --git a/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java b/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java index f120b9d0581..d75ce815d58 100644 --- a/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java +++ b/config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java @@ -49,6 +49,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; @@ -103,6 +104,11 @@ public void setUp() { ReflectionTestUtils.setField(this.validator, "logger", this.logger); } + @Test + void validateWhenFilterSecurityInterceptorConfiguredThenValidates() { + assertThatNoException().isThrownBy(() -> this.validator.validate(this.chain)); + } + // SEC-1878 @SuppressWarnings("unchecked") @Test From 9ae432f0d216f16a869c1e21be3dfb8575f3898a Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:40:50 -0700 Subject: [PATCH 33/42] Add Filter Chain Validation Test Issue gh-15982 --- .../WebSecurityFilterChainValidatorTests.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java index 2e6984ab9b3..450a3dfdc17 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java @@ -33,6 +33,8 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatchers; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -95,4 +97,23 @@ void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainExceptio .isThrownBy(() -> this.validator.validate(proxy)); } + @Test + void validateWhenSameComposedRequestMatchersArePresentThenUnreachableFilterChainException() { + RequestMatcher matcher1 = RequestMatchers.anyOf(RequestMatchers.allOf(AntPathRequestMatcher.antMatcher("/api"), + AntPathRequestMatcher.antMatcher("*.do")), AntPathRequestMatcher.antMatcher("/admin")); + RequestMatcher matcher2 = RequestMatchers.anyOf(RequestMatchers.allOf(AntPathRequestMatcher.antMatcher("/api"), + AntPathRequestMatcher.antMatcher("*.do")), AntPathRequestMatcher.antMatcher("/admin")); + SecurityFilterChain chain1 = new DefaultSecurityFilterChain(matcher1, this.authenticationFilter, + this.exceptionTranslationFilter, this.authorizationInterceptor); + SecurityFilterChain chain2 = new DefaultSecurityFilterChain(matcher2, this.authenticationFilter, + this.exceptionTranslationFilter, this.authorizationInterceptor); + List chains = new ArrayList<>(); + chains.add(chain2); + chains.add(chain1); + FilterChainProxy proxy = new FilterChainProxy(chains); + + assertThatExceptionOfType(UnreachableFilterChainException.class) + .isThrownBy(() -> this.validator.validate(proxy)); + } + } From 95ec49a21df1cc4bdf8e1123e8a3ecfdf6991aa2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Dec 2024 01:10:41 +0000 Subject: [PATCH 34/42] Support Meta-Annotation Parameters on Parameter Annotations Closes gh-16248 --- .../UniqueSecurityAnnotationScanner.java | 54 ++++++++- .../UniqueSecurityAnnotationScannerTests.java | 104 ++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java index 5093e1bd1d1..e3f5c9297c6 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java @@ -18,6 +18,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -83,6 +84,7 @@ * * @param the annotation to search for and synthesize * @author Josh Cummings + * @author DingHao * @since 6.4 */ final class UniqueSecurityAnnotationScanner extends AbstractSecurityAnnotationScanner { @@ -107,7 +109,7 @@ final class UniqueSecurityAnnotationScanner extends Abstra MergedAnnotation merge(AnnotatedElement element, Class targetClass) { if (element instanceof Parameter parameter) { return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> { - List> annotations = findDirectAnnotations(p); + List> annotations = findParameterAnnotations(p); return requireUnique(p, annotations); }); } @@ -137,6 +139,56 @@ private MergedAnnotation requireUnique(AnnotatedElement element, List> findParameterAnnotations(Parameter current) { + List> directAnnotations = findDirectAnnotations(current); + if (!directAnnotations.isEmpty()) { + return directAnnotations; + } + Executable executable = current.getDeclaringExecutable(); + if (executable instanceof Method method) { + Class clazz = method.getDeclaringClass(); + Set> visited = new HashSet<>(); + while (clazz != null && clazz != Object.class) { + directAnnotations = findClosestParameterAnnotations(method, clazz, current, visited); + if (!directAnnotations.isEmpty()) { + return directAnnotations; + } + clazz = clazz.getSuperclass(); + } + } + return Collections.emptyList(); + } + + private List> findClosestParameterAnnotations(Method method, Class clazz, Parameter current, + Set> visited) { + if (!visited.add(clazz)) { + return Collections.emptyList(); + } + List> annotations = new ArrayList<>(findDirectParameterAnnotations(method, clazz, current)); + for (Class ifc : clazz.getInterfaces()) { + annotations.addAll(findClosestParameterAnnotations(method, ifc, current, visited)); + } + return annotations; + } + + private List> findDirectParameterAnnotations(Method method, Class clazz, Parameter current) { + try { + Method methodToUse = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); + for (Parameter parameter : methodToUse.getParameters()) { + if (parameter.getName().equals(current.getName())) { + List> directAnnotations = findDirectAnnotations(parameter); + if (!directAnnotations.isEmpty()) { + return directAnnotations; + } + } + } + } + catch (NoSuchMethodException ex) { + // move on + } + return Collections.emptyList(); + } + private List> findMethodAnnotations(Method method, Class targetClass) { // The method may be on an interface, but we need attributes from the target // class. diff --git a/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java b/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java index b1a7a779aac..976e0879ab1 100644 --- a/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java +++ b/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java @@ -16,7 +16,13 @@ package org.springframework.security.core.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.List; import org.junit.jupiter.api.Test; @@ -34,6 +40,9 @@ public class UniqueSecurityAnnotationScannerTests { private UniqueSecurityAnnotationScanner scanner = new UniqueSecurityAnnotationScanner<>( PreAuthorize.class); + private UniqueSecurityAnnotationScanner parameterScanner = new UniqueSecurityAnnotationScanner<>( + CustomParameterAnnotation.class); + @Test void scanWhenAnnotationOnInterfaceThenResolves() throws Exception { Method method = AnnotationOnInterface.class.getDeclaredMethod("method"); @@ -251,6 +260,101 @@ void scanWhenClassInheritingAbstractClassNoAnnotationsThenNoAnnotation() throws assertThat(preAuthorize).isNull(); } + @Test + void scanParameterAnnotationWhenAnnotationOnInterface() throws Exception { + Parameter parameter = UserService.class.getDeclaredMethod("add", String.class).getParameters()[0]; + CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter); + assertThat(customParameterAnnotation.value()).isEqualTo("one"); + } + + @Test + void scanParameterAnnotationWhenClassInheritingInterfaceAnnotation() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("add", String.class).getParameters()[0]; + CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter); + assertThat(customParameterAnnotation.value()).isEqualTo("one"); + } + + @Test + void scanParameterAnnotationWhenClassOverridingMethodOverridingInterface() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("get", String.class).getParameters()[0]; + CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter); + assertThat(customParameterAnnotation.value()).isEqualTo("five"); + } + + @Test + void scanParameterAnnotationWhenMultipleMethodInheritanceThenException() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("list", String.class).getParameters()[0]; + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> this.parameterScanner.scan(parameter)); + } + + @Test + void scanParameterAnnotationWhenInterfaceNoAnnotationsThenException() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("delete", String.class).getParameters()[0]; + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> this.parameterScanner.scan(parameter)); + } + + interface UserService { + + void add(@CustomParameterAnnotation("one") String user); + + List list(@CustomParameterAnnotation("two") String user); + + String get(@CustomParameterAnnotation("three") String user); + + void delete(@CustomParameterAnnotation("five") String user); + + } + + interface OtherUserService { + + List list(@CustomParameterAnnotation("four") String user); + + } + + interface ThirdPartyUserService { + + void delete(@CustomParameterAnnotation("five") String user); + + } + + interface RemoteUserService extends ThirdPartyUserService { + + } + + static class UserServiceImpl implements UserService, OtherUserService, RemoteUserService { + + @Override + public void add(String user) { + + } + + @Override + public List list(String user) { + return List.of(user); + } + + @Override + public String get(@CustomParameterAnnotation("five") String user) { + return user; + } + + @Override + public void delete(String user) { + + } + + } + + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @interface CustomParameterAnnotation { + + String value(); + + } + @PreAuthorize("one") private interface AnnotationOnInterface { From 15faf0f6211b054055d6b98c4d4ad3b4b16d0761 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 03:34:59 +0000 Subject: [PATCH 35/42] Bump ch.qos.logback:logback-classic from 1.5.13 to 1.5.14 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.13 to 1.5.14. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.13...v_1.5.14) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 39d83b9ee9a..62d2851612e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ org-opensaml = "4.3.2" org-springframework = "6.1.16" [libraries] -ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.13" +ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.14" com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.3" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" From e63b031b9b1815096de56034dd1e7b8902684e33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 03:46:22 +0000 Subject: [PATCH 36/42] Bump ch.qos.logback:logback-classic from 1.5.13 to 1.5.14 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.13 to 1.5.14. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.13...v_1.5.14) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 c45c80b4452..de4de91e0e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ org-opensaml5 = "5.1.2" org-springframework = "6.2.1" [libraries] -ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.13" +ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.14" com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.2" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" From 5c812d49babb7786a9f43e8093b0035cef04e660 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 04:02:20 +0000 Subject: [PATCH 37/42] Bump ch.qos.logback:logback-classic from 1.5.13 to 1.5.14 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.13 to 1.5.14. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.13...v_1.5.14) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 c45c80b4452..de4de91e0e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ org-opensaml5 = "5.1.2" org-springframework = "6.2.1" [libraries] -ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.13" +ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.14" com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.2" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" From 82272dac254b694c5f16b9ff7529f477b1bbd452 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 04:02:01 +0000 Subject: [PATCH 38/42] Bump org.jetbrains.kotlinx:kotlinx-coroutines-bom from 1.9.0 to 1.10.0 Bumps [org.jetbrains.kotlinx:kotlinx-coroutines-bom](https://github.com/Kotlin/kotlinx.coroutines) from 1.9.0 to 1.10.0. - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.9.0...1.10.0) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 de4de91e0e7..7de738f4737 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ org-aspectj = "1.9.22.1" org-bouncycastle = "1.79" org-eclipse-jetty = "11.0.24" org-jetbrains-kotlin = "1.9.25" -org-jetbrains-kotlinx = "1.9.0" +org-jetbrains-kotlinx = "1.10.0" org-mockito = "5.14.2" org-opensaml = "4.3.2" org-opensaml5 = "5.1.2" From c72359bf4bd84e5581e6b318bb1eb0af11f8ed0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 04:02:06 +0000 Subject: [PATCH 39/42] Bump org.assertj:assertj-core from 3.26.3 to 3.27.0 Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.26.3 to 3.27.0. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.26.3...assertj-build-3.27.0) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 7de738f4737..6dd0c0fd157 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ org-apereo-cas-client-cas-client-core = "org.apereo.cas.client:cas-client-core:4 io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:8.11" org-aspectj-aspectjrt = { module = "org.aspectj:aspectjrt", version.ref = "org-aspectj" } org-aspectj-aspectjweaver = { module = "org.aspectj:aspectjweaver", version.ref = "org-aspectj" } -org-assertj-assertj-core = "org.assertj:assertj-core:3.26.3" +org-assertj-assertj-core = "org.assertj:assertj-core:3.27.0" org-bouncycastle-bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "org-bouncycastle" } org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "org-bouncycastle" } org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" } From 38523faaa09b66e15b68fa4c3d74d278b1c2c61d Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Fri, 20 Dec 2024 11:02:24 +0300 Subject: [PATCH 40/42] Remove Unused loggers Closes gh-16319 --- .../security/web/util/matcher/AndRequestMatcher.java | 4 ---- .../security/web/util/matcher/NegatedRequestMatcher.java | 4 ---- 2 files changed, 8 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java index b585db8eceb..0630629cf25 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java @@ -23,8 +23,6 @@ import java.util.Objects; import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; @@ -37,8 +35,6 @@ */ public final class AndRequestMatcher implements RequestMatcher { - private final Log logger = LogFactory.getLog(getClass()); - private final List requestMatchers; /** diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/NegatedRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/NegatedRequestMatcher.java index 61da5dca623..60b7c32647b 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/NegatedRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/NegatedRequestMatcher.java @@ -17,8 +17,6 @@ package org.springframework.security.web.util.matcher; import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; @@ -33,8 +31,6 @@ */ public class NegatedRequestMatcher implements RequestMatcher { - private final Log logger = LogFactory.getLog(getClass()); - private final RequestMatcher requestMatcher; /** From 7b07ef5ff378b201d8a59e0316d7d7eee2da23a7 Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Fri, 13 Dec 2024 17:27:40 +0300 Subject: [PATCH 41/42] Add Support JdbcUserCredentialRepository Closes gh-16224 --- .../aot/hint/UserCredentialRuntimeHints.java | 40 +++ .../JdbcUserCredentialRepository.java | 305 ++++++++++++++++++ .../resources/META-INF/spring/aot.factories | 3 +- .../security/user-credentials-schema.sql | 18 ++ .../hint/UserCredentialRuntimeHintsTests.java | 59 ++++ .../webauthn/api/TestCredentialRecord.java | 21 ++ .../JdbcUserCredentialRepositoryTests.java | 180 +++++++++++ 7 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 web/src/main/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHints.java create mode 100644 web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java create mode 100644 web/src/main/resources/org/springframework/security/user-credentials-schema.sql create mode 100644 web/src/test/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHintsTests.java create mode 100644 web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java diff --git a/web/src/main/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHints.java b/web/src/main/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHints.java new file mode 100644 index 00000000000..c3b4c95a148 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHints.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.aot.hint; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.security.web.webauthn.api.CredentialRecord; +import org.springframework.security.web.webauthn.management.UserCredentialRepository; + +/** + * + * A JDBC implementation of an {@link UserCredentialRepository} that uses a + * {@link JdbcOperations} for {@link CredentialRecord} persistence. + * + * @author Max Batischev + * @since 6.5 + */ +class UserCredentialRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("org/springframework/security/user-credentials-schema.sql"); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java b/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java new file mode 100644 index 00000000000..aa012d6964b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java @@ -0,0 +1,305 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.webauthn.management; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.security.web.webauthn.api.AuthenticatorTransport; +import org.springframework.security.web.webauthn.api.Bytes; +import org.springframework.security.web.webauthn.api.CredentialRecord; +import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord; +import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCose; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialType; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * A JDBC implementation of an {@link UserCredentialRepository} that uses a + * {@link JdbcOperations} for {@link CredentialRecord} persistence. + * + * NOTE: This {@code UserCredentialRepository} depends on the table definition + * described in "classpath:org/springframework/security/user-credentials-schema.sql" and + * therefore MUST be defined in the database schema. + * + * @author Max Batischev + * @since 6.5 + * @see UserCredentialRepository + * @see CredentialRecord + * @see JdbcOperations + * @see RowMapper + */ +public final class JdbcUserCredentialRepository implements UserCredentialRepository { + + private RowMapper credentialRecordRowMapper = new CredentialRecordRowMapper(); + + private Function> credentialRecordParametersMapper = new CredentialRecordParametersMapper(); + + private LobHandler lobHandler = new DefaultLobHandler(); + + private final JdbcOperations jdbcOperations; + + private static final String TABLE_NAME = "user_credentials"; + + // @formatter:off + private static final String COLUMN_NAMES = "credential_id, " + + "user_entity_user_id, " + + "public_key, " + + "signature_count, " + + "uv_initialized, " + + "backup_eligible, " + + "authenticator_transports, " + + "public_key_credential_type, " + + "backup_state, " + + "attestation_object, " + + "attestation_client_data_json, " + + "created, " + + "last_used, " + + "label "; + // @formatter:on + + // @formatter:off + private static final String SAVE_CREDENTIAL_RECORD_SQL = "INSERT INTO " + TABLE_NAME + + " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + // @formatter:on + + private static final String ID_FILTER = "credential_id = ? "; + + private static final String USER_ID_FILTER = "user_entity_user_id = ? "; + + // @formatter:off + private static final String FIND_CREDENTIAL_RECORD_BY_ID_SQL = "SELECT " + COLUMN_NAMES + + " FROM " + TABLE_NAME + + " WHERE " + ID_FILTER; + // @formatter:on + + // @formatter:off + private static final String FIND_CREDENTIAL_RECORD_BY_USER_ID_SQL = "SELECT " + COLUMN_NAMES + + " FROM " + TABLE_NAME + + " WHERE " + USER_ID_FILTER; + // @formatter:on + + private static final String DELETE_CREDENTIAL_RECORD_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + ID_FILTER; + + /** + * Constructs a {@code JdbcUserCredentialRepository} using the provided parameters. + * @param jdbcOperations the JDBC operations + */ + public JdbcUserCredentialRepository(JdbcOperations jdbcOperations) { + Assert.notNull(jdbcOperations, "jdbcOperations cannot be null"); + this.jdbcOperations = jdbcOperations; + } + + @Override + public void delete(Bytes credentialId) { + Assert.notNull(credentialId, "credentialId cannot be null"); + SqlParameterValue[] parameters = new SqlParameterValue[] { + new SqlParameterValue(Types.VARCHAR, credentialId.toBase64UrlString()), }; + PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters); + this.jdbcOperations.update(DELETE_CREDENTIAL_RECORD_SQL, pss); + } + + @Override + public void save(CredentialRecord record) { + Assert.notNull(record, "record cannot be null"); + List parameters = this.credentialRecordParametersMapper.apply(record); + try (LobCreator lobCreator = this.lobHandler.getLobCreator()) { + PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator, + parameters.toArray()); + this.jdbcOperations.update(SAVE_CREDENTIAL_RECORD_SQL, pss); + } + } + + @Override + public CredentialRecord findByCredentialId(Bytes credentialId) { + Assert.notNull(credentialId, "credentialId cannot be null"); + List result = this.jdbcOperations.query(FIND_CREDENTIAL_RECORD_BY_ID_SQL, + this.credentialRecordRowMapper, credentialId.toBase64UrlString()); + return !result.isEmpty() ? result.get(0) : null; + } + + @Override + public List findByUserId(Bytes userId) { + Assert.notNull(userId, "userId cannot be null"); + return this.jdbcOperations.query(FIND_CREDENTIAL_RECORD_BY_USER_ID_SQL, this.credentialRecordRowMapper, + userId.toBase64UrlString()); + } + + /** + * Sets a {@link LobHandler} for large binary fields and large text field parameters. + * @param lobHandler the lob handler + */ + public void setLobHandler(LobHandler lobHandler) { + Assert.notNull(lobHandler, "lobHandler cannot be null"); + this.lobHandler = lobHandler; + } + + private static class CredentialRecordParametersMapper + implements Function> { + + @Override + public List apply(CredentialRecord record) { + List parameters = new ArrayList<>(); + + List transports = new ArrayList<>(); + if (!CollectionUtils.isEmpty(record.getTransports())) { + for (AuthenticatorTransport transport : record.getTransports()) { + transports.add(transport.getValue()); + } + } + + parameters.add(new SqlParameterValue(Types.VARCHAR, record.getCredentialId().toBase64UrlString())); + parameters.add(new SqlParameterValue(Types.VARCHAR, record.getUserEntityUserId().toBase64UrlString())); + parameters.add(new SqlParameterValue(Types.BLOB, record.getPublicKey().getBytes())); + parameters.add(new SqlParameterValue(Types.BIGINT, record.getSignatureCount())); + parameters.add(new SqlParameterValue(Types.BOOLEAN, record.isUvInitialized())); + parameters.add(new SqlParameterValue(Types.BOOLEAN, record.isBackupEligible())); + parameters.add(new SqlParameterValue(Types.VARCHAR, + (!CollectionUtils.isEmpty(record.getTransports())) ? String.join(",", transports) : "")); + parameters.add(new SqlParameterValue(Types.VARCHAR, + (record.getCredentialType() != null) ? record.getCredentialType().getValue() : null)); + parameters.add(new SqlParameterValue(Types.BOOLEAN, record.isBackupState())); + parameters.add(new SqlParameterValue(Types.BLOB, + (record.getAttestationObject() != null) ? record.getAttestationObject().getBytes() : null)); + parameters.add(new SqlParameterValue(Types.BLOB, (record.getAttestationClientDataJSON() != null) + ? record.getAttestationClientDataJSON().getBytes() : null)); + parameters.add(new SqlParameterValue(Types.TIMESTAMP, fromInstant(record.getCreated()))); + parameters.add(new SqlParameterValue(Types.TIMESTAMP, fromInstant(record.getLastUsed()))); + parameters.add(new SqlParameterValue(Types.VARCHAR, record.getLabel())); + + return parameters; + } + + private Timestamp fromInstant(Instant instant) { + if (instant == null) { + return null; + } + return Timestamp.from(instant); + } + + } + + private static final class LobCreatorArgumentPreparedStatementSetter extends ArgumentPreparedStatementSetter { + + private final LobCreator lobCreator; + + private LobCreatorArgumentPreparedStatementSetter(LobCreator lobCreator, Object[] args) { + super(args); + this.lobCreator = lobCreator; + } + + @Override + protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException { + if (argValue instanceof SqlParameterValue paramValue) { + if (paramValue.getSqlType() == Types.BLOB) { + if (paramValue.getValue() != null) { + Assert.isInstanceOf(byte[].class, paramValue.getValue(), + "Value of blob parameter must be byte[]"); + } + byte[] valueBytes = (byte[]) paramValue.getValue(); + this.lobCreator.setBlobAsBytes(ps, parameterPosition, valueBytes); + return; + } + } + super.doSetValue(ps, parameterPosition, argValue); + } + + } + + private static class CredentialRecordRowMapper implements RowMapper { + + private LobHandler lobHandler = new DefaultLobHandler(); + + @Override + public CredentialRecord mapRow(ResultSet rs, int rowNum) throws SQLException { + Bytes credentialId = Bytes.fromBase64(new String(rs.getString("credential_id").getBytes())); + Bytes userEntityUserId = Bytes.fromBase64(new String(rs.getString("user_entity_user_id").getBytes())); + ImmutablePublicKeyCose publicKey = new ImmutablePublicKeyCose( + this.lobHandler.getBlobAsBytes(rs, "public_key")); + long signatureCount = rs.getLong("signature_count"); + boolean uvInitialized = rs.getBoolean("uv_initialized"); + boolean backupEligible = rs.getBoolean("backup_eligible"); + PublicKeyCredentialType credentialType = PublicKeyCredentialType + .valueOf(rs.getString("public_key_credential_type")); + boolean backupState = rs.getBoolean("backup_state"); + + Bytes attestationObject = null; + byte[] rawAttestationObject = this.lobHandler.getBlobAsBytes(rs, "attestation_object"); + if (rawAttestationObject != null) { + attestationObject = new Bytes(rawAttestationObject); + } + + Bytes attestationClientDataJson = null; + byte[] rawAttestationClientDataJson = this.lobHandler.getBlobAsBytes(rs, "attestation_client_data_json"); + if (rawAttestationClientDataJson != null) { + attestationClientDataJson = new Bytes(rawAttestationClientDataJson); + } + + Instant created = fromTimestamp(rs.getTimestamp("created")); + Instant lastUsed = fromTimestamp(rs.getTimestamp("last_used")); + String label = rs.getString("label"); + String[] transports = rs.getString("authenticator_transports").split(","); + + Set authenticatorTransports = new HashSet<>(); + for (String transport : transports) { + authenticatorTransports.add(AuthenticatorTransport.valueOf(transport)); + } + return ImmutableCredentialRecord.builder() + .credentialId(credentialId) + .userEntityUserId(userEntityUserId) + .publicKey(publicKey) + .signatureCount(signatureCount) + .uvInitialized(uvInitialized) + .backupEligible(backupEligible) + .credentialType(credentialType) + .backupState(backupState) + .attestationObject(attestationObject) + .attestationClientDataJSON(attestationClientDataJson) + .created(created) + .label(label) + .lastUsed(lastUsed) + .transports(authenticatorTransports) + .build(); + } + + private Instant fromTimestamp(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + return timestamp.toInstant(); + } + + } + +} diff --git a/web/src/main/resources/META-INF/spring/aot.factories b/web/src/main/resources/META-INF/spring/aot.factories index dcc4be6a067..4c3991233fc 100644 --- a/web/src/main/resources/META-INF/spring/aot.factories +++ b/web/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,3 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ -org.springframework.security.web.aot.hint.WebMvcSecurityRuntimeHints +org.springframework.security.web.aot.hint.WebMvcSecurityRuntimeHints,\ +org.springframework.security.web.aot.hint.UserCredentialRuntimeHints diff --git a/web/src/main/resources/org/springframework/security/user-credentials-schema.sql b/web/src/main/resources/org/springframework/security/user-credentials-schema.sql new file mode 100644 index 00000000000..1be48f2fb1e --- /dev/null +++ b/web/src/main/resources/org/springframework/security/user-credentials-schema.sql @@ -0,0 +1,18 @@ +create table user_credentials +( + credential_id varchar(1000) not null, + user_entity_user_id varchar(1000) not null, + public_key blob not null, + signature_count bigint, + uv_initialized boolean, + backup_eligible boolean not null, + authenticator_transports varchar(1000), + public_key_credential_type varchar(100), + backup_state boolean not null, + attestation_object blob, + attestation_client_data_json blob, + created timestamp, + last_used timestamp, + label varchar(1000) not null, + primary key (credential_id) +); diff --git a/web/src/test/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHintsTests.java b/web/src/test/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHintsTests.java new file mode 100644 index 00000000000..33799cc6f92 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/aot/hint/UserCredentialRuntimeHintsTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.aot.hint; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UserCredentialRuntimeHints} + * + * @author Max Batischev + */ +public class UserCredentialRuntimeHintsTests { + + private final RuntimeHints hints = new RuntimeHints(); + + @BeforeEach + void setup() { + SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories") + .load(RuntimeHintsRegistrar.class) + .forEach((registrar) -> registrar.registerHints(this.hints, ClassUtils.getDefaultClassLoader())); + } + + @ParameterizedTest + @MethodSource("getClientRecordsSqlFiles") + void credentialRecordsSqlFilesHasHints(String schemaFile) { + assertThat(RuntimeHintsPredicates.resource().forResource(schemaFile)).accepts(this.hints); + } + + private static Stream getClientRecordsSqlFiles() { + return Stream.of("org/springframework/security/user-credentials-schema.sql"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/webauthn/api/TestCredentialRecord.java b/web/src/test/java/org/springframework/security/web/webauthn/api/TestCredentialRecord.java index 917125ae670..1ed190c03d5 100644 --- a/web/src/test/java/org/springframework/security/web/webauthn/api/TestCredentialRecord.java +++ b/web/src/test/java/org/springframework/security/web/webauthn/api/TestCredentialRecord.java @@ -16,6 +16,9 @@ package org.springframework.security.web.webauthn.api; +import java.time.Instant; +import java.util.Set; + public final class TestCredentialRecord { public static ImmutableCredentialRecord.ImmutableCredentialRecordBuilder userCredential() { @@ -29,6 +32,24 @@ public static ImmutableCredentialRecord.ImmutableCredentialRecordBuilder userCre .backupState(true); } + public static ImmutableCredentialRecord.ImmutableCredentialRecordBuilder fullUserCredential() { + return ImmutableCredentialRecord.builder() + .label("label") + .credentialId(Bytes.fromBase64("NauGCN7bZ5jEBwThcde51g")) + .userEntityUserId(Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM")) + .publicKey(ImmutablePublicKeyCose.fromBase64( + "pQECAyYgASFYIC7DAiV_trHFPjieOxXbec7q2taBcgLnIi19zrUwVhCdIlggvN6riHORK_velHcTLFK_uJhyKK0oBkJqzNqR2E-2xf8=")) + .backupEligible(true) + .created(Instant.now()) + .transports(Set.of(AuthenticatorTransport.BLE, AuthenticatorTransport.HYBRID)) + .signatureCount(100) + .uvInitialized(false) + .credentialType(PublicKeyCredentialType.PUBLIC_KEY) + .attestationObject(new Bytes("test".getBytes())) + .attestationClientDataJSON(new Bytes(("test").getBytes())) + .backupState(true); + } + private TestCredentialRecord() { } diff --git a/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java b/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java new file mode 100644 index 00000000000..4829b537f0a --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepositoryTests.java @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.webauthn.management; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.security.web.webauthn.api.AuthenticatorTransport; +import org.springframework.security.web.webauthn.api.CredentialRecord; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialType; +import org.springframework.security.web.webauthn.api.TestCredentialRecord; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link JdbcUserCredentialRepository} + * + * @author Max Batischev + */ +public class JdbcUserCredentialRepositoryTests { + + private EmbeddedDatabase db; + + private JdbcUserCredentialRepository jdbcUserCredentialRepository; + + private static final String USER_CREDENTIALS_SQL_RESOURCE = "org/springframework/security/user-credentials-schema.sql"; + + @BeforeEach + void setUp() { + this.db = createDb(); + JdbcOperations jdbcOperations = new JdbcTemplate(this.db); + this.jdbcUserCredentialRepository = new JdbcUserCredentialRepository(jdbcOperations); + } + + @AfterEach + void tearDown() { + this.db.shutdown(); + } + + private static EmbeddedDatabase createDb() { + // @formatter:off + return new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(EmbeddedDatabaseType.HSQL) + .setScriptEncoding("UTF-8") + .addScript(USER_CREDENTIALS_SQL_RESOURCE) + .build(); + // @formatter:on + } + + @Test + void constructorWhenJdbcOperationsIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> new JdbcUserCredentialRepository(null)) + .withMessage("jdbcOperations cannot be null"); + // @formatter:on + } + + @Test + void saveWhenCredentialRecordIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.jdbcUserCredentialRepository.save(null)) + .withMessage("record cannot be null"); + // @formatter:on + } + + @Test + void findByCredentialIdWheCredentialIdIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.jdbcUserCredentialRepository.findByCredentialId(null)) + .withMessage("credentialId cannot be null"); + // @formatter:on + } + + @Test + void findByCredentialIdWheUserIdIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.jdbcUserCredentialRepository.findByUserId(null)) + .withMessage("userId cannot be null"); + // @formatter:on + } + + @Test + void saveCredentialRecordWhenSaveThenReturnsSaved() { + CredentialRecord userCredential = TestCredentialRecord.fullUserCredential().build(); + this.jdbcUserCredentialRepository.save(userCredential); + + CredentialRecord savedUserCredential = this.jdbcUserCredentialRepository + .findByCredentialId(userCredential.getCredentialId()); + + assertThat(savedUserCredential).isNotNull(); + assertThat(savedUserCredential.getCredentialId()).isEqualTo(userCredential.getCredentialId()); + assertThat(savedUserCredential.getUserEntityUserId()).isEqualTo(userCredential.getUserEntityUserId()); + assertThat(savedUserCredential.getLabel()).isEqualTo(userCredential.getLabel()); + assertThat(savedUserCredential.getPublicKey().getBytes()).isEqualTo(userCredential.getPublicKey().getBytes()); + assertThat(savedUserCredential.isBackupEligible()).isEqualTo(userCredential.isBackupEligible()); + assertThat(savedUserCredential.isBackupState()).isEqualTo(userCredential.isBackupState()); + assertThat(savedUserCredential.getCreated()).isNotNull(); + assertThat(savedUserCredential.getLastUsed()).isNotNull(); + assertThat(savedUserCredential.isUvInitialized()).isFalse(); + assertThat(savedUserCredential.getSignatureCount()).isEqualTo(100); + assertThat(savedUserCredential.getCredentialType()).isEqualTo(PublicKeyCredentialType.PUBLIC_KEY); + assertThat(savedUserCredential.getTransports().contains(AuthenticatorTransport.HYBRID)).isTrue(); + assertThat(savedUserCredential.getTransports().contains(AuthenticatorTransport.BLE)).isTrue(); + assertThat(new String(savedUserCredential.getAttestationObject().getBytes())).isEqualTo("test"); + assertThat(new String(savedUserCredential.getAttestationClientDataJSON().getBytes())).isEqualTo("test"); + } + + @Test + void findCredentialRecordByUserIdWhenRecordExistsThenReturnsSaved() { + CredentialRecord userCredential = TestCredentialRecord.fullUserCredential().build(); + this.jdbcUserCredentialRepository.save(userCredential); + + List credentialRecords = this.jdbcUserCredentialRepository + .findByUserId(userCredential.getUserEntityUserId()); + + assertThat(credentialRecords).isNotNull(); + assertThat(credentialRecords.size()).isEqualTo(1); + } + + @Test + void findCredentialRecordByUserIdWhenRecordDoesNotExistThenReturnsEmpty() { + CredentialRecord userCredential = TestCredentialRecord.fullUserCredential().build(); + + List credentialRecords = this.jdbcUserCredentialRepository + .findByUserId(userCredential.getUserEntityUserId()); + + assertThat(credentialRecords.size()).isEqualTo(0); + } + + @Test + void findCredentialRecordByCredentialIdWhenRecordDoesNotExistThenReturnsNull() { + CredentialRecord userCredential = TestCredentialRecord.fullUserCredential().build(); + + CredentialRecord credentialRecord = this.jdbcUserCredentialRepository + .findByCredentialId(userCredential.getCredentialId()); + + assertThat(credentialRecord).isNull(); + } + + @Test + void deleteCredentialRecordWhenRecordExistThenSuccess() { + CredentialRecord userCredential = TestCredentialRecord.fullUserCredential().build(); + this.jdbcUserCredentialRepository.save(userCredential); + + this.jdbcUserCredentialRepository.delete(userCredential.getCredentialId()); + + CredentialRecord credentialRecord = this.jdbcUserCredentialRepository + .findByCredentialId(userCredential.getCredentialId()); + assertThat(credentialRecord).isNull(); + } + +} From fd267dfb71bfc8e1ab5bcc8270c12fbaad46fddf Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Fri, 13 Dec 2024 18:16:45 +0300 Subject: [PATCH 42/42] Add Support JdbcPublicKeyCredentialUserEntityRepository Closes gh-16224 --- ...icKeyCredentialUserEntityRuntimeHints.java | 40 ++++ ...blicKeyCredentialUserEntityRepository.java | 193 ++++++++++++++++++ .../resources/META-INF/spring/aot.factories | 3 +- .../security/user-entities-schema.sql | 7 + ...CredentialUserEntityRuntimeHintsTests.java | 59 ++++++ ...eyCredentialUserEntityRepositoryTests.java | 182 +++++++++++++++++ 6 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 web/src/main/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHints.java create mode 100644 web/src/main/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepository.java create mode 100644 web/src/main/resources/org/springframework/security/user-entities-schema.sql create mode 100644 web/src/test/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHintsTests.java create mode 100644 web/src/test/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepositoryTests.java diff --git a/web/src/main/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHints.java b/web/src/main/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHints.java new file mode 100644 index 00000000000..c35cf5b81c6 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHints.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.aot.hint; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository; + +/** + * + * A JDBC implementation of an {@link PublicKeyCredentialUserEntityRepository} that uses a + * {@link JdbcOperations} for {@link PublicKeyCredentialUserEntity} persistence. + * + * @author Max Batischev + * @since 6.5 + */ +class PublicKeyCredentialUserEntityRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("org/springframework/security/user-entities-schema.sql"); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepository.java b/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepository.java new file mode 100644 index 00000000000..bfeaafb0e87 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepository.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.webauthn.management; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.security.web.webauthn.api.Bytes; +import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; +import org.springframework.util.Assert; + +/** + * A JDBC implementation of an {@link PublicKeyCredentialUserEntityRepository} that uses a + * {@link JdbcOperations} for {@link PublicKeyCredentialUserEntity} persistence. + * + * NOTE: This {@code PublicKeyCredentialUserEntityRepository} depends on the table + * definition described in + * "classpath:org/springframework/security/user-entities-schema.sql" and therefore MUST be + * defined in the database schema. + * + * @author Max Batischev + * @since 6.5 + * @see PublicKeyCredentialUserEntityRepository + * @see PublicKeyCredentialUserEntity + * @see JdbcOperations + * @see RowMapper + */ +public final class JdbcPublicKeyCredentialUserEntityRepository implements PublicKeyCredentialUserEntityRepository { + + private RowMapper userEntityRowMapper = new UserEntityRecordRowMapper(); + + private Function> userEntityParametersMapper = new UserEntityParametersMapper(); + + private final JdbcOperations jdbcOperations; + + private static final String TABLE_NAME = "user_entities"; + + // @formatter:off + private static final String COLUMN_NAMES = "id, " + + "name, " + + "display_name "; + // @formatter:on + + // @formatter:off + private static final String SAVE_USER_SQL = "INSERT INTO " + TABLE_NAME + + " (" + COLUMN_NAMES + ") VALUES (?, ?, ?)"; + // @formatter:on + + private static final String ID_FILTER = "id = ? "; + + private static final String USER_NAME_FILTER = "name = ? "; + + // @formatter:off + private static final String FIND_USER_BY_ID_SQL = "SELECT " + COLUMN_NAMES + + " FROM " + TABLE_NAME + + " WHERE " + ID_FILTER; + // @formatter:on + + // @formatter:off + private static final String FIND_USER_BY_NAME_SQL = "SELECT " + COLUMN_NAMES + + " FROM " + TABLE_NAME + + " WHERE " + USER_NAME_FILTER; + // @formatter:on + + private static final String DELETE_USER_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + ID_FILTER; + + // @formatter:off + private static final String UPDATE_USER_SQL = "UPDATE " + TABLE_NAME + + " SET name = ?, display_name = ? " + + " WHERE " + ID_FILTER; + // @formatter:on + + /** + * Constructs a {@code JdbcPublicKeyCredentialUserEntityRepository} using the provided + * parameters. + * @param jdbcOperations the JDBC operations + */ + public JdbcPublicKeyCredentialUserEntityRepository(JdbcOperations jdbcOperations) { + Assert.notNull(jdbcOperations, "jdbcOperations cannot be null"); + this.jdbcOperations = jdbcOperations; + } + + @Override + public PublicKeyCredentialUserEntity findById(Bytes id) { + Assert.notNull(id, "id cannot be null"); + List result = this.jdbcOperations.query(FIND_USER_BY_ID_SQL, + this.userEntityRowMapper, id.toBase64UrlString()); + return !result.isEmpty() ? result.get(0) : null; + } + + @Override + public PublicKeyCredentialUserEntity findByUsername(String username) { + Assert.hasText(username, "name cannot be null or empty"); + List result = this.jdbcOperations.query(FIND_USER_BY_NAME_SQL, + this.userEntityRowMapper, username); + return !result.isEmpty() ? result.get(0) : null; + } + + @Override + public void save(PublicKeyCredentialUserEntity userEntity) { + Assert.notNull(userEntity, "userEntity cannot be null"); + boolean existsUserEntity = null != this.findById(userEntity.getId()); + if (existsUserEntity) { + updateUserEntity(userEntity); + } + else { + try { + insertUserEntity(userEntity); + } + catch (DuplicateKeyException ex) { + updateUserEntity(userEntity); + } + } + } + + private void insertUserEntity(PublicKeyCredentialUserEntity userEntity) { + List parameters = this.userEntityParametersMapper.apply(userEntity); + PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray()); + this.jdbcOperations.update(SAVE_USER_SQL, pss); + } + + private void updateUserEntity(PublicKeyCredentialUserEntity userEntity) { + List parameters = this.userEntityParametersMapper.apply(userEntity); + SqlParameterValue userEntityId = parameters.remove(0); + parameters.add(userEntityId); + PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray()); + this.jdbcOperations.update(UPDATE_USER_SQL, pss); + } + + @Override + public void delete(Bytes id) { + Assert.notNull(id, "id cannot be null"); + SqlParameterValue[] parameters = new SqlParameterValue[] { + new SqlParameterValue(Types.VARCHAR, id.toBase64UrlString()), }; + PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters); + this.jdbcOperations.update(DELETE_USER_SQL, pss); + } + + private static class UserEntityParametersMapper + implements Function> { + + @Override + public List apply(PublicKeyCredentialUserEntity userEntity) { + List parameters = new ArrayList<>(); + + parameters.add(new SqlParameterValue(Types.VARCHAR, userEntity.getId().toBase64UrlString())); + parameters.add(new SqlParameterValue(Types.VARCHAR, userEntity.getName())); + parameters.add(new SqlParameterValue(Types.VARCHAR, userEntity.getDisplayName())); + + return parameters; + } + + } + + private static class UserEntityRecordRowMapper implements RowMapper { + + @Override + public PublicKeyCredentialUserEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + Bytes id = Bytes.fromBase64(new String(rs.getString("id").getBytes())); + String name = rs.getString("name"); + String displayName = rs.getString("display_name"); + + return ImmutablePublicKeyCredentialUserEntity.builder().id(id).name(name).displayName(displayName).build(); + } + + } + +} diff --git a/web/src/main/resources/META-INF/spring/aot.factories b/web/src/main/resources/META-INF/spring/aot.factories index 4c3991233fc..2a3c8ad7681 100644 --- a/web/src/main/resources/META-INF/spring/aot.factories +++ b/web/src/main/resources/META-INF/spring/aot.factories @@ -1,3 +1,4 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.security.web.aot.hint.WebMvcSecurityRuntimeHints,\ -org.springframework.security.web.aot.hint.UserCredentialRuntimeHints +org.springframework.security.web.aot.hint.UserCredentialRuntimeHints,\ +org.springframework.security.web.aot.hint.PublicKeyCredentialUserEntityRuntimeHints diff --git a/web/src/main/resources/org/springframework/security/user-entities-schema.sql b/web/src/main/resources/org/springframework/security/user-entities-schema.sql new file mode 100644 index 00000000000..ec66c66519b --- /dev/null +++ b/web/src/main/resources/org/springframework/security/user-entities-schema.sql @@ -0,0 +1,7 @@ +create table user_entities +( + id varchar(1000) not null, + name varchar(100) not null, + display_name varchar(200), + primary key (id) +); diff --git a/web/src/test/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHintsTests.java b/web/src/test/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHintsTests.java new file mode 100644 index 00000000000..4909a643030 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/aot/hint/PublicKeyCredentialUserEntityRuntimeHintsTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.aot.hint; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PublicKeyCredentialUserEntityRuntimeHints} + * + * @author Max Batischev + */ +public class PublicKeyCredentialUserEntityRuntimeHintsTests { + + private final RuntimeHints hints = new RuntimeHints(); + + @BeforeEach + void setup() { + SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories") + .load(RuntimeHintsRegistrar.class) + .forEach((registrar) -> registrar.registerHints(this.hints, ClassUtils.getDefaultClassLoader())); + } + + @ParameterizedTest + @MethodSource("getUserEntitiesSqlFiles") + void userEntitiesSqlFilesHasHints(String schemaFile) { + assertThat(RuntimeHintsPredicates.resource().forResource(schemaFile)).accepts(this.hints); + } + + private static Stream getUserEntitiesSqlFiles() { + return Stream.of("org/springframework/security/user-entities-schema.sql"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepositoryTests.java b/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepositoryTests.java new file mode 100644 index 00000000000..503108ac4ea --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepositoryTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2024 the original author or 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 org.springframework.security.web.webauthn.management; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.security.web.webauthn.api.Bytes; +import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link JdbcPublicKeyCredentialUserEntityRepository} + * + * @author Max Batischev + */ +public class JdbcPublicKeyCredentialUserEntityRepositoryTests { + + private EmbeddedDatabase db; + + private JdbcPublicKeyCredentialUserEntityRepository repository; + + private static final String USER_ENTITIES_SQL_RESOURCE = "org/springframework/security/user-entities-schema.sql"; + + @BeforeEach + void setUp() { + this.db = createDb(); + JdbcOperations jdbcOperations = new JdbcTemplate(this.db); + this.repository = new JdbcPublicKeyCredentialUserEntityRepository(jdbcOperations); + } + + @AfterEach + void tearDown() { + this.db.shutdown(); + } + + private static EmbeddedDatabase createDb() { + // @formatter:off + return new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(EmbeddedDatabaseType.HSQL) + .setScriptEncoding("UTF-8") + .addScript(USER_ENTITIES_SQL_RESOURCE) + .build(); + // @formatter:on + } + + @Test + void constructorWhenJdbcOperationsIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> new JdbcPublicKeyCredentialUserEntityRepository(null)) + .withMessage("jdbcOperations cannot be null"); + // @formatter:on + } + + @Test + void saveWhenUserEntityIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.save(null)) + .withMessage("userEntity cannot be null"); + // @formatter:on + } + + @Test + void findByUserEntityIdWheIdIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.findById(null)) + .withMessage("id cannot be null"); + // @formatter:on + } + + @Test + void findByUserNameWheUserNameIsNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.repository.findByUsername(null)) + .withMessage("name cannot be null or empty"); + // @formatter:on + } + + @Test + void saveUserEntityWhenSaveThenReturnsSaved() { + PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build(); + + this.repository.save(userEntity); + + PublicKeyCredentialUserEntity savedUserEntity = this.repository.findById(userEntity.getId()); + assertThat(savedUserEntity).isNotNull(); + assertThat(savedUserEntity.getId()).isEqualTo(userEntity.getId()); + assertThat(savedUserEntity.getDisplayName()).isEqualTo(userEntity.getDisplayName()); + assertThat(savedUserEntity.getName()).isEqualTo(userEntity.getName()); + } + + @Test + void saveUserEntityWhenUserEntityExistsThenUpdates() { + PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build(); + this.repository.save(userEntity); + + this.repository.save(testUserEntity(userEntity.getId())); + + PublicKeyCredentialUserEntity savedUserEntity = this.repository.findById(userEntity.getId()); + assertThat(savedUserEntity).isNotNull(); + assertThat(savedUserEntity.getId()).isEqualTo(userEntity.getId()); + assertThat(savedUserEntity.getDisplayName()).isEqualTo("user2"); + assertThat(savedUserEntity.getName()).isEqualTo("user2"); + } + + @Test + void findUserEntityByUserNameWhenUserEntityExistsThenReturnsSaved() { + PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build(); + this.repository.save(userEntity); + + PublicKeyCredentialUserEntity savedUserEntity = this.repository.findByUsername(userEntity.getName()); + + assertThat(savedUserEntity).isNotNull(); + } + + @Test + void deleteUserEntityWhenRecordExistThenSuccess() { + PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build(); + this.repository.save(userEntity); + + this.repository.delete(userEntity.getId()); + + PublicKeyCredentialUserEntity savedUserEntity = this.repository.findById(userEntity.getId()); + assertThat(savedUserEntity).isNull(); + } + + @Test + void findUserEntityByIdWhenUserEntityDoesNotExistThenReturnsNull() { + PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build(); + + PublicKeyCredentialUserEntity savedUserEntity = this.repository.findById(userEntity.getId()); + assertThat(savedUserEntity).isNull(); + } + + @Test + void findUserEntityByUserNameWhenUserEntityDoesNotExistThenReturnsEmpty() { + PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build(); + + PublicKeyCredentialUserEntity savedUserEntity = this.repository.findByUsername(userEntity.getName()); + assertThat(savedUserEntity).isNull(); + } + + private PublicKeyCredentialUserEntity testUserEntity(Bytes id) { + // @formatter:off + return ImmutablePublicKeyCredentialUserEntity.builder() + .name("user2") + .id(id) + .displayName("user2") + .build(); + // @formatter:on + } + +}