1
0
mirror of synced 2026-07-09 12:50:19 +00:00

Compare commits

...

139 Commits

Author SHA1 Message Date
github-actions[bot] ff3c560c53 Release 6.0.3 2023-04-17 15:21:32 +00:00
Marcus Da Coregio a484044591 Merge branch '5.8.x' into 6.0.x 2023-04-17 07:29:42 -03:00
Marcus Da Coregio 6cf8c53aaa Merge branch '5.7.x' into 5.8.x 2023-04-17 07:16:47 -03:00
Marcus Da Coregio 2d52fb8e4b Clear Repository on Logout 2023-04-17 06:47:57 -03:00
Marcus Da Coregio cc56fc647c Update spring-ldap-core to 3.0.2
Closes gh-13043
2023-04-14 14:27:33 -03:00
Marcus Da Coregio 67d8df790e Update org.springframework.data to 2022.0.5
Closes gh-13042
2023-04-14 14:27:26 -03:00
Marcus Da Coregio fe6cfe819a Update org.springframework to 6.0.8
Closes gh-13041
2023-04-14 14:24:44 -03:00
Marcus Da Coregio 4162bdadd9 Update slf4j-api to 2.0.7
Closes gh-13040
2023-04-14 14:24:40 -03:00
Marcus Da Coregio 9c181890ea Update org.eclipse.jetty to 11.0.15
Closes gh-13039
2023-04-14 14:24:37 -03:00
Marcus Da Coregio af41a4053e Update assertj-core to 3.24.2
Closes gh-13038
2023-04-14 14:24:33 -03:00
Marcus Da Coregio 440b575dd7 Update maven-resolver-provider to 3.8.8
Closes gh-13037
2023-04-14 14:24:30 -03:00
Marcus Da Coregio 8793639fe4 Update io.spring.javaformat to 0.0.38
Closes gh-13036
2023-04-14 14:24:26 -03:00
Marcus Da Coregio 95f2b822a7 Update io.projectreactor to 2022.0.6
Closes gh-13034
2023-04-14 14:24:19 -03:00
Marcus Da Coregio 8eaf90a5e9 Update mockk to 1.13.5
Closes gh-13033
2023-04-14 14:24:16 -03:00
Marcus Da Coregio 6d6042485b Update micrometer-observation to 1.10.6
Closes gh-13032
2023-04-14 14:24:12 -03:00
Marcus Da Coregio 09632dbe51 Update unboundid-ldapsdk to 6.0.8
Closes gh-13031
2023-04-14 14:24:09 -03:00
Marcus Da Coregio 7e49d70e77 Update logback-classic to 1.4.6
Closes gh-13030
2023-04-14 14:24:05 -03:00
Marcus Da Coregio cfcdb15d6b Merge branch '5.8.x' into 6.0.x 2023-04-14 14:19:27 -03:00
Marcus Da Coregio e9ee2cefb9 Update org.springframework.data to 2021.2.11
Closes gh-13029
2023-04-14 14:18:08 -03:00
Marcus Da Coregio 15dc917c23 Update org.springframework.data to 2021.2.10
Closes gh-13028
2023-04-14 14:02:14 -03:00
Marcus Da Coregio 46368f0a75 Update org.springframework to 5.3.27
Closes gh-13027
2023-04-14 14:02:11 -03:00
Marcus Da Coregio 0448e2804c Update org.eclipse.jetty to 9.4.51.v20230217
Closes gh-13026
2023-04-14 14:02:06 -03:00
Marcus Da Coregio 7571ab9b7d Update io.spring.javaformat to 0.0.38
Closes gh-13025
2023-04-14 14:02:03 -03:00
Marcus Da Coregio 8566bbcdac Update blockhound to 1.0.8.RELEASE
Closes gh-13024
2023-04-14 14:02:00 -03:00
Marcus Da Coregio 609d5a71c6 Update io.projectreactor to 2020.0.31
Closes gh-13022
2023-04-14 14:01:53 -03:00
Marcus Da Coregio c73876e596 Update logback-classic to 1.2.12
Closes gh-13021
2023-04-14 14:01:49 -03:00
Marcus Da Coregio 8a07d17c69 Merge branch '5.7.x' into 5.8.x
Closes gh-13020
2023-04-14 13:57:53 -03:00
Marcus Da Coregio 37d8846652 Update org.springframework.data to 2021.2.11
Closes gh-13019
2023-04-14 13:57:28 -03:00
Marcus Da Coregio 1a4a2a9055 Merge branch '5.8.x' into 6.0.x 2023-04-14 13:32:10 -03:00
Marcus Da Coregio 54117d7d27 Fix test suffix to align with checkstyle 2023-04-14 13:29:15 -03:00
Marcus Da Coregio 74b8c6715b Update org.springframework to 5.3.27
Closes gh-13018
2023-04-14 10:41:45 -03:00
Marcus Da Coregio 8c9275067e Update org.eclipse.jetty to 9.4.51.v20230217
Closes gh-13017
2023-04-14 10:41:41 -03:00
Marcus Da Coregio 4b8bfee7c5 Update blockhound to 1.0.8.RELEASE
Closes gh-13016
2023-04-14 10:41:37 -03:00
Marcus Da Coregio b727f24c95 Update io.projectreactor to 2020.0.31
Closes gh-13014
2023-04-14 10:41:31 -03:00
Marcus Da Coregio d55c8aac4f Update logback-classic to 1.2.12
Closes gh-13013
2023-04-14 10:41:27 -03:00
Josh Cummings b423db5f93 Merge branch '5.8.x' into 6.0.x
Closes gh-13005
2023-04-12 12:46:11 -06:00
Josh Cummings 28be37238d Merge branch '5.7.x' into 5.8.x
Closes gh-13004
2023-04-12 12:45:28 -06:00
Nick Meverden c4e9fb885d Delay JWSVerificationKeySelector Construction
Updating the NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder processor to execute the ConfigurableJWTProcessor customizations before holding a reference to the JWSKeySelector.

Closes gh-12960
2023-04-12 12:45:00 -06:00
Josh Cummings 4813ec1e09 Merge branch '5.8.x' into 6.0.x
Closes gh-13000
2023-04-11 17:08:54 -06:00
Josh Cummings dad1fba1bf Merge branch '5.7.x' into 5.8.x
Closes gh-12999
2023-04-11 17:02:16 -06:00
Christian Marck 442faccb5f Avoid NPE in FilterInvocation
Handle unknown headers in dummy request wrapper.

Closes gh-12998
2023-04-11 17:01:59 -06:00
Marcus Da Coregio 34015944f2 Merge branch '5.8.x' into 6.0.x
Closes gh-12992
2023-04-11 09:57:52 -03:00
Marcus Da Coregio 68fd755abc Merge branch '5.7.x' into 5.8.x
Closes gh-12991
2023-04-11 09:57:15 -03:00
Marcus Da Coregio e25117856e Add static imports to code snippets
Closes gh-6597
2023-04-11 09:56:19 -03:00
Rob Winch 9c3f91a2d3 Merge branch '5.8.x' into 6.0.x
Closes gh-12983
2023-04-10 11:25:32 -05:00
Rob Winch 16dcfd1cfe Merge branch '5.7.x' into 5.8.x
Closes gh-12982
2023-04-10 11:25:01 -05:00
Yuanhang Guo c69df9fba0 Fix javadoc typo in ReactiveAuthorizationManager
Closes gh-12978
2023-04-10 11:24:49 -05:00
1993heqiang 54de5c9537 Fix documentation code block bug.
Closes gh-12980
2023-04-10 11:11:10 -05:00
Josh Cummings 64a1ad5cd6 Merge branch '5.8.x' into 6.0.x 2023-04-04 13:32:34 -06:00
Josh Cummings 5ffebaf12b Merge branch '5.7.x' into 5.8.x 2023-04-04 13:32:04 -06:00
Josh Cummings 39cee36065 Use SingletonSupplier
Issue gh-9991
2023-04-04 13:25:31 -06:00
Petr Svoboda 44c4a4ae86 Add new DaoAuthenticationProvider constructor
Add a new constructor to the DaoAuthenticationProvider, which allows
providing a custom PasswordEncoder to prevent instantiation of the
default delegating PasswordEncoder in the default constructor.

This provides a way to instantiate the DaoAuthenticationProvider on JDKs
where the default delegating PasswordEncoder cannot be instantiated due
to limited JCE providers for compliance reasons (e.g., FIPS).

Closes gh-12874
2023-04-04 10:21:22 -03:00
Marcus Da Coregio 05675e83dc Merge branch '5.8.x' into 6.0.x 2023-04-04 09:33:50 -03:00
Marcus Da Coregio d5603a944d Avoid exception if PBKDF2WithHmacSHA256 is not available
Issue gh-12873
2023-04-04 09:33:12 -03:00
Marcus Da Coregio b4b6469d83 Merge branch '5.8.x' into 6.0.x 2023-03-30 09:38:46 -03:00
Brummolix a513fc0f38 Fix SecurityWebApplicationInitializer.getSecurityDispatcherTypes example in doc #12939 2023-03-30 09:38:17 -03:00
Marcus Da Coregio b4b4cd0ffa Merge branch '5.8.x' into 6.0.x
Closes gh-12941
2023-03-28 15:23:21 -03:00
Marcus Da Coregio eb58655fa9 Improve Docs by mentioning that Empty SecurityContext should be saved
Closes gh-12906
2023-03-28 15:21:30 -03:00
Josh Cummings 20358e769d Merge branch '5.8.x' into 6.0.x
Closes gh-12936
2023-03-27 16:39:48 -06:00
Josh Cummings c15589ede1 Merge branch '5.7.x' into 5.8.x
Closes gh-12935
2023-03-27 16:35:46 -06:00
Tatiana Slednikova a106188add Add nameIdFormat processing to OpenSamlAuthenticationRequestResolver
Closes gh-12825
2023-03-27 16:33:11 -06:00
Josh Cummings 6db2b0dcd0 Align Filter Chain Observability Lineage
Closes gh-12849
2023-03-27 16:30:32 -06:00
Josh Cummings 834e361898 Merge branch '5.8.x' into 6.0.x
Closes gh-12932
2023-03-27 14:41:07 -06:00
Josh Cummings 6bda1d2bf3 Document WebExpressionAuthorizationManager
Closes gh-12928
2023-03-27 14:38:09 -06:00
Josh Cummings 97b53f0472 Read Extracted Variables
Closes gh-12540
2023-03-23 09:57:31 -06:00
Josh Cummings aea13b656e Add Testing
Issue gh-12540
2023-03-23 09:52:38 -06:00
Marcus Da Coregio 9ec9e77c6b Merge branch '5.8.x' into 6.0.x 2023-03-22 10:34:02 -03:00
slauth a708007536 fix ID of WebSocket Authorization section
Throughout this document there are 3 references to `<<websocket-authorization>>` but the section ID was actually named `[[websocket-configuration]]`
2023-03-22 10:33:10 -03:00
Marcus Da Coregio 67645b32f4 Merge branch '5.8.x' into 6.0.x 2023-03-22 10:12:11 -03:00
Jesper Rønn-Jensen ed42dc4a09 chore: typo, removed extra "s" in word implementationss 2023-03-22 10:11:16 -03:00
Gabriel Maciel 2fa1bbc9d1 Fix typo architecture.adoc 2023-03-22 10:10:28 -03:00
Marcus Da Coregio fd65dc6756 Merge branch '5.7.x' into 5.8.x 2023-03-22 10:08:17 -03:00
Martin Tarjányi 5eefe9dcff Fix typo in SessionManagementConfigurer javadoc 2023-03-22 10:07:44 -03:00
Marcus Da Coregio 8a2b96795e Merge branch '5.8.x' into 6.0.x 2023-03-22 09:21:50 -03:00
Bishakh Ghosh c75ee25a6d Fix documentation code block bug
Fixes #12850
2023-03-22 09:21:09 -03:00
Marcus Da Coregio 177514b6c5 Merge branch '5.8.x' into 6.0.x
Closes gh-12919
2023-03-22 08:54:57 -03:00
Marcus Da Coregio 8d664bc4c2 DelegatingSecurityContextRepository should call loadContext
Closes gh-12314
2023-03-22 08:53:19 -03:00
twosom cbb4e40166 fix typo in RequestCacheResultMatcher 2023-03-20 17:02:00 -06:00
Josh Cummings f03c904c02 Merge branch '5.8.x' into 6.0.x
Closes gh-12912
2023-03-20 16:59:52 -06:00
Josh Cummings 889fa55c9c Merge branch '5.7.x' into 5.8.x
Closes gh-12911
2023-03-20 16:59:23 -06:00
Siva Kumar Edupuganti 98321b769a Fixed Broken Support Link
Closes gh-12759
2023-03-20 16:59:04 -06:00
Josh Cummings 3fbb64db96 Fix javax package 2023-03-20 16:28:52 -06:00
Josh Cummings 229325a0bb Merge branch '5.8.x' into 6.0.x 2023-03-20 16:22:23 -06:00
Josh Cummings a74008cc79 Merge branch '5.7.x' into 5.8.x 2023-03-20 16:20:46 -06:00
twosom 3d7e22a4e9 Add test to SimpleUrlAuthenticationSuccessHandlerTests 2023-03-20 16:20:30 -06:00
zks cfeb1ce303 Fix docs typo 2023-03-20 16:18:10 -06:00
Josh Cummings 6935045172 Merge branch '5.8.x' into 6.0.x
Closes gh-12909
2023-03-20 16:10:35 -06:00
twosom abd51f7b63 Polished DefaultLoginPageGeneratingFilterTests Validation
Closes gh-12694
2023-03-20 15:31:59 -06:00
Josh Cummings b22dd9a3e9 Merge branch '5.8.x' into 6.0.x 2023-03-20 15:24:19 -06:00
el-hopaness-romtic 4154ed543a Fix .access(...) parameter 2023-03-20 15:23:40 -06:00
Logan Kulinski 8ad91287d9 Fix Broken Link
Closes gh-12824
2023-03-20 15:13:10 -06:00
rai-sandeep ffe029d5bd Fix broken links in form login section
Closes gh-12822
2023-03-07 11:52:19 -07:00
Marcus Da Coregio cdc0fa0e5b Merge branch '5.8.x' into 6.0.x
Closes gh-12836
2023-03-07 13:28:31 -03:00
Marcus Da Coregio 2e92dad761 Merge branch '5.7.x' into 5.8.x
Closes gh-12835
2023-03-07 13:27:57 -03:00
Marcus Da Coregio 84cca81edf Use HttpSessionSecurityContextRepository by default in SwitchUserFilter
Closes gh-12834
2023-03-07 13:27:18 -03:00
Josh Cummings c06e604278 Address Observability Thread Safety
Closes gh-12829
2023-03-06 12:46:23 -07:00
Josh Cummings acf48721cd Merge branch '5.8.x' into 6.0.x 2023-03-03 15:02:34 -07:00
Josh Cummings ebabcaa51a Merge branch '5.7.x' into 5.8.x 2023-03-03 15:02:07 -07:00
bist 094bf1b527 Validate hasRole Input
There are no check for role prefix in AuthorizeHttpRequestsConfigurer#XXXrole
methods. This PR adds check for the same. Now the configuration
will fail if role/s start with prefix for hasRole and hasAnyRole methods.

Closes #12581
2023-03-03 15:00:34 -07:00
Josh Cummings 14ee873b74 Merge branch '5.8.x' into 6.0.x 2023-03-01 11:25:21 -07:00
Josh Cummings e7b14b3ca2 Merge branch '5.7.x' into 5.8.x 2023-03-01 11:24:17 -07:00
Josh Cummings 66665344c5 Formatting
Issue gh-12664
2023-03-01 11:23:45 -07:00
stnor 6c7703789a Ignore SPSSODescriptor elements
Closes gh-12664
2023-03-01 11:23:45 -07:00
Steve Riesenberg e144a84163 Merge branch '5.8.x' into 6.0.x 2023-03-01 11:50:27 -06:00
Steve Riesenberg a6918f9e0c Merge branch '5.7.x' into 5.8.x 2023-03-01 11:50:17 -06:00
Steve Riesenberg fabf7f649c Fix path of copied schema files
Issue gh-12804
2023-03-01 11:49:36 -06:00
Qie 8b2ce79341 Correct deprecated description in docs
Remove deprecated SecurityContextPersistenceFilter from docs.

Closes gh-12690
2023-02-28 17:01:47 -06:00
Steve Riesenberg 1eff924598 Merge branch '5.8.x' into 6.0.x 2023-02-28 16:53:33 -06:00
Steve Riesenberg b2240f376e Merge branch '5.7.x' into 5.8.x 2023-02-28 16:53:14 -06:00
Steve Riesenberg 7b88ab289d Add nohttp exclusion
Issue gh-12804
2023-02-28 16:52:19 -06:00
Steve Riesenberg 82ffc954d3 Merge branch '5.8.x' into 6.0.x
Closes gh-12806
2023-02-28 16:20:24 -06:00
Steve Riesenberg ad57c0567f Merge branch '5.7.x' into 5.8.x
Closes gh-12805
2023-02-28 16:19:54 -06:00
Steve Riesenberg 26566af431 Copy spring-security-oauth schemas on deploy
Closes gh-12804
2023-02-28 16:16:08 -06:00
Steve Riesenberg 2a4fe7de58 Merge branch '5.8.x' into 6.0.x
Closes gh-12802
2023-02-28 14:14:11 -06:00
Steve Riesenberg 86578ea9de Merge branch '5.7.x' into 5.8.x
Closes gh-12801
2023-02-28 14:13:07 -06:00
Steve Riesenberg 5257e36ffc Update SaganApi to work with Contentful backend
Also fixed an issue with incorrect referenceDocUrl for Antora
with SNAPSHOT versions.

Closes gh-12575
2023-02-28 14:09:51 -06:00
Josh Cummings e28ea6dbad Preserve OpenSamlAssertingPartyDetails Instance
Closes gh-12667
2023-02-28 13:08:13 -07:00
Josh Cummings ddad623abf Merge branch '5.8.x' into 6.0.x 2023-02-28 12:49:04 -07:00
Josh Cummings 383e0c2cf0 Merge branch '5.7.x' into 5.8.x 2023-02-28 12:47:06 -07:00
Josh Cummings 0421e25cba Document Common SAML URI Endpoints
Issue gh-12764
2023-02-28 12:45:48 -07:00
Josh Cummings 1c885cf3a3 Document Federation Usecase
Closes gh-12764
2023-02-28 12:35:04 -07:00
Josh Cummings 109f6e7028 Add Note about RoleHierarchy
Closes gh-12766
2023-02-24 13:43:43 -07:00
Josh Cummings bbd31f0e33 Defer ObservationRegistry Lookup
Closes gh-12780
2023-02-24 11:03:32 -07:00
Marcus Da Coregio 7d22e02593 Merge branch '5.8.x' into 6.0.x
Closes gh-12777
2023-02-23 15:17:25 -03:00
Marcus Da Coregio 97ba596ca3 Merge branch '5.7.x' into 5.8.x
Closes gh-12776
2023-02-23 15:17:04 -03:00
Marcus Da Coregio 1c3ce1e401 Fix entity-id ignored in RelyingPartyRegistration XML config
Closes gh-11898
2023-02-23 15:16:40 -03:00
Josh Cummings d3a65dbbbe Merge branch '5.8.x' into 6.0.x
Closes gh-12767
2023-02-22 10:22:11 -07:00
Dayan Kodippily 79887fa213 Closes gh-12472 2023-02-22 10:21:46 -07:00
Josh Cummings 978c736169 Merge branch '5.8.x' into 6.0.x 2023-02-21 16:58:52 -07:00
Josh Cummings 35cf52d3bd Add DefaultMethodSecurityExpressionHandler
Closes gh-12356
2023-02-21 16:58:08 -07:00
Josh Cummings 7fdc6070fd Merge branch '5.8.x' into 6.0.x 2023-02-21 15:07:58 -07:00
Josh Cummings 6bf11181ef Adjust AfterInvocationManager Migration Docs
The original documentation only addresses the post-authorize case.
Some implementations want also to modify the return type.

Issue gh-12620
2023-02-21 15:07:17 -07:00
Steve Riesenberg 70219e4a80 Merge branch '5.8.x' into 6.0.x 2023-02-21 11:45:36 -06:00
Steve Riesenberg 5be58b2622 Merge branch '5.7.x' into 5.8.x 2023-02-21 11:45:21 -06:00
github-actions[bot] b851a8bf6d Next development version 2023-02-21 15:20:58 +00:00
Steve Riesenberg 7dcdb4b1a8 Next development version 2023-02-21 08:55:46 -06:00
github-actions[bot] faadadf71a Release 5.7.7 2023-02-21 14:41:42 +00:00
github-actions[bot] 207e41f6f6 Next development version 2023-02-21 14:36:49 +00:00
github-actions[bot] ce46f06d81 Release 5.8.2 2023-02-20 15:22:34 +00:00
83 changed files with 2250 additions and 349 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ Discover more commands with `./gradlew tasks`.
== Getting Support
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
https://spring.io/services[Commercial support] is available too.
https://spring.io/support[Commercial support] is available too.
== Contributing
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/main/CONTRIBUTING.adoc[contributor guidelines] for details.
+1 -1
View File
@@ -87,7 +87,7 @@ dependencies {
implementation localGroovy()
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
implementation 'io.projectreactor:reactor-core:3.5.3'
implementation 'io.projectreactor:reactor-core:3.5.5'
implementation 'org.gretty:gretty:3.0.9'
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
@@ -62,6 +62,33 @@ public class SchemaDeployPlugin implements Plugin<Project> {
execute "rm -f $tempPath*.zip"
execute "rm -rf $extractPath*"
execute "mv $tempPath/* $extractPath"
/*
* Copy spring-security-oauth schemas so that legacy projects using the "http" scheme can
* resolve the following schema locations:
*
* - http://www.springframework.org/schema/security/spring-security-oauth-1.0.xsd
* - http://www.springframework.org/schema/security/spring-security-oauth2-1.0.xsd
* - http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd
*
* Note: The directory:
* https://www.springframework.org/schema/security/
*
* is a symbolic link pointing to:
* https://docs.spring.io/autorepo/schema/spring-security/current/security/
*
* which in turn points to the latest:
* https://docs.spring.io/autorepo/schema/spring-security/$version/security/
*
* with the help of the autoln command which is run regularly via a cron job.
*
* See https://github.com/spring-io/autoln for more info.
*/
if (name == "spring-security") {
def springSecurityOauthPath = "/var/www/domains/spring.io/docs/htdocs/autorepo/schema/spring-security-oauth/current/security"
execute "cp $springSecurityOauthPath/* ${extractPath}security"
}
execute "chmod -R g+w $extractPath"
}
}
@@ -0,0 +1,82 @@
/*
* Copyright 2020-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.
* 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.gradle.github.user;
import java.io.IOException;
import com.google.gson.Gson;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* @author Steve Riesenberg
*/
public class GitHubUserApi {
private String baseUrl = "https://api.github.com";
private final OkHttpClient httpClient;
private Gson gson = new Gson();
public GitHubUserApi(String gitHubAccessToken) {
this.httpClient = new OkHttpClient.Builder()
.addInterceptor(new AuthorizationInterceptor(gitHubAccessToken))
.build();
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
/**
* Retrieve a GitHub user by the personal access token.
*
* @return The GitHub user
*/
public User getUser() {
String url = this.baseUrl + "/user";
Request request = new Request.Builder().url(url).get().build();
try (Response response = this.httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new RuntimeException(
String.format("Unable to retrieve GitHub user." +
" Please check the personal access token and try again." +
" Got response %s", response));
}
return this.gson.fromJson(response.body().charStream(), User.class);
} catch (IOException ex) {
throw new RuntimeException("Unable to retrieve GitHub user.", ex);
}
}
private static class AuthorizationInterceptor implements Interceptor {
private final String token;
public AuthorizationInterceptor(String token) {
this.token = token;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + this.token)
.build();
return chain.proceed(request);
}
}
}
@@ -0,0 +1,53 @@
package org.springframework.gradle.github.user;
/**
* @author Steve Riesenberg
*/
public class User {
private Long id;
private String login;
private String name;
private String url;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getLogin() {
return this.login;
}
public void setLogin(String login) {
this.login = login;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", login='" + login + '\'' +
", name='" + name + '\'' +
", url='" + url + '\'' +
'}';
}
}
@@ -26,14 +26,14 @@ import java.util.Base64;
* Implements necessary calls to the Sagan API See https://spring.io/restdocs/index.html
*/
public class SaganApi {
private String baseUrl = "https://spring.io/api";
private String baseUrl = "https://api.spring.io";
private OkHttpClient client;
private Gson gson = new Gson();
public SaganApi(String gitHubToken) {
public SaganApi(String username, String gitHubToken) {
this.client = new OkHttpClient.Builder()
.addInterceptor(new BasicInterceptor("not-used", gitHubToken))
.addInterceptor(new BasicInterceptor(username, gitHubToken))
.build();
}
@@ -20,6 +20,9 @@ import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
import org.springframework.gradle.github.user.GitHubUserApi;
import org.springframework.gradle.github.user.User;
public class SaganCreateReleaseTask extends DefaultTask {
@Input
@@ -35,11 +38,22 @@ public class SaganCreateReleaseTask extends DefaultTask {
@TaskAction
public void saganCreateRelease() {
SaganApi sagan = new SaganApi(this.gitHubAccessToken);
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
User user = github.getUser();
// Antora reference docs URLs for snapshots do not contain -SNAPSHOT
String referenceDocUrl = this.referenceDocUrl;
if (this.version.endsWith("-SNAPSHOT")) {
referenceDocUrl = this.referenceDocUrl
.replace("{version}", this.version)
.replace("-SNAPSHOT", "");
}
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
Release release = new Release();
release.setVersion(this.version);
release.setApiDocUrl(this.apiDocUrl);
release.setReferenceDocUrl(this.referenceDocUrl);
release.setReferenceDocUrl(referenceDocUrl);
sagan.createReleaseForProject(release, this.projectName);
}
@@ -20,6 +20,9 @@ import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
import org.springframework.gradle.github.user.GitHubUserApi;
import org.springframework.gradle.github.user.User;
public class SaganDeleteReleaseTask extends DefaultTask {
@Input
@@ -31,7 +34,10 @@ public class SaganDeleteReleaseTask extends DefaultTask {
@TaskAction
public void saganCreateRelease() {
SaganApi sagan = new SaganApi(this.gitHubAccessToken);
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
User user = github.getUser();
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
sagan.deleteReleaseForProject(this.version, this.projectName);
}
@@ -42,7 +42,7 @@ public class SaganApiTests {
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
this.sagan = new SaganApi("mock-oauth-token");
this.sagan = new SaganApi("user", "mock-oauth-token");
this.baseUrl = this.server.url("/api").toString();
this.sagan.setBaseUrl(this.baseUrl);
}
@@ -63,7 +63,7 @@ public class SaganApiTests {
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases");
assertThat(request.getMethod()).isEqualToIgnoringCase("post");
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg==");
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
assertThat(request.getBody().readString(Charset.defaultCharset())).isEqualToIgnoringWhitespace("{\n" +
" \"version\":\"5.6.0-SNAPSHOT\",\n" +
" \"current\":false,\n" +
@@ -79,7 +79,7 @@ public class SaganApiTests {
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases/5.6.0-SNAPSHOT");
assertThat(request.getMethod()).isEqualToIgnoringCase("delete");
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg==");
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
assertThat(request.getBody().readString(Charset.defaultCharset())).isEmpty();
}
}
@@ -0,0 +1,106 @@
/*
* Copyright 2020-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.
* 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.gradle.github.user;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Steve Riesenberg
*/
public class GitHubUserApiTests {
private GitHubUserApi gitHubUserApi;
private MockWebServer server;
private String baseUrl;
@BeforeEach
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
this.baseUrl = this.server.url("/api").toString();
this.gitHubUserApi = new GitHubUserApi("mock-oauth-token");
this.gitHubUserApi.setBaseUrl(this.baseUrl);
}
@AfterEach
public void cleanup() throws Exception {
this.server.shutdown();
}
@Test
public void getUserWhenValidParametersThenSuccess() {
// @formatter:off
String responseJson = "{\n" +
" \"avatar_url\": \"https://avatars.githubusercontent.com/u/583231?v=4\",\n" +
" \"bio\": null,\n" +
" \"blog\": \"https://github.blog\",\n" +
" \"company\": \"@github\",\n" +
" \"created_at\": \"2011-01-25T18:44:36Z\",\n" +
" \"email\": null,\n" +
" \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n" +
" \"followers\": 8481,\n" +
" \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n" +
" \"following\": 9,\n" +
" \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n" +
" \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n" +
" \"gravatar_id\": \"\",\n" +
" \"hireable\": null,\n" +
" \"html_url\": \"https://github.com/octocat\",\n" +
" \"id\": 583231,\n" +
" \"location\": \"San Francisco\",\n" +
" \"login\": \"octocat\",\n" +
" \"name\": \"The Octocat\",\n" +
" \"node_id\": \"MDQ6VXNlcjU4MzIzMQ==\",\n" +
" \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n" +
" \"public_gists\": 8,\n" +
" \"public_repos\": 8,\n" +
" \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n" +
" \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n" +
" \"site_admin\": false,\n" +
" \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n" +
" \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n" +
" \"twitter_username\": null,\n" +
" \"type\": \"User\",\n" +
" \"updated_at\": \"2023-02-25T12:14:58Z\",\n" +
" \"url\": \"https://api.github.com/users/octocat\"\n" +
"}";
// @formatter:on
this.server.enqueue(new MockResponse().setBody(responseJson));
User user = this.gitHubUserApi.getUser();
assertThat(user.getId()).isEqualTo(583231);
assertThat(user.getLogin()).isEqualTo("octocat");
assertThat(user.getName()).isEqualTo("The Octocat");
assertThat(user.getUrl()).isEqualTo("https://api.github.com/users/octocat");
}
@Test
public void getUserWhenErrorResponseThenException() {
this.server.enqueue(new MockResponse().setResponseCode(400));
// @formatter:off
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> this.gitHubUserApi.getUser());
// @formatter:on
}
}
@@ -0,0 +1,51 @@
/*
* 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.
* 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.method.configuration;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier;
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
private final Supplier<ReactiveAuthorizationManager<T>> delegate;
DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
ReactiveAuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> {
ObservationRegistry registry = provider.getIfAvailable(() -> ObservationRegistry.NOOP);
if (registry.isNoop()) {
return delegate;
}
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
});
}
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
return this.delegate.get().check(authentication, object);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -28,7 +28,6 @@ import org.springframework.context.annotation.Role;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
@@ -93,11 +92,7 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
static <T> ReactiveAuthorizationManager<T> manager(ReactiveAuthorizationManager<T> delegate,
ObjectProvider<ObservationRegistry> registryProvider) {
ObservationRegistry registry = registryProvider.getIfAvailable(() -> ObservationRegistry.NOOP);
if (registry.isNoop()) {
return delegate;
}
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -35,6 +35,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -326,6 +328,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
*/
private LogoutFilter createLogoutFilter(H http) {
this.contextLogoutHandler.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
this.logoutHandlers.add(this.contextLogoutHandler);
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
@@ -337,6 +340,14 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
return result;
}
private SecurityContextRepository getSecurityContextRepository(H http) {
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
if (securityContextRepository == null) {
securityContextRepository = new HttpSessionSecurityContextRepository();
}
return securityContextRepository;
}
private RequestMatcher getLogoutRequestMatcher(H http) {
if (this.logoutRequestMatcher != null) {
return this.logoutRequestMatcher;
@@ -324,7 +324,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
/**
* Controls the maximum number of sessions for a user. The default is to allow any
* number of users.
* number of sessions.
* @param maximumSessions the maximum number of sessions for a user
* @return the {@link SessionManagementConfigurer} for further customizations
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -208,30 +208,49 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
ParserContext parserContext) {
String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID);
String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION);
RelyingPartyRegistration.Builder builder;
if (StringUtils.hasText(metadataLocation)) {
builder = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId);
}
else {
builder = RelyingPartyRegistration.withRegistrationId(registrationId)
.assertingPartyDetails((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt,
assertingParties, apBuilder, parserContext));
}
addRemainingProperties(relyingPartyRegistrationElt, builder);
return builder;
}
private static void addRemainingProperties(Element relyingPartyRegistrationElt,
RelyingPartyRegistration.Builder builder) {
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
String singleLogoutServiceLocation = relyingPartyRegistrationElt
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt);
if (StringUtils.hasText(metadataLocation)) {
return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId)
.singleLogoutServiceLocation(singleLogoutServiceLocation)
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
.singleLogoutServiceBinding(singleLogoutServiceBinding);
}
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
String assertionConsumerServiceLocation = relyingPartyRegistrationElt
.getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION);
Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding(
relyingPartyRegistrationElt);
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId)
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
.assertionConsumerServiceBinding(assertionConsumerServiceBinding)
.singleLogoutServiceLocation(singleLogoutServiceLocation)
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
.singleLogoutServiceBinding(singleLogoutServiceBinding)
.assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties,
builder, parserContext));
if (StringUtils.hasText(entityId)) {
builder.entityId(entityId);
}
if (StringUtils.hasText(singleLogoutServiceLocation)) {
builder.singleLogoutServiceLocation(singleLogoutServiceLocation);
}
if (StringUtils.hasText(singleLogoutServiceResponseLocation)) {
builder.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation);
}
if (singleLogoutServiceBinding != null) {
builder.singleLogoutServiceBinding(singleLogoutServiceBinding);
}
if (StringUtils.hasText(assertionConsumerServiceLocation)) {
builder.assertionConsumerServiceLocation(assertionConsumerServiceLocation);
}
if (assertionConsumerServiceBinding != null) {
builder.assertionConsumerServiceBinding(assertionConsumerServiceBinding);
}
}
private static void buildAssertingParty(Element relyingPartyElt, Map<String, Map<String, Object>> assertingParties,
@@ -309,7 +328,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
if (StringUtils.hasText(assertionConsumerServiceBinding)) {
return Saml2MessageBinding.valueOf(assertionConsumerServiceBinding);
}
return Saml2MessageBinding.REDIRECT;
return null;
}
private static Saml2MessageBinding getSingleLogoutServiceBinding(Element relyingPartyRegistrationElt) {
@@ -317,7 +336,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
if (StringUtils.hasText(singleLogoutServiceBinding)) {
return Saml2MessageBinding.valueOf(singleLogoutServiceBinding);
}
return Saml2MessageBinding.POST;
return null;
}
private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) {
@@ -48,7 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ExtendWith(SpringTestContextExtension.class)
public class HttpSecurityDeferAddFilterTest {
public class HttpSecurityDeferAddFilterTests {
public final SpringTestContext spring = new SpringTestContext(this);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -16,6 +16,8 @@
package org.springframework.security.config.annotation.web.configurers;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.http.HttpHeaders;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -39,6 +43,8 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -48,6 +54,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -324,6 +331,80 @@ public class LogoutConfigurerTests {
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
}
@Test
public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception {
CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class);
this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder logoutRequest = post("/logout")
.with(csrf())
.with(user("user"))
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
this.mvc.perform(logoutRequest)
.andExpect(status().isFound())
.andExpect(redirectedUrl("/login?logout"));
// @formatter:on
int invocationCount = 2; // 1 from user() post processor and 1 from
// SecurityContextLogoutHandler
verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(),
any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception {
this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire();
MockHttpSession session = mock(MockHttpSession.class);
// @formatter:off
MockHttpServletRequestBuilder logoutRequest = post("/logout")
.with(csrf())
.with(user("user"))
.session(session)
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
this.mvc.perform(logoutRequest)
.andExpect(status().isFound())
.andExpect(redirectedUrl("/login?logout"))
.andReturn();
// @formatter:on
verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
}
@Configuration
@EnableWebSecurity
static class InvalidateHttpSessionFalseConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.logout((logout) -> logout.invalidateHttpSession(false))
.securityContext((context) -> context.requireExplicitSave(true));
return http.build();
// @formatter:on
}
}
@Configuration
@EnableWebSecurity
static class CustomSecurityContextRepositoryConfig {
static SecurityContextRepository repository;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.logout(Customizer.withDefaults())
.securityContext((context) -> context
.requireExplicitSave(true)
.securityContextRepository(repository)
);
return http.build();
// @formatter:on
}
}
@Configuration
@EnableWebSecurity
static class NullLogoutSuccessHandlerConfig {
@@ -53,7 +53,7 @@ import static org.mockito.Mockito.verifyNoInteractions;
* @author Alavudin Kuttikkattil
*/
@ExtendWith(SpringTestContextExtension.class)
public class ReactiveOAuth2ClientImportSelectorTest {
public class ReactiveOAuth2ClientImportSelectorTests {
public final SpringTestContext spring = new SpringTestContext(this);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -62,6 +62,27 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
"</b:beans>\n";
// @formatter:on
// @formatter:off
private static final String METADATA_LOCATION_OVERRIDE_PROPERTIES_XML_CONFIG = "<b:beans xmlns:b=\"http://www.springframework.org/schema/beans\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xmlns=\"http://www.springframework.org/schema/security\"\n" +
" xsi:schemaLocation=\"\n" +
"\t\t\thttp://www.springframework.org/schema/security\n" +
"\t\t\thttps://www.springframework.org/schema/security/spring-security.xsd\n" +
"\t\t\thttp://www.springframework.org/schema/beans\n" +
"\t\t\thttps://www.springframework.org/schema/beans/spring-beans.xsd\">\n" +
" \n" +
" <relying-party-registrations>\n" +
" <relying-party-registration registration-id=\"one\"\n" +
" entity-id=\"https://rp.example.org\"\n" +
" metadata-location=\"${metadata-location}\"\n" +
" assertion-consumer-service-location=\"https://rp.example.org/location\"\n" +
" assertion-consumer-service-binding=\"REDIRECT\"/>" +
" </relying-party-registrations>\n" +
"\n" +
"</b:beans>\n";
// @formatter:on
// @formatter:off
private static final String METADATA_RESPONSE = "<?xml version=\"1.0\"?>\n" +
"<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" entityID=\"https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php\" ID=\"_e793a707d3e1a9ee6cbec7454fdad2c7cd793dd3703179a527b9620a6e9682af\"><ds:Signature>\n" +
@@ -143,6 +164,41 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
.containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
}
@Test
public void parseWhenMetadataLocationConfiguredAndRegistrationHasPropertiesThenDoNotOverrideSpecifiedProperties()
throws Exception {
this.server = new MockWebServer();
this.server.start();
String serverUrl = this.server.url("/").toString();
this.server.enqueue(xmlResponse(METADATA_RESPONSE));
String metadataConfig = METADATA_LOCATION_OVERRIDE_PROPERTIES_XML_CONFIG.replace("${metadata-location}",
serverUrl);
this.spring.context(metadataConfig).autowire();
assertThat(this.relyingPartyRegistrationRepository)
.isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class);
RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository
.findByRegistrationId("one");
RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = relyingPartyRegistration
.getAssertingPartyDetails();
assertThat(relyingPartyRegistration).isNotNull();
assertThat(relyingPartyRegistration.getRegistrationId()).isEqualTo("one");
assertThat(relyingPartyRegistration.getEntityId()).isEqualTo("https://rp.example.org");
assertThat(relyingPartyRegistration.getAssertionConsumerServiceLocation())
.isEqualTo("https://rp.example.org/location");
assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding())
.isEqualTo(Saml2MessageBinding.REDIRECT);
assertThat(assertingPartyDetails.getEntityId())
.isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php");
assertThat(assertingPartyDetails.getWantAuthnRequestsSigned()).isFalse();
assertThat(assertingPartyDetails.getVerificationX509Credentials()).hasSize(1);
assertThat(assertingPartyDetails.getEncryptionX509Credentials()).hasSize(1);
assertThat(assertingPartyDetails.getSingleSignOnServiceLocation())
.isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php");
assertThat(assertingPartyDetails.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
assertThat(assertingPartyDetails.getSigningAlgorithms())
.containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
}
@Test
public void parseWhenSingleRelyingPartyRegistrationThenAvailableInRepository() {
this.spring.configLocations(xml("SingleRegistration")).autowire();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -25,13 +25,13 @@ import org.springframework.test.web.servlet.ResultMatcher;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Ensures that the MockMvcResult redirects to the saved ReqeuestCache.getRedirectUrl().
* Ensures that the MockMvcResult redirects to the saved RequestCache.getRedirectUrl().
*/
public final class RequestCacheResultMatcher {
/**
* Verifies that the MockMvcResult redirects to the saved
* ReqeustCache.getRedirectUrl().
* RequestCache.getRedirectUrl().
* @return a ResultMatcher that performs the verification.
*/
public static ResultMatcher redirectToCachedRequest() {
@@ -39,7 +39,7 @@ public final class RequestCacheResultMatcher {
RequestCache requestCache = new HttpSessionRequestCache();
MockHttpServletResponse response = mvcResult.getResponse();
SavedRequest savedRequest = requestCache.getRequest(mvcResult.getRequest(), response);
assertThat(savedRequest).describedAs("savedReqeust cannot be null").isNotNull();
assertThat(savedRequest).describedAs("savedRequest cannot be null").isNotNull();
String cachedRedirectUrl = savedRequest.getRedirectUrl();
assertThat(response.getRedirectedUrl()).isEqualTo(cachedRedirectUrl);
};
@@ -61,7 +61,16 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
this(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* Creates a new instance using the provided {@link PasswordEncoder}
* @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null.
* @since 6.0.3
*/
public DaoAuthenticationProvider(PasswordEncoder passwordEncoder) {
setPasswordEncoder(passwordEncoder);
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -62,12 +62,15 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
/**
* Creates an instance of {@link AuthorityAuthorizationManager} with the provided
* authority.
* @param role the authority to check for prefixed with "ROLE_"
* @param role the authority to check for prefixed with "ROLE_". Role should not start
* with "ROLE_" since it is automatically prepended already.
* @param <T> the type of object being authorized
* @return the new instance
*/
public static <T> AuthorityAuthorizationManager<T> hasRole(String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(!role.startsWith(ROLE_PREFIX), () -> role + " should not start with " + ROLE_PREFIX + " since "
+ ROLE_PREFIX + " is automatically prepended when using hasRole. Consider using hasAuthority instead.");
return hasAuthority(ROLE_PREFIX + role);
}
@@ -86,7 +89,8 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
/**
* Creates an instance of {@link AuthorityAuthorizationManager} with the provided
* authorities.
* @param roles the authorities to check for prefixed with "ROLE_"
* @param roles the authorities to check for prefixed with "ROLE_". Each role should
* not start with "ROLE_" since it is automatically prepended already.
* @param <T> the type of object being authorized
* @return the new instance
*/
@@ -125,7 +129,11 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
private static String[] toNamedRolesArray(String rolePrefix, String[] roles) {
String[] result = new String[roles.length];
for (int i = 0; i < roles.length; i++) {
result[i] = rolePrefix + roles[i];
String role = roles[i];
Assert.isTrue(!role.startsWith(rolePrefix), () -> role + " should not start with " + rolePrefix + " since "
+ rolePrefix
+ " is automatically prepended when using hasAnyRole. Consider using hasAnyAuthority instead.");
result[i] = rolePrefix + role;
}
return result;
}
@@ -25,7 +25,7 @@ import org.springframework.security.core.Authentication;
* A reactive authorization manager which can determine if an {@link Authentication} has
* access to a specific object.
*
* @param <T> the type of object that the authorization check is being done one.
* @param <T> the type of object that the authorization check is being done on.
* @author Rob Winch
* @since 5.0
*/
@@ -29,7 +29,7 @@ import org.springframework.security.access.intercept.aspectj.MethodInvocationAda
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
public class PostInvocationAdviceProviderTest {
public class PostInvocationAdviceProviderTests {
@Mock
private PostInvocationAuthorizationAdvice authorizationAdvice;
@@ -441,6 +441,13 @@ public class DaoAuthenticationProviderTests {
assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(() -> provider.authenticate(token));
}
@Test
public void constructWhenPasswordEncoderProvidedThenSets() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(
NoOpPasswordEncoder.getInstance());
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
}
/**
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -44,6 +44,15 @@ public class AuthorityAuthorizationManagerTests {
.withMessage("role cannot be null");
}
@Test
public void hasRoleWhenContainRoleWithRolePrefixThenException() {
String ROLE_PREFIX = "ROLE_";
String ROLE_USER = ROLE_PREFIX + "USER";
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasRole(ROLE_USER))
.withMessage(ROLE_USER + " should not start with " + ROLE_PREFIX + " since " + ROLE_PREFIX
+ " is automatically prepended when using hasRole. Consider using hasAuthority instead.");
}
@Test
public void hasAuthorityWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAuthority(null))
@@ -76,6 +85,16 @@ public class AuthorityAuthorizationManagerTests {
.withMessage("rolePrefix cannot be null");
}
@Test
public void hasAnyRoleWhenContainRoleWithRolePrefixThenException() {
String ROLE_PREFIX = "ROLE_";
String ROLE_USER = ROLE_PREFIX + "USER";
assertThatIllegalArgumentException()
.isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(new String[] { ROLE_USER }))
.withMessage(ROLE_USER + " should not start with " + ROLE_PREFIX + " since " + ROLE_PREFIX
+ " is automatically prepended when using hasAnyRole. Consider using hasAnyAuthority instead.");
}
@Test
public void hasAnyAuthorityWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null))
+10 -10
View File
@@ -12,20 +12,20 @@ dependencies {
api platform("io.rsocket:rsocket-bom:1.1.3")
api platform("org.junit:junit-bom:5.9.2")
api platform("org.mockito:mockito-bom:4.8.1")
api platform("org.springframework.data:spring-data-bom:2022.0.2")
api platform("org.springframework.data:spring-data-bom:2022.0.5")
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")
api platform("com.fasterxml.jackson:jackson-bom:2.14.2")
constraints {
api "ch.qos.logback:logback-classic:1.4.5"
api "ch.qos.logback:logback-classic:1.4.6"
api "com.google.inject:guice:3.0"
api "com.nimbusds:nimbus-jose-jwt:9.24.4"
api "com.nimbusds:oauth2-oidc-sdk:9.43.1"
api "com.squareup.okhttp3:mockwebserver:3.14.9"
api "com.squareup.okhttp3:okhttp:3.14.9"
api "com.unboundid:unboundid-ldapsdk:6.0.7"
api "com.unboundid:unboundid-ldapsdk:6.0.8"
api "commons-collections:commons-collections:3.2.2"
api "io.mockk:mockk:1.13.4"
api "io.mockk:mockk:1.13.5"
api "io.micrometer:micrometer-observation:$micrometerVersion"
api "jakarta.annotation:jakarta.annotation-api:2.1.1"
api "jakarta.inject:jakarta.inject-api:2.0.1"
@@ -48,11 +48,11 @@ dependencies {
api "org.apache.httpcomponents:httpclient:4.5.14"
api "org.aspectj:aspectjrt:$aspectjVersion"
api "org.aspectj:aspectjweaver:$aspectjVersion"
api "org.assertj:assertj-core:3.23.1"
api "org.assertj:assertj-core:3.24.2"
api "org.bouncycastle:bcpkix-jdk15on:1.70"
api "org.bouncycastle:bcprov-jdk15on:1.70"
api "org.eclipse.jetty:jetty-server:11.0.13"
api "org.eclipse.jetty:jetty-servlet:11.0.13"
api "org.eclipse.jetty:jetty-server:11.0.15"
api "org.eclipse.jetty:jetty-servlet:11.0.15"
api "jakarta.persistence:jakarta.persistence-api:3.1.0"
api "org.hamcrest:hamcrest:2.2"
api "org.hibernate.orm:hibernate-core:6.1.7.Final"
@@ -67,13 +67,13 @@ dependencies {
api "org.seleniumhq.selenium:selenium-support:3.141.59"
api "org.skyscreamer:jsonassert:1.5.1"
api "org.slf4j:log4j-over-slf4j:1.7.36"
api "org.slf4j:slf4j-api:2.0.6"
api "org.springframework.ldap:spring-ldap-core:3.0.1"
api "org.slf4j:slf4j-api:2.0.7"
api "org.springframework.ldap:spring-ldap-core:3.0.2"
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
api 'org.apache.maven.resolver:maven-resolver-connector-basic:1.8.2'
api 'org.apache.maven.resolver:maven-resolver-impl:1.8.2'
api 'org.apache.maven.resolver:maven-resolver-transport-http:1.8.2'
api 'org.apache.maven:maven-resolver-provider:3.8.7'
api 'org.apache.maven:maven-resolver-provider:3.8.8'
}
}
@@ -213,7 +213,7 @@ There are convenience mechanisms to make this easier, but this is still not inte
.Java
[source,java,role="primary",attrs="-attributes"]
----
User user = User.withDefaultPasswordEncoder()
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
@@ -243,12 +243,12 @@ If you are creating multiple users, you can also reuse the builder:
[source,java,role="primary"]
----
UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
User admin = users
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
@@ -13,8 +13,14 @@ In Spring Security 6, the default behavior is that the xref:servlet/authenticati
Users now must explicitly save the `SecurityContext` with the `SecurityContextRepository` if they want the `SecurityContext` to persist between requests.
This removes ambiguity and improves performance by only requiring writing to the `SecurityContextRepository` (i.e. `HttpSession`) when it is necessary.
[NOTE]
====
Saving the context is also needed when clearing it out, for example during logout. Refer to this section to xref:servlet/authentication/session-management.adoc#properly-clearing-authentication[know more about that].
====
If you are explicitly opting into Spring Security 6's new defaults, the following configuration can be removed to accept the Spring Security 6 defaults.
include::partial$servlet/architecture/security-context-explicit.adoc[]
== Multiple SecurityContextRepository
@@ -6,6 +6,8 @@ After xref:reactive/test/web/setup.adoc[applying the Spring Security support to
.Java
[source,java,role="primary"]
----
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
@Test
public void messageWhenNotAuthenticated() throws Exception {
this.rest
@@ -66,6 +68,7 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception {
[source,kotlin,role="secondary"]
----
import org.springframework.test.web.reactive.server.expectBody
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser
//...
@@ -6,6 +6,8 @@ Spring Security also provides support for CSRF testing with `WebTestClient` -- f
.Java
[source,java,role="primary"]
----
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
this.rest
// provide a valid CSRF token
.mutateWith(csrf())
@@ -17,6 +19,8 @@ this.rest
.Kotlin
[source,kotlin,role="secondary"]
----
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
this.rest
// provide a valid CSRF token
.mutateWith(csrf())
@@ -6,6 +6,9 @@ The basic setup looks like this:
.Java
[source,java,role="primary"]
----
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
public class HelloWebfluxMethodApplicationTests {
@@ -31,6 +34,9 @@ public class HelloWebfluxMethodApplicationTests {
.Kotlin
[source,kotlin,role="secondary"]
----
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class])
class HelloWebfluxMethodApplicationTests {
@@ -152,7 +152,7 @@ Assuming that no other `SecurityFilterChain` instances match, `SecurityFilterCha
// FIXME: add link to pattern matching
Notice that `SecurityFilterChain~0~` has only three security `Filter` instances configured.
However, `SecurityFilterChain~n~` has four security `Filter` instanes configured.
However, `SecurityFilterChain~n~` has four security `Filter` instances configured.
It is important to note that each `SecurityFilterChain` can be unique and can be configured in isolation.
In fact, a `SecurityFilterChain` might have zero security `Filter` instances if the application wants Spring Security to ignore certain requests.
// FIXME: add link to configuring multiple `SecurityFilterChain` instances
@@ -217,7 +217,6 @@ image::{figures}/exceptiontranslationfilter.png[]
* image:{icondir}/number_2.png[] If the user is not authenticated or it is an `AuthenticationException`, then __Start Authentication__.
** The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out.
** The `HttpServletRequest` is <<savedrequests,saved>> so that it can be used to replay the original request once authentication is successful.
// FIXME: add link to authentication success
** The `AuthenticationEntryPoint` is used to request credentials from the client.
For example, it might redirect to a log in page or send a `WWW-Authenticate` header.
@@ -248,8 +248,9 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
* `SessionAuthenticationStrategy` is notified of a new login.
See the {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface.
* The <<servlet-authentication-authentication>> is set on the <<servlet-authentication-securitycontextholder>>.
Later, the `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession`.
See the {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class.
Later, if you need to save the `SecurityContext` so that it can be automatically set on future requests, `SecurityContextRepository#saveContext` must be explicitly invoked.
See the {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] class.
* `RememberMeServices.loginSuccess` is invoked.
If remember me is not configured, this is a no-op.
See the {security-api-url}org/springframework/security/web/authentication/rememberme/package-frame.html[`rememberme`] package.
@@ -12,6 +12,7 @@ The default is that accessing the URL `/logout` logs the user out by:
- Invalidating the HTTP Session
- Cleaning up any RememberMe authentication that was configured
- Clearing the `SecurityContextHolder`
- Clearing the `SecurityContextRepository`
- Redirecting to `/login?logout`
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
@@ -141,6 +142,7 @@ If not configured, a status code 200 is returned by default.
[[jc-logout-references]]
== Further Logout-Related References
- xref:servlet/authentication/session-management.adoc#properly-clearing-authentication[Properly Clearing Authentication When Explicit Save Is Enabled]
- <<ns-logout, Logout Handling>>
- xref:servlet/test/mockmvc/logout.adoc#test-logout[Testing Logout]
- xref:servlet/integrations/servlet-api.adoc#servletapi-logout[`HttpServletRequest.logout()`]
@@ -47,17 +47,17 @@ image:{icondir}/number_3.png[] If authentication fails, then __Failure__.
If remember me is not configured, this is a no-op.
See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc.
. `AuthenticationFailureHandler` is invoked.
See the {security-api-url}springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] class in the Javadoc
See the {security-api-url}org/springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] class in the Javadoc
image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
. `SessionAuthenticationStrategy` is notified of a new login.
See the {security-api-url}springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface in the Javadoc.
See the {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface in the Javadoc.
. The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
See the {security-api-url}springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class in the Javadoc.
See the {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class in the Javadoc.
. `RememberMeServices.loginSuccess` is invoked.
If remember me is not configured, this is a no-op.
See the {security-api-url}springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc.
See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc.
. `ApplicationEventPublisher` publishes an `InteractiveAuthenticationSuccessEvent`.
. The `AuthenticationSuccessHandler` is invoked. Typically, this is a `SimpleUrlAuthenticationSuccessHandler`, which redirects to a request saved by xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] when we redirect to the login page.
@@ -12,6 +12,7 @@ But before you leave, consider if any of these use cases fit your application:
* I want to <<understanding-session-management-components,Understand Session Management's components>>
* I want to <<ns-concurrent-sessions,restrict the number of times>> a user can be logged in concurrently
* I want <<store-authentication-manually,to store the authentication directly>> myself instead of Spring Security doing it for me
* I am storing the authentication manually and I want <<properly-clearing-authentication,to remove it>>
* I am using <<the-sessionmanagementfilter, `SessionManagementFilter`>> and I need <<moving-away-from-sessionmanagementfilter,guidance on moving away from that>>
* I want to store the authentication <<customizing-where-authentication-is-stored,in something other than the session>>
* I am using a <<stateless-authentication, stateless authentication>>, but <<storing-stateless-authentication-in-the-session,I'd still like to store it in the session>>
@@ -84,12 +85,6 @@ By default, Spring Security stores the security context for you in the HTTP sess
First, you need to create an implementation of `SecurityContextRepository` or use an existing implementation like `HttpSessionSecurityContextRepository`, then you can set it in `HttpSecurity`.
[NOTE]
====
The above configuration sets the `SecurityContextRepository` on the `SecurityContextHolderFilter` and **participating** authentication filters, like `UsernamePasswordAuthenticationFilter`.
To also set it in stateless filters, please see <<storing-stateless-authentication-in-the-session,how to customize the `SecurityContextRepository` for Stateless Authentication>>.
====
[[customizing-the-securitycontextrepository]]
.Customizing the `SecurityContextRepository`
====
@@ -134,6 +129,12 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
----
====
[NOTE]
====
The above configuration sets the `SecurityContextRepository` on the `SecurityContextHolderFilter` and **participating** authentication filters, like `UsernamePasswordAuthenticationFilter`.
To also set it in stateless filters, please see <<storing-stateless-authentication-in-the-session,how to customize the `SecurityContextRepository` for Stateless Authentication>>.
====
If you are using a custom authentication mechanism, you might want to <<store-authentication-manually,store the `Authentication` by yourself>>.
[[store-authentication-manually]]
@@ -157,7 +158,7 @@ public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest req
Authentication authentication = authenticationManager.authenticate(token); <4>
SecurityContext context = securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication); <5>
securityContextHolderStrategy.setContext(authentication);
securityContextHolderStrategy.setContext(context);
securityContextRepository.saveContext(context, request, response); <6>
}
@@ -181,6 +182,32 @@ class LoginRequest {
And that's it.
If you are not sure what `securityContextHolderStrategy` is in the above example, you can read more about it in the <<use-securitycontextholderstrategy, Using `SecurityContextStrategy` section>>.
[[properly-clearing-authentication]]
=== Properly Clearing an Authentication
If you are using Spring Security's xref:servlet/authentication/logout.adoc[Logout Support] then it handles a lot of stuff for you including clearing and saving the context.
But, let's say you need to manually log users out of your app. In that case, you'll need to make sure you're clearing and saving the context properly.
Now, you might already be familiar with clearing the `SecurityContextHolder` by doing `SecurityContextHolderStrategy#clearContext()`.
That's great, but if your app requires an xref:migration/servlet/session-management.adoc#_require_explicit_saving_of_securitycontextrepository[explicit save of the context], simply clearing it isn't enough.
The reason is that it doesn't remove it from the `SecurityContextRepository`, which means the `SecurityContext` could still be available for the next requests, and we definitely don't want that.
To make sure the authentication is properly cleared and saved, you can invoke {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[the `SecurityContextLogoutHandler`] which does that for us, like so:
====
.Java
[source,java,role="primary"]
----
SecurityContextLogoutHandler handler = new SecurityContextLogoutHandler(); <1>
handler.logout(httpServletRequest, httpServletResponse, null); <2>
----
====
<1> Create a new instance of `SecurityContextLogoutHandler`
<2> Call the `logout` method passing in the `HttpServletRequest`, `HttpServletResponse` and a `null` authentication because it is not required for this handler.
It's important to remember that clearing and saving the context is just one piece of the logout process, therefore we recommend having Spring Security take care of it.
[[stateless-authentication]]
=== Configuring Persistence for Stateless Authentication
@@ -793,7 +820,7 @@ Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = SecurityContextHolder.createEmptyContext(); <1>
context.setAuthentication(authentication); <2>
SecurityContextHolder.setContext(authentication); <3>
SecurityContextHolder.setContext(context); <3>
----
====
@@ -825,7 +852,7 @@ public class SomeClass {
// ...
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); <1>
context.setAuthentication(authentication); <2>
this.securityContextHolderStrategy.setContext(authentication); <3>
this.securityContextHolderStrategy.setContext(context); <3>
}
}
@@ -225,6 +225,11 @@ AccessDecisionVoter hierarchyVoter() {
----
====
[NOTE]
`RoleHierarchy` bean configuration is not yet ported over to `@EnableMethodSecurity`.
As such this example is using `AccessDecisionVoter`.
If you need `RoleHierarchy` support for method security, please continue using `@EnableGlobalMethodSecurity` until https://github.com/spring-projects/spring-security/issues/12783 is complete.
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AuthorizationManager` adapted to call the above `RoleHierarchyVoter`.
The `>` symbol can be thought of as meaning "includes".
@@ -408,3 +408,46 @@ open class SecurityConfig {
<3> Allow access to URLs that start with `/user/` to users with the `USER` role, using `AntPathRequestMatcher`
<4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
<5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher`
== Expressions
It is recommended that you use type-safe authorization managers instead of SpEL.
However, `WebExpressionAuthorizationManager` is available to help migrate legacy SpEL.
To use `WebExpressionAuthorizationManager`, you can construct one with the expression you are trying to migrate, like so:
====
.Java
[source,java,role="primary"]
----
.requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
----
.Kotlin
[source,kotlin,role="secondary"]
----
.requestMatchers("/test/**").access(WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
----
====
If you are referring to a bean in your expression like so: `@webSecurity.check(authentication, request)`, it's recommended that you instead call the bean directly, which will look something like the following:
====
.Java
[source,java,role="primary"]
----
.requestMatchers("/test/**").access((authentication, context) ->
new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
----
.Kotlin
[source,kotlin,role="secondary"]
----
.requestMatchers("/test/**").access((authentication, context): AuthorizationManager<RequestAuthorizationContext> ->
AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
----
====
For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement `AuthorizationManager` and refer to them by calling `.access(AuthorizationManager)`.
If you are not able to do that, you can configure a `DefaultHttpSecurityExpressionHandler` with a bean resolver and supply that to `WebExpressionAuthorizationManager#setExpressionhandler`.
@@ -210,7 +210,7 @@ You could then refer to the method as follows:
----
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@webSecurity.checkUserId(authentication,#userId)"))
...
);
----
@@ -20,7 +20,7 @@ If you are using Spring Security, the `Principal` on the `HttpServletRequest` is
More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application.
[[websocket-configuration]]
[[websocket-authorization]]
== WebSocket Authorization
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
@@ -5,7 +5,7 @@
Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login.
This section shows how to configure the {gh-samples-url}/boot/oauth2login[*OAuth 2.0 Login sample*] by using _Google_ as the _Authentication Provider_ and covers the following topics:
This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] by using _Google_ as the _Authentication Provider_ and covers the following topics:
* <<oauth2login-sample-initial-setup>>
* <<oauth2login-sample-redirect-uri>>
@@ -1,17 +1,97 @@
[[servlet-saml2login-authenticate-responses]]
= Authenticating ``<saml2:Response>``s
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default.
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it.
You can configure this in a number of ways including:
1. Setting a clock skew to timestamp validation
2. Mapping the response to a list of `GrantedAuthority` instances
3. Customizing the strategy for validating assertions
4. Customizing the strategy for decrypting response and assertion elements
1. Changing the way the `RelyingPartyRegistration` is Looked Up
2. Setting a clock skew to timestamp validation
3. Mapping the response to a list of `GrantedAuthority` instances
4. Customizing the strategy for validating assertions
5. Customizing the strategy for decrypting response and assertion elements
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
[[relyingpartyregistrationresolver-apply]]
== Changing `RelyingPartyRegistration` Lookup
`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`].
To apply a `RelyingPartyRegistrationResolver` when processing `<saml2:Response>` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so:
====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) {
return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations));
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter {
return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations));
}
----
====
Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default.
If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata:
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
http
// ...
.saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso"))
// ...
return http.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
http {
// ...
.saml2Login {
filterProcessingUrl = "/saml2/login/sso"
}
// ...
}
return http.build()
}
----
====
and:
====
.Java
[source,java,role="primary"]
----
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
----
.Kotlin
[source,kotlin,role="secondary"]
----
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
----
====
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
== Setting a Clock Skew
@@ -41,6 +41,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
====
[[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
@@ -150,7 +151,7 @@ To achieve this, any interfaces or classes where Spring Security uses OpenSAML i
This makes it possible for you to switch out OpenSAML for some other library or an unsupported version of OpenSAML.
As a natural outcome of these two goals, Spring Security's SAML API is quite small relative to other modules.
Instead, such classes as `OpenSamlAuthenticationRequestFactory` and `OpenSamlAuthenticationProvider` expose `Converter` implementationss that customize various steps in the authentication process.
Instead, such classes as `OpenSamlAuthenticationRequestFactory` and `OpenSamlAuthenticationProvider` expose `Converter` implementations that customize various steps in the authentication process.
For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter delegates to `OpenSamlAuthenticationProvider`:
@@ -662,6 +663,16 @@ In a deployed application, that translates to:
`+https://rp.example.com/adfs+`
The prevailing URI patterns are as follows:
* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`] based on the configurations for that `RelyingPartyRegistration`
* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state
* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration`
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
[[servlet-saml2login-rpr-credentials]]
=== Credentials
@@ -736,58 +747,6 @@ resource.inputStream.use {
When you specify the locations of these files as the appropriate Spring Boot properties, Spring Boot performs these conversions for you.
====
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
=== Resolving the Relying Party from the Request
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration ID in the URI path.
You may want to customize for a number of reasons, including:
* You may know that your application is never going to be a multi-tenant application and, as a result, want a simpler URL scheme.
* You may identify tenants in a way other than by the URI path.
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
The default looks up the registration ID from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
You can provide a simpler resolver that, for example, always returns the same relying party:
====
.Java
[source,java,role="primary"]
----
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final RelyingPartyRegistrationResolver delegate;
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
}
@Override
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
return this.delegate.resolve(request, "single");
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
return this.delegate.resolve(request, "single")
}
}
----
====
Then you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce `<saml2:AuthnRequest>` instances], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate `<saml2:Response>` instances], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
[NOTE]
====
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
====
[[servlet-saml2login-rpr-duplicated]]
=== Duplicated Relying Party Configurations
@@ -884,3 +843,184 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
}
----
====
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
=== Resolving the `RelyingPartyRegistration` from the Request
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
There are a number of reasons you may want to customize that. Among them:
* You may already <<relyingpartyregistrationresolver-single, know which `RelyingPartyRegistration` you need>>
* You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>>
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
[NOTE]
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
[[relyingpartyregistrationresolver-single]]
==== Resolving to a Single Consistent `RelyingPartyRegistration`
You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`:
====
.Java
[source,java,role="primary"]
----
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final RelyingPartyRegistrationResolver delegate;
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
}
@Override
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
return this.delegate.resolve(request, "single");
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
return this.delegate.resolve(request, "single")
}
}
----
====
[TIP]
You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`<saml2:SPSSODescriptor>` metadata production].
[[relyingpartyregistrationresolver-entityid]]
==== Resolving Based on the `<saml2:Response#Issuer>`
When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <<servlet-saml2login-rpr-duplicated, relying party information duplicated across each instance>>.
This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable.
You can instead resolve the `registrationId` via the `Issuer`.
A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like:
====
.Java
[source,java,role="primary"]
----
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final InMemoryRelyingPartyRegistrationRepository registrations;
// ... constructor
@Override
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId);
}
String entityId = resolveEntityIdFromSamlResponse(request);
for (RelyingPartyRegistration registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration;
}
}
return null;
}
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
// ...
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
RelyingPartyRegistrationResolver {
@Override
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId)
}
String entityId = resolveEntityIdFromSamlResponse(request)
for (val registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration
}
}
return null
}
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
// ...
}
}
----
====
[TIP]
You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`<saml2:Response>` authentication].
[[federating-saml2-login]]
=== Federating Login
One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties.
In this case, the identity provider's metadata endpoint returns multiple `<md:IDPSSODescriptor>` elements.
These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so:
====
.Java
[source,java,role="primary"]
----
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
.stream().map((builder) -> builder
.registrationId(UUID.randomUUID().toString())
.entityId("https://example.org/saml2/sp")
.build()
)
.collect(Collectors.toList()));
----
.Kotlin
[source,java,role="secondary"]
----
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
.registrationId(UUID.randomUUID().toString())
.entityId("https://example.org/saml2/sp")
.build()
}
.collect(Collectors.toList()));
----
====
Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable.
There are several ways to address this; let's focus on a way that suits the specific use case of federation.
In many federation cases, all the asserting parties share service provider configuration.
Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`.
There are two main URIs you will want to change along those lines:
* <<relyingpartyregistrationresolver-entityid,Resolve by `<saml2:Response#Issuer>`>>
* <<relyingpartyregistrationresolver-single,Resolve with a default `RelyingPartyRegistration`>>
[NOTE]
Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal.
You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
[[using-spring-security-saml-extension-uris]]
=== Using Spring Security SAML Extension URIs
In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.
For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
@@ -1,5 +1,36 @@
[[servlet-saml2login-metadata]]
= Producing `<saml2:SPSSODescriptor>` Metadata
= Saml 2.0 Metadata
Spring Security can <<parsing-asserting-party-metadata,parse asserting party metadata>> to produce an `AssertingPartyDetails` instance as well as <<publishing-relying-party-metadata,publish relying party metadata>> from a `RelyingPartyRegistration` instance.
[[parsing-asserting-party-metadata]]
== Parsing `<saml2:IDPSSODescriptor>` metadata
You can parse an asserting party's metadata xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[using `RelyingPartyRegistrations`].
When using the OpenSAML vendor support, the resulting `AssertingPartyDetails` will be of type `OpenSamlAssertingPartyDetails`.
This means you'll be able to do get the underlying OpenSAML XMLObject by doing the following:
====
.Java
[source,java,role="primary"]
----
OpenSamlAssertingPartyDetails details = (OpenSamlAssertingPartyDetails)
registration.getAssertingPartyDetails();
EntityDescriptor openSamlEntityDescriptor = details.getEntityDescriptor();
----
.Kotlin
[source,kotlin,role="secondary"]
----
val details: OpenSamlAssertingPartyDetails =
registration.getAssertingPartyDetails() as OpenSamlAssertingPartyDetails;
val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor();
----
====
[[publishing-relying-party-metadata]]
== Producing `<saml2:SPSSODescriptor>` Metadata
You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
@@ -72,3 +103,45 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
----
====
== Changing the Way a `RelyingPartyRegistration` Is Looked Up
To apply a custom `RelyingPartyRegistrationResolver` to the metadata endpoint, you can provide it directly in the filter constructor like so:
====
.Java
[source,java,role="primary"]
----
RelyingPartyRegistrationResolver myRegistrationResolver = ...;
Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver());
// ...
http.addFilterBefore(metadata, BasicAuthenticationFilter.class);
----
.Kotlin
----
val myRegistrationResolver: RelyingPartyRegistrationResolver = ...;
val metadata = new Saml2MetadataFilter(myRegistrationResolver, OpenSamlMetadataResolver());
// ...
http.addFilterBefore(metadata, BasicAuthenticationFilter::class.java);
----
====
In the event that you are applying a `RelyingPartyRegistrationResolver` to remove the `registrationId` from the URI, you must also change the URI in the filter like so:
====
.Java
[source,java,role="primary"]
----
metadata.setRequestMatcher("/saml2/metadata")
----
.Kotlin
----
metadata.setRequestMatcher("/saml2/metadata")
----
====
+2 -1
View File
@@ -7,4 +7,5 @@
^http://www.w3.org/2000/09/xmldsig.*
^http://www.w3.org/2001/10/xml-exc-c14n
^http://www.w3.org/2001/04/xmldsig-more
^http://www.w3.org/2001/04/xmlenc
^http://www.w3.org/2001/04/xmlenc
^http://www.springframework.org/schema/security/.*
+5 -5
View File
@@ -1,11 +1,11 @@
aspectjVersion=1.9.19
reactorVersion=2022.0.3
springJavaformatVersion=0.0.35
reactorVersion=2022.0.6
springJavaformatVersion=0.0.38
springBootVersion=2.4.2
springFrameworkVersion=6.0.5
micrometerVersion=1.10.4
springFrameworkVersion=6.0.8
micrometerVersion=1.10.6
openSamlVersion=4.1.1
version=6.0.2
version=6.0.3
kotlinVersion=1.7.22
samplesBranch=main
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
@@ -18,6 +18,7 @@ package org.springframework.security.messaging.access.intercept;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
@@ -37,6 +38,7 @@ import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatche
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.function.SingletonSupplier;
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> {
@@ -87,6 +89,10 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
SimpDestinationMessageMatcher simp = (SimpDestinationMessageMatcher) matcher;
return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message));
}
if (matcher instanceof Builder.LazySimpDestinationMessageMatcher) {
Builder.LazySimpDestinationMessageMatcher path = (Builder.LazySimpDestinationMessageMatcher) matcher;
return new MessageAuthorizationContext<>(message, path.extractPathVariables(message));
}
return new MessageAuthorizationContext<>(message);
}
@@ -192,8 +198,7 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
for (String pattern : patterns) {
Supplier<MessageMatcher<Object>> supplier = new Builder.PathMatcherMessageMatcherBuilder(pattern, type);
MessageMatcher<?> matcher = new Builder.SupplierMessageMatcher(supplier);
MessageMatcher<Object> matcher = new LazySimpDestinationMessageMatcher(pattern, type);
matchers.add(matcher);
}
return new Builder.Constraint(matchers);
@@ -375,58 +380,33 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
}
private static final class SupplierMessageMatcher implements MessageMatcher<Object> {
private final class LazySimpDestinationMessageMatcher implements MessageMatcher<Object> {
private final Supplier<MessageMatcher<Object>> supplier;
private final Supplier<SimpDestinationMessageMatcher> delegate;
private volatile MessageMatcher<Object> delegate;
SupplierMessageMatcher(Supplier<MessageMatcher<Object>> supplier) {
this.supplier = supplier;
private LazySimpDestinationMessageMatcher(String pattern, SimpMessageType type) {
this.delegate = SingletonSupplier.of(() -> {
PathMatcher pathMatcher = Builder.this.pathMatcher.get();
if (type == null) {
return new SimpDestinationMessageMatcher(pattern, pathMatcher);
}
if (SimpMessageType.MESSAGE == type) {
return SimpDestinationMessageMatcher.createMessageMatcher(pattern, pathMatcher);
}
if (SimpMessageType.SUBSCRIBE == type) {
return SimpDestinationMessageMatcher.createSubscribeMatcher(pattern, pathMatcher);
}
throw new IllegalStateException(type + " is not supported since it does not have a destination");
});
}
@Override
public boolean matches(Message<?> message) {
if (this.delegate == null) {
synchronized (this.supplier) {
if (this.delegate == null) {
this.delegate = this.supplier.get();
}
}
}
return this.delegate.matches(message);
return this.delegate.get().matches(message);
}
}
private final class PathMatcherMessageMatcherBuilder implements Supplier<MessageMatcher<Object>> {
private final String pattern;
private final SimpMessageType type;
private PathMatcherMessageMatcherBuilder(String pattern, SimpMessageType type) {
this.pattern = pattern;
this.type = type;
}
private PathMatcher resolvePathMatcher() {
return Builder.this.pathMatcher.get();
}
@Override
public MessageMatcher<Object> get() {
PathMatcher pathMatcher = resolvePathMatcher();
if (this.type == null) {
return new SimpDestinationMessageMatcher(this.pattern, pathMatcher);
}
if (SimpMessageType.MESSAGE == this.type) {
return SimpDestinationMessageMatcher.createMessageMatcher(this.pattern, pathMatcher);
}
if (SimpMessageType.SUBSCRIBE == this.type) {
return SimpDestinationMessageMatcher.createSubscribeMatcher(this.pattern, pathMatcher);
}
throw new IllegalStateException(this.type + " is not supported since it does not have a destination");
Map<String, String> extractPathVariables(Message<?> message) {
return this.delegate.get().extractPathVariables(message);
}
}
@@ -0,0 +1,128 @@
/*
* 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.
* 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.messaging.access.intercept;
import java.util.Map;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MessageMatcherDelegatingAuthorizationManager}
*/
public final class MessageMatcherDelegatingAuthorizationManagerTests {
@Test
void checkWhenPermitAllThenPermits() {
AuthorizationManager<Message<?>> authorizationManager = builder().anyMessage().permitAll().build();
Message<?> message = new GenericMessage<>(new Object());
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
}
@Test
void checkWhenAnyMessageHasRoleThenRequires() {
AuthorizationManager<Message<?>> authorizationManager = builder().anyMessage().hasRole("USER").build();
Message<?> message = new GenericMessage<>(new Object());
Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
assertThat(authorizationManager.check(() -> user, message).isGranted()).isTrue();
Authentication admin = new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
assertThat(authorizationManager.check(() -> admin, message).isGranted()).isFalse();
}
@Test
void checkWhenSimpDestinationMatchesThenUses() {
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("destination").permitAll()
.anyMessage().denyAll().build();
MessageHeaders headers = new MessageHeaders(
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination"));
Message<?> message = new GenericMessage<>(new Object(), headers);
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
}
@Test
void checkWhenNullDestinationHeaderMatchesThenUses() {
AuthorizationManager<Message<?>> authorizationManager = builder().nullDestMatcher().permitAll().anyMessage()
.denyAll().build();
Message<?> message = new GenericMessage<>(new Object());
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
MessageHeaders headers = new MessageHeaders(
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination"));
message = new GenericMessage<>(new Object(), headers);
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isFalse();
}
@Test
void checkWhenSimpTypeMatchesThenUses() {
AuthorizationManager<Message<?>> authorizationManager = builder().simpTypeMatchers(SimpMessageType.CONNECT)
.permitAll().anyMessage().denyAll().build();
MessageHeaders headers = new MessageHeaders(
Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.CONNECT));
Message<?> message = new GenericMessage<>(new Object(), headers);
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
}
// gh-12540
@Test
void checkWhenSimpDestinationMatchesThenVariablesExtracted() {
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("destination/{id}")
.access(variable("id").isEqualTo("3")).anyMessage().denyAll().build();
MessageHeaders headers = new MessageHeaders(
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination/3"));
Message<?> message = new GenericMessage<>(new Object(), headers);
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
}
private MessageMatcherDelegatingAuthorizationManager.Builder builder() {
return MessageMatcherDelegatingAuthorizationManager.builder();
}
private Builder variable(String name) {
return new Builder(name);
}
private static final class Builder {
private final String name;
private Builder(String name) {
this.name = name;
}
AuthorizationManager<MessageAuthorizationContext<?>> isEqualTo(String value) {
return (authentication, object) -> {
String extracted = object.getVariables().get(this.name);
return new AuthorizationDecision(value.equals(extracted));
};
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 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.
@@ -55,6 +55,8 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -388,15 +390,19 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
});
ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri);
source.setWebClient(this.webClient);
Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = getExpectedJwsAlgorithms(jwsKeySelector);
Mono<ConfigurableJWTProcessor<JWKSecurityContext>> jwtProcessorMono = this.jwtProcessorCustomizer
Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = this.jwtProcessorCustomizer
.apply(source, jwtProcessor)
.map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
return (jwt) -> {
JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
return jwtProcessorMono.flatMap((processor) -> source.get(selector)
.onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex))
.map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList))));
return jwtProcessorMono.flatMap((tuple) -> {
JWTProcessor<JWKSecurityContext> processor = tuple.getT1();
Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = tuple.getT2();
JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
return source.get(selector)
.onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex))
.map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList)));
});
};
}
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.jwt;
import java.util.function.Supplier;
import org.springframework.util.function.SingletonSupplier;
/**
* A {@link JwtDecoder} that lazily initializes another {@link JwtDecoder}
*
@@ -26,12 +28,17 @@ import java.util.function.Supplier;
*/
public final class SupplierJwtDecoder implements JwtDecoder {
private final Supplier<JwtDecoder> jwtDecoderSupplier;
private volatile JwtDecoder delegate;
private final Supplier<JwtDecoder> delegate;
public SupplierJwtDecoder(Supplier<JwtDecoder> jwtDecoderSupplier) {
this.jwtDecoderSupplier = jwtDecoderSupplier;
this.delegate = SingletonSupplier.of(() -> {
try {
return jwtDecoderSupplier.get();
}
catch (Exception ex) {
throw wrapException(ex);
}
});
}
/**
@@ -39,19 +46,7 @@ public final class SupplierJwtDecoder implements JwtDecoder {
*/
@Override
public Jwt decode(String token) throws JwtException {
if (this.delegate == null) {
synchronized (this.jwtDecoderSupplier) {
if (this.delegate == null) {
try {
this.delegate = this.jwtDecoderSupplier.get();
}
catch (Exception ex) {
throw wrapException(ex);
}
}
}
}
return this.delegate.decode(token);
return this.delegate.get().decode(token);
}
private JwtDecoderInitializationException wrapException(Exception ex) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 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.
@@ -39,6 +39,8 @@ import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JWKSecurityContext;
@@ -365,6 +367,20 @@ public class NimbusReactiveJwtDecoderTests {
// @formatter:on
}
@Test
public void withJwkSetUriWhenJwtProcessorCustomizerSetsJWSKeySelectorThenUseCustomizedJWSKeySelector()
throws InvalidKeySpecException {
WebClient webClient = mockJwkSetResponse(new JWKSet(new RSAKey.Builder(key()).build()).toString());
// @formatter:off
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(SignatureAlgorithm.ES256).webClient(webClient)
.jwtProcessorCustomizer((p) -> p
.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.RS512, new JWKSecurityContextJWKSet())))
.build();
assertThat(decoder.decode(this.rsa512).block()).extracting(Jwt::getSubject).isEqualTo("test-subject");
// @formatter:on
}
@Test
public void withPublicKeyWhenNullThenThrowsException() {
// @formatter:off
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -60,7 +60,7 @@ public final class Saml2LogoutRequest implements Serializable {
private final String relyingPartyRegistrationId;
private Function<Map<String, String>, String> encoder;
private transient Function<Map<String, String>, String> encoder;
private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map<String, String> parameters, String id,
String relyingPartyRegistrationId) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -71,7 +71,12 @@ class OpenSamlMetadataAssertingPartyDetailsConverter {
if (xmlObject instanceof EntitiesDescriptor) {
EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject;
for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
builders.add(convert(descriptor));
if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
builders.add(convert(descriptor));
}
}
if (builders.isEmpty()) {
throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements");
}
return builders;
}
@@ -0,0 +1,35 @@
/*
* 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.
* 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.saml2.provider.service.registration;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
class OpenSamlMetadataRelyingPartyRegistrationConverter {
private final OpenSamlMetadataAssertingPartyDetailsConverter converter = new OpenSamlMetadataAssertingPartyDetailsConverter();
Collection<RelyingPartyRegistration.Builder> convert(InputStream source) {
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
for (RelyingPartyRegistration.AssertingPartyDetails.Builder builder : this.converter.convert(source)) {
builders.add(new RelyingPartyRegistration.Builder(builder));
}
return builders;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -89,8 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
@Override
public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return RelyingPartyRegistration
.withAssertingPartyDetails(this.converter.convert(inputMessage.getBody()).iterator().next().build());
return new RelyingPartyRegistration.Builder(this.converter.convert(inputMessage.getBody()).iterator().next());
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -26,6 +26,7 @@ import java.util.function.Consumer;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -737,7 +738,7 @@ public final class RelyingPartyRegistration {
public static final class Builder {
private String registrationId;
private Converter<AssertingPartyDetails, String> registrationId = AssertingPartyDetails::getEntityId;
private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
@@ -757,10 +758,15 @@ public final class RelyingPartyRegistration {
private String nameIdFormat = null;
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder;
private Builder(String registrationId) {
this.registrationId = registrationId;
this.registrationId = (party) -> registrationId;
this.assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
}
Builder(AssertingPartyDetails.Builder builder) {
this.assertingPartyDetailsBuilder = builder;
}
/**
@@ -769,7 +775,7 @@ public final class RelyingPartyRegistration {
* @return this object
*/
public Builder registrationId(String id) {
this.registrationId = id;
this.registrationId = (party) -> id;
return this;
}
@@ -967,11 +973,12 @@ public final class RelyingPartyRegistration {
this.singleLogoutServiceBindings.add(Saml2MessageBinding.POST);
}
return new RelyingPartyRegistration(this.registrationId, this.entityId,
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
this.singleLogoutServiceBindings, this.assertingPartyDetailsBuilder.build(), this.nameIdFormat,
this.decryptionX509Credentials, this.signingX509Credentials);
AssertingPartyDetails party = this.assertingPartyDetailsBuilder.build();
String registrationId = this.registrationId.convert(party);
return new RelyingPartyRegistration(registrationId, this.entityId, this.assertionConsumerServiceLocation,
this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation,
this.singleLogoutServiceResponseLocation, this.singleLogoutServiceBindings, party,
this.nameIdFormat, this.decryptionX509Credentials, this.signingX509Credentials);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -18,13 +18,11 @@ package org.springframework.security.saml2.provider.service.registration;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails;
/**
* A utility class for constructing instances of {@link RelyingPartyRegistration}
@@ -36,7 +34,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
*/
public final class RelyingPartyRegistrations {
private static final OpenSamlMetadataAssertingPartyDetailsConverter assertingPartyMetadataConverter = new OpenSamlMetadataAssertingPartyDetailsConverter();
private static final OpenSamlMetadataRelyingPartyRegistrationConverter relyingPartyRegistrationConverter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
@@ -215,11 +213,7 @@ public final class RelyingPartyRegistrations {
* @since 5.7
*/
public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadata(InputStream source) {
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
for (AssertingPartyDetails.Builder builder : assertingPartyMetadataConverter.convert(source)) {
builders.add(RelyingPartyRegistration.withAssertingPartyDetails(builder.build()));
}
return builders;
return relyingPartyRegistrationConverter.convert(source);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -30,10 +30,12 @@ import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.NameIDPolicy;
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder;
import org.w3c.dom.Element;
import org.springframework.core.convert.converter.Converter;
@@ -69,6 +71,8 @@ class OpenSamlAuthenticationRequestResolver {
private final NameIDBuilder nameIdBuilder;
private final NameIDPolicyBuilder nameIdPolicyBuilder;
private RequestMatcher requestMatcher = new AntPathRequestMatcher(
Saml2AuthenticationRequestResolver.DEFAULT_AUTHENTICATION_REQUEST_URI);
@@ -94,6 +98,9 @@ class OpenSamlAuthenticationRequestResolver {
Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML");
this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.nameIdBuilder, "nameIdBuilder must be configured in OpenSAML");
this.nameIdPolicyBuilder = (NameIDPolicyBuilder) registry.getBuilderFactory()
.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.nameIdPolicyBuilder, "nameIdPolicyBuilder must be configured in OpenSAML");
}
void setRelayStateResolver(Converter<HttpServletRequest, String> relayStateResolver) {
@@ -129,6 +136,11 @@ class OpenSamlAuthenticationRequestResolver {
authnRequest.setIssuer(iss);
authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
authnRequest.setAssertionConsumerServiceURL(registration.getAssertionConsumerServiceLocation());
if (registration.getNameIdFormat() != null) {
NameIDPolicy nameIdPolicy = this.nameIdPolicyBuilder.buildObject();
nameIdPolicy.setFormat(registration.getNameIdFormat());
authnRequest.setNameIDPolicy(nameIdPolicy);
}
authnRequestConsumer.accept(registration, authnRequest);
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
@@ -27,7 +27,7 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
import static org.assertj.core.api.Assertions.assertThat;
class Saml2AuthenticationExceptionMixinTest {
class Saml2AuthenticationExceptionMixinTests {
private ObjectMapper mapper;
@@ -0,0 +1,57 @@
/*
* 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.
* 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.saml2.provider.service.registration;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
private String metadata;
@BeforeEach
public void setup() throws Exception {
ClassPathResource resource = new ClassPathResource("test-metadata.xml");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
this.metadata = reader.lines().collect(Collectors.joining());
}
}
// gh-12667
@Test
public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception {
try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) {
this.converter.convert(source)
.forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails())
.isInstanceOf(OpenSamlAssertingPartyDetails.class));
}
}
}
@@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -243,4 +244,17 @@ public class RelyingPartyRegistrationsTests {
}
}
@Test
public void collectionFromMetadataLocationCanHandleFederationMetadata() {
Collection<RelyingPartyRegistration.Builder> federationMetadataWithSkippedSPEntries = RelyingPartyRegistrations
.collectionFromMetadataLocation("classpath:test-federated-metadata.xml");
assertThat(federationMetadataWithSkippedSPEntries.size()).isEqualTo(1);
}
@Test
public void collectionFromMetadataLocationWithoutIdpThenSaml2Exception() {
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> RelyingPartyRegistrations
.collectionFromMetadataLocation("classpath:test-metadata-without-idp.xml"));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -38,7 +38,7 @@ public final class TestRelyingPartyRegistrations {
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId).nameIdFormat("format")
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
.singleLogoutServiceLocation(singleLogoutServiceLocation)
.signingX509Credentials((c) -> c.add(signingCredential)).assertingPartyDetails(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -52,6 +52,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation());
assertThat(authnRequest.getProtocolBinding())
@@ -76,6 +77,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation());
assertThat(authnRequest.getProtocolBinding())
@@ -114,6 +116,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
.build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation());
assertThat(authnRequest.getProtocolBinding())
@@ -137,6 +140,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
.assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation());
assertThat(authnRequest.getProtocolBinding())
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 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.
@@ -16,6 +16,11 @@
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;
@@ -77,6 +82,23 @@ public class HttpSessionLogoutRequestRepositoryTests {
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isEqualTo(two);
}
@Test
void serializeAndDeserializeSaml2LogoutRequest() throws IOException, ClassNotFoundException {
Saml2LogoutRequest requestToSerialize = createLogoutRequest().relayState("state-serialized").build();
byte[] data;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
objectOutputStream.writeObject(requestToSerialize);
data = outputStream.toByteArray();
}
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream)) {
Saml2LogoutRequest deserializedRequest = (Saml2LogoutRequest) objectInputStream.readObject();
assertThat(requestToSerialize.getRelayState()).isEqualTo(deserializedRequest.getRelayState());
}
}
@Test
public void loadLogoutRequestWhenSavedAndStateParameterNullThenReturnNull() {
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="federation_root"
cacheDuration="P0Y0M0DT0H15M0.000S" validUntil="2099-03-04T20:18:29.383Z">
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
entityID="https://localhost/simplesaml/saml2/idp/metadata.php">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://localhost/simplesaml/saml2/idp/SingleLogoutService.php"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://localhost/simplesaml/saml2/idp/SSOService.php"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
<md:EntityDescriptor entityID="https://service.provider.org">
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:Extensions>
<idpdisco:DiscoveryResponse xmlns:idpdisco="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://nomp.se/saml/login?disco=true" index="0"/>
</md:Extensions>
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SingleLogout"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sp.provider.org/saml/SingleLogout"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SSO" index="0" isDefault="true"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sp.provider.org/saml/SSO" index="1"/>
<md:AttributeConsumingService index="0">
<md:ServiceName xml:lang="en">The SP</md:ServiceName>
<md:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" isRequired="true"/>
<md:RequestedAttribute FriendlyName="eduPersonPrincipalName" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" isRequired="true"/>
<md:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42"/>
<md:RequestedAttribute FriendlyName="surName" Name="urn:oid:2.5.4.4"/>
</md:AttributeConsumingService>
</md:SPSSODescriptor>
<md:Organization>
<md:OrganizationName xml:lang="en">Service Provider</md:OrganizationName>
<md:OrganizationDisplayName xml:lang="en">Service Provider</md:OrganizationDisplayName>
</md:Organization>
</md:EntityDescriptor>
</md:EntitiesDescriptor>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="federation_root"
cacheDuration="P0Y0M0DT0H15M0.000S" validUntil="2099-03-04T20:18:29.383Z">
<md:EntityDescriptor entityID="https://service.provider.org">
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:Extensions>
<idpdisco:DiscoveryResponse xmlns:idpdisco="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://nomp.se/saml/login?disco=true" index="0"/>
</md:Extensions>
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SingleLogout"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sp.provider.org/saml/SingleLogout"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SSO" index="0" isDefault="true"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sp.provider.org/saml/SSO" index="1"/>
<md:AttributeConsumingService index="0">
<md:ServiceName xml:lang="en">The SP</md:ServiceName>
<md:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" isRequired="true"/>
<md:RequestedAttribute FriendlyName="eduPersonPrincipalName" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" isRequired="true"/>
<md:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42"/>
<md:RequestedAttribute FriendlyName="surName" Name="urn:oid:2.5.4.4"/>
</md:AttributeConsumingService>
</md:SPSSODescriptor>
<md:Organization>
<md:OrganizationName xml:lang="en">Service Provider</md:OrganizationName>
<md:OrganizationDisplayName xml:lang="en">Service Provider</md:OrganizationDisplayName>
</md:Organization>
</md:EntityDescriptor>
</md:EntitiesDescriptor>
@@ -43,9 +43,9 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityMockMvcResultHandlersTest.Config.class)
@ContextConfiguration(classes = SecurityMockMvcResultHandlersTests.Config.class)
@WebAppConfiguration
public class SecurityMockMvcResultHandlersTest {
public class SecurityMockMvcResultHandlersTests {
@Autowired
private WebApplicationContext context;
@@ -26,6 +26,7 @@ import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.FilterChain;
@@ -257,7 +258,11 @@ public class FilterInvocation {
@Override
public Enumeration<String> getHeaders(String name) {
return Collections.enumeration(this.headers.get(name));
List<String> headerList = this.headers.get(name);
if (headerList == null) {
return Collections.emptyEnumeration();
}
return Collections.enumeration(headerList);
}
@Override
@@ -18,9 +18,8 @@ package org.springframework.security.web;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import io.micrometer.common.KeyValues;
@@ -227,49 +226,38 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F
class SimpleAroundFilterObservation implements AroundFilterObservation {
private final Iterator<Observation> observations;
private final ObservationReference before;
private final Observation before;
private final ObservationReference after;
private final Observation after;
private final AtomicReference<Observation.Scope> currentScope = new AtomicReference<>(null);
private final AtomicReference<ObservationReference> reference = new AtomicReference<>(
ObservationReference.NOOP);
SimpleAroundFilterObservation(Observation before, Observation after) {
this.before = before;
this.after = after;
this.observations = Arrays.asList(before, after).iterator();
this.before = new ObservationReference(before);
this.after = new ObservationReference(after);
}
@Override
public void start() {
if (this.observations.hasNext()) {
stop();
Observation observation = this.observations.next();
observation.start();
Observation.Scope scope = observation.openScope();
this.currentScope.set(scope);
if (this.reference.compareAndSet(ObservationReference.NOOP, this.before)) {
this.before.start();
return;
}
if (this.reference.compareAndSet(this.before, this.after)) {
this.before.stop();
this.after.start();
}
}
@Override
public void error(Throwable ex) {
Observation.Scope scope = this.currentScope.get();
if (scope == null) {
return;
}
scope.close();
scope.getCurrentObservation().error(ex);
this.reference.get().error(ex);
}
@Override
public void stop() {
Observation.Scope scope = this.currentScope.getAndSet(null);
if (scope == null) {
return;
}
scope.close();
scope.getCurrentObservation().stop();
this.reference.get().stop();
}
@Override
@@ -304,12 +292,50 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F
@Override
public Observation before() {
return this.before;
return this.before.observation;
}
@Override
public Observation after() {
return this.after;
return this.after.observation;
}
private static final class ObservationReference {
private static final ObservationReference NOOP = new ObservationReference(Observation.NOOP);
private final AtomicInteger state = new AtomicInteger(0);
private final Observation observation;
private volatile Observation.Scope scope;
private ObservationReference(Observation observation) {
this.observation = observation;
this.scope = Observation.Scope.NOOP;
}
private void start() {
if (this.state.compareAndSet(0, 1)) {
this.observation.start();
this.scope = this.observation.openScope();
}
}
private void error(Throwable error) {
if (this.state.get() == 1) {
this.scope.close();
this.scope.getCurrentObservation().error(error);
}
}
private void stop() {
if (this.state.compareAndSet(1, 2)) {
this.scope.close();
this.scope.getCurrentObservation().stop();
}
}
}
}
@@ -27,6 +27,8 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
/**
@@ -53,6 +55,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
private boolean clearAuthentication = true;
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
/**
* Requires the request to be passed in.
* @param request from which to obtain a HTTP session (cannot be null)
@@ -76,6 +80,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
if (this.clearAuthentication) {
context.setAuthentication(null);
}
SecurityContext emptyContext = this.securityContextHolderStrategy.createEmptyContext();
this.securityContextRepository.saveContext(emptyContext, request, response);
}
public boolean isInvalidateHttpSession() {
@@ -114,4 +120,14 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
this.clearAuthentication = clearAuthentication;
}
/**
* Sets the {@link SecurityContextRepository} to use. Default is
* {@link HttpSessionSecurityContextRepository}.
* @param securityContextRepository the {@link SecurityContextRepository} to use.
*/
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
}
@@ -59,6 +59,7 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.UrlUtils;
@@ -148,7 +149,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
private AuthenticationFailureHandler failureHandler;
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
@Override
public void afterPropertiesSet() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -46,7 +46,14 @@ public final class DelegatingSecurityContextRepository implements SecurityContex
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
return loadDeferredContext(requestResponseHolder.getRequest()).get();
SecurityContext result = null;
for (SecurityContextRepository delegate : this.delegates) {
SecurityContext delegateResult = delegate.loadContext(requestResponseHolder);
if (result == null || delegate.containsContext(requestResponseHolder.getRequest())) {
result = delegateResult;
}
}
return result;
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -149,13 +149,46 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
SaveContextOnUpdateOrErrorResponseWrapper.class);
if (responseWrapper == null) {
boolean httpSessionExists = request.getSession(false) != null;
SecurityContext initialContext = this.securityContextHolderStrategy.createEmptyContext();
responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
saveContextInHttpSession(context, request);
return;
}
responseWrapper.saveContext(context);
}
private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
if (isTransient(context) || isTransient(context.getAuthentication())) {
return;
}
SecurityContext emptyContext = generateNewContext();
if (emptyContext.equals(context)) {
HttpSession session = request.getSession(false);
removeContextFromSession(context, session);
}
else {
boolean createSession = this.allowSessionCreation;
HttpSession session = request.getSession(createSession);
setContextInSession(context, session);
}
}
private void setContextInSession(SecurityContext context, HttpSession session) {
if (session != null) {
session.setAttribute(this.springSecurityContextKey, context);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session));
}
}
}
private void removeContextFromSession(SecurityContext context, HttpSession session) {
if (session != null) {
session.removeAttribute(this.springSecurityContextKey);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Removed %s from HttpSession [%s]", context, session));
}
}
}
@Override
public boolean containsContext(HttpServletRequest request) {
HttpSession session = request.getSession(false);
@@ -392,11 +425,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
// We may have a new session, so check also whether the context attribute
// is set SEC-1561
if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
httpSession.setAttribute(springSecurityContextKey, context);
HttpSessionSecurityContextRepository.this.saveContextInHttpSession(context, this.request);
this.isSaveContextInvoked = true;
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -17,12 +17,12 @@
package org.springframework.security.web.server;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
@@ -253,46 +253,81 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
class SimpleAroundWebFilterObservation implements AroundWebFilterObservation {
private final Iterator<Observation> observations;
private final ObservationReference before;
private final Observation before;
private final ObservationReference after;
private final Observation after;
private final AtomicReference<Observation> currentObservation = new AtomicReference<>(null);
private final AtomicReference<ObservationReference> currentObservation = new AtomicReference<>(
ObservationReference.NOOP);
SimpleAroundWebFilterObservation(Observation before, Observation after) {
this.before = before;
this.after = after;
this.observations = Arrays.asList(before, after).iterator();
this.before = new ObservationReference(before);
this.after = new ObservationReference(after);
}
@Override
public void start() {
if (this.observations.hasNext()) {
stop();
Observation observation = this.observations.next();
observation.start();
this.currentObservation.set(observation);
public Observation start() {
if (this.currentObservation.compareAndSet(ObservationReference.NOOP, this.before)) {
this.before.start();
return this.before.observation;
}
if (this.currentObservation.compareAndSet(this.before, this.after)) {
this.before.stop();
this.after.start();
return this.after.observation;
}
return Observation.NOOP;
}
@Override
public void error(Throwable ex) {
Observation observation = this.currentObservation.get();
if (observation == null) {
return;
}
observation.error(ex);
public Observation error(Throwable ex) {
this.currentObservation.get().error(ex);
return this.currentObservation.get().observation;
}
@Override
public void stop() {
Observation observation = this.currentObservation.getAndSet(null);
if (observation == null) {
return;
}
observation.stop();
this.currentObservation.get().stop();
}
@Override
public Observation contextualName(String contextualName) {
return this.currentObservation.get().observation.contextualName(contextualName);
}
@Override
public Observation parentObservation(Observation parentObservation) {
return this.currentObservation.get().observation.parentObservation(parentObservation);
}
@Override
public Observation lowCardinalityKeyValue(KeyValue keyValue) {
return this.currentObservation.get().observation.lowCardinalityKeyValue(keyValue);
}
@Override
public Observation highCardinalityKeyValue(KeyValue keyValue) {
return this.currentObservation.get().observation.highCardinalityKeyValue(keyValue);
}
@Override
public Observation observationConvention(ObservationConvention<?> observationConvention) {
return this.currentObservation.get().observation.observationConvention(observationConvention);
}
@Override
public Observation event(Event event) {
return this.currentObservation.get().observation.event(event);
}
@Override
public Context getContext() {
return this.currentObservation.get().observation.getContext();
}
@Override
public Scope openScope() {
return this.currentObservation.get().observation.openScope();
}
@Override
@@ -322,26 +357,64 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
.doOnError((t) -> {
error(t);
stop();
});
})
.contextWrite((context) -> context.put(ObservationThreadLocalAccessor.KEY, this));
// @formatter:on
};
}
@Override
public Observation before() {
return this.before;
return this.before.observation;
}
@Override
public Observation after() {
return this.after;
return this.after.observation;
}
@Override
public String toString() {
return this.currentObservation.get().observation.toString();
}
private static final class ObservationReference {
private static final ObservationReference NOOP = new ObservationReference(Observation.NOOP);
private final AtomicInteger state = new AtomicInteger(0);
private final Observation observation;
private ObservationReference(Observation observation) {
this.observation = observation;
}
private void start() {
if (this.state.compareAndSet(0, 1)) {
this.observation.start();
}
}
private void error(Throwable ex) {
if (this.state.get() == 1) {
this.observation.error(ex);
}
}
private void stop() {
if (this.state.compareAndSet(1, 2)) {
this.observation.stop();
}
}
}
}
}
interface WebFilterObservation {
interface WebFilterObservation extends Observation {
WebFilterObservation NOOP = new WebFilterObservation() {
};
@@ -353,13 +426,59 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
return new SimpleWebFilterObservation(observation);
}
default void start() {
@Override
default Observation contextualName(String contextualName) {
return Observation.NOOP;
}
default void error(Throwable ex) {
@Override
default Observation parentObservation(Observation parentObservation) {
return Observation.NOOP;
}
@Override
default Observation lowCardinalityKeyValue(KeyValue keyValue) {
return Observation.NOOP;
}
@Override
default Observation highCardinalityKeyValue(KeyValue keyValue) {
return Observation.NOOP;
}
@Override
default Observation observationConvention(ObservationConvention<?> observationConvention) {
return Observation.NOOP;
}
@Override
default Observation error(Throwable error) {
return Observation.NOOP;
}
@Override
default Observation event(Event event) {
return Observation.NOOP;
}
@Override
default Observation start() {
return Observation.NOOP;
}
@Override
default Context getContext() {
return new Observation.Context();
}
@Override
default void stop() {
}
@Override
default Scope openScope() {
return Scope.NOOP;
}
default WebFilter wrap(WebFilter filter) {
@@ -379,13 +498,13 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
}
@Override
public void start() {
this.observation.start();
public Observation start() {
return this.observation.start();
}
@Override
public void error(Throwable ex) {
this.observation.error(ex);
public Observation error(Throwable ex) {
return this.observation.error(ex);
}
@Override
@@ -393,6 +512,46 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
this.observation.stop();
}
@Override
public Observation contextualName(String contextualName) {
return this.observation.contextualName(contextualName);
}
@Override
public Observation parentObservation(Observation parentObservation) {
return this.observation.parentObservation(parentObservation);
}
@Override
public Observation lowCardinalityKeyValue(KeyValue keyValue) {
return this.observation.lowCardinalityKeyValue(keyValue);
}
@Override
public Observation highCardinalityKeyValue(KeyValue keyValue) {
return this.observation.highCardinalityKeyValue(keyValue);
}
@Override
public Observation observationConvention(ObservationConvention<?> observationConvention) {
return this.observation.observationConvention(observationConvention);
}
@Override
public Observation event(Event event) {
return this.observation.event(event);
}
@Override
public Context getContext() {
return this.observation.getContext();
}
@Override
public Scope openScope() {
return this.observation.openScope();
}
@Override
public WebFilter wrap(WebFilter filter) {
if (this.observation.isNoop()) {
@@ -419,7 +578,8 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
.doOnCancel(this.observation::stop).doOnError((t) -> {
this.observation.error(t);
this.observation.stop();
});
}).contextWrite(
(context) -> context.put(ObservationThreadLocalAccessor.KEY, this.observation));
};
}
@@ -16,6 +16,9 @@
package org.springframework.security.web;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -141,4 +144,23 @@ public class FilterInvocationTests {
assertThat(filterInvocation.getRequest().getServletContext()).isSameAs(mockServletContext);
}
@Test
public void testDummyRequestGetHeaders() {
DummyRequest request = new DummyRequest();
request.addHeader("known", "val");
Enumeration<String> headers = request.getHeaders("known");
assertThat(headers.hasMoreElements()).isTrue();
assertThat(headers.nextElement()).isEqualTo("val");
assertThat(headers.hasMoreElements()).isFalse();
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
}
@Test
public void testDummyRequestGetHeadersNull() {
DummyRequest request = new DummyRequest();
Enumeration<String> headers = request.getHeaders("unknown");
assertThat(headers.hasMoreElements()).isFalse();
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -132,12 +132,14 @@ public class DefaultLoginPageGeneratingFilterTests {
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(
new UsernamePasswordAuthenticationFilter());
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/login");
request.addParameter("login_error", "true");
MockHttpServletResponse response = new MockHttpServletResponse();
request.setQueryString("error");
MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
String message = messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials", Locale.KOREA);
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new BadCredentialsException(message));
filter.doFilter(request, new MockHttpServletResponse(), this.chain);
filter.doFilter(request, response, this.chain);
assertThat(response.getContentAsString()).contains(message);
}
// gh-5394
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -16,11 +16,15 @@
package org.springframework.security.web.authentication;
import jakarta.servlet.http.HttpSession;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.WebAttributes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -108,4 +112,20 @@ public class SimpleUrlAuthenticationSuccessHandlerTests {
assertThatIllegalArgumentException().isThrownBy(() -> ash.setTargetUrlParameter(" "));
}
@Test
public void shouldRemoveAuthenticationAttributeWhenOnAuthenticationSuccess() throws Exception {
SimpleUrlAuthenticationSuccessHandler ash = new SimpleUrlAuthenticationSuccessHandler();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HttpSession session = request.getSession();
assertThat(session).isNotNull();
session.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
new BadCredentialsException("Invalid credentials"));
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNotNull();
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION))
.isInstanceOf(AuthenticationException.class);
ash.onAuthenticationSuccess(request, response, mock(Authentication.class));
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNull();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -27,12 +27,19 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* @author Rob Winch
*
*/
public class SecurityContextLogoutHandlerTests {
@@ -76,4 +83,35 @@ public class SecurityContextLogoutHandlerTests {
assertThat(beforeContext.getAuthentication()).isSameAs(beforeAuthentication);
}
@Test
public void logoutWhenSecurityContextRepositoryThenSaveEmptyContext() {
SecurityContextRepository repository = mock(SecurityContextRepository.class);
this.handler.setSecurityContextRepository(repository);
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
}
@Test
public void logoutWhenClearAuthenticationFalseThenSaveEmptyContext() {
SecurityContextRepository repository = mock(SecurityContextRepository.class);
this.handler.setSecurityContextRepository(repository);
this.handler.setClearAuthentication(false);
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
}
@Test
public void constructorWhenDefaultSecurityContextRepositoryThenHttpSessionSecurityContextRepository() {
SecurityContextRepository securityContextRepository = (SecurityContextRepository) ReflectionTestUtils
.getField(this.handler, "securityContextRepository");
assertThat(securityContextRepository).isInstanceOf(HttpSessionSecurityContextRepository.class);
}
@Test
public void setSecurityContextRepositoryWhenNullThenException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.handler.setSecurityContextRepository(null))
.withMessage("securityContextRepository cannot be null");
}
}
@@ -49,7 +49,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.util.FieldUtils;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.test.util.ReflectionTestUtils;
@@ -510,10 +510,10 @@ public class SwitchUserFilterTests {
}
@Test
void filterWhenDefaultSecurityContextRepositoryThenRequestAttributeRepository() {
void filterWhenDefaultSecurityContextRepositoryThenHttpSessionRepository() {
SwitchUserFilter switchUserFilter = new SwitchUserFilter();
assertThat(ReflectionTestUtils.getField(switchUserFilter, "securityContextRepository"))
.isInstanceOf(RequestAttributeSecurityContextRepository.class);
.isInstanceOf(HttpSessionSecurityContextRepository.class);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -141,4 +141,64 @@ public class DelegatingSecurityContextRepositoryTests {
verifyNoInteractions(delegates.get(2));
}
// gh-12314
@Test
public void loadContextWhenSecondDelegateReturnsThenContextFromSecondDelegate() {
SecurityContextRepository delegate1 = mock(SecurityContextRepository.class);
SecurityContextRepository delegate2 = mock(SecurityContextRepository.class);
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(this.request, this.response);
SecurityContext securityContext1 = mock(SecurityContext.class);
SecurityContext securityContext2 = mock(SecurityContext.class);
given(delegate1.loadContext(holder)).willReturn(securityContext1);
given(delegate1.containsContext(holder.getRequest())).willReturn(false);
given(delegate2.loadContext(holder)).willReturn(securityContext2);
given(delegate2.containsContext(holder.getRequest())).willReturn(true);
DelegatingSecurityContextRepository repository = new DelegatingSecurityContextRepository(delegate1, delegate2);
SecurityContext returnedSecurityContext = repository.loadContext(holder);
assertThat(returnedSecurityContext).isSameAs(securityContext2);
}
// gh-12314
@Test
public void loadContextWhenBothDelegateReturnsThenContextFromSecondDelegate() {
SecurityContextRepository delegate1 = mock(SecurityContextRepository.class);
SecurityContextRepository delegate2 = mock(SecurityContextRepository.class);
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(this.request, this.response);
SecurityContext securityContext1 = mock(SecurityContext.class);
SecurityContext securityContext2 = mock(SecurityContext.class);
given(delegate1.loadContext(holder)).willReturn(securityContext1);
given(delegate1.containsContext(holder.getRequest())).willReturn(true);
given(delegate2.loadContext(holder)).willReturn(securityContext2);
given(delegate2.containsContext(holder.getRequest())).willReturn(true);
DelegatingSecurityContextRepository repository = new DelegatingSecurityContextRepository(delegate1, delegate2);
SecurityContext returnedSecurityContext = repository.loadContext(holder);
assertThat(returnedSecurityContext).isSameAs(securityContext2);
}
// gh-12314
@Test
public void loadContextWhenFirstDelegateReturnsThenContextFromFirstDelegate() {
SecurityContextRepository delegate1 = mock(SecurityContextRepository.class);
SecurityContextRepository delegate2 = mock(SecurityContextRepository.class);
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(this.request, this.response);
SecurityContext securityContext1 = mock(SecurityContext.class);
SecurityContext securityContext2 = mock(SecurityContext.class);
given(delegate1.loadContext(holder)).willReturn(securityContext1);
given(delegate1.containsContext(holder.getRequest())).willReturn(true);
given(delegate2.loadContext(holder)).willReturn(securityContext2);
given(delegate2.containsContext(holder.getRequest())).willReturn(false);
DelegatingSecurityContextRepository repository = new DelegatingSecurityContextRepository(delegate1, delegate2);
SecurityContext returnedSecurityContext = repository.loadContext(holder);
assertThat(returnedSecurityContext).isSameAs(securityContext1);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -52,6 +52,7 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.context.TransientSecurityContext;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
@@ -748,6 +749,53 @@ public class HttpSessionSecurityContextRepositoryTests {
assertThat(session).isNull();
}
@Test
public void saveContextWhenSecurityContextEmptyThenRemoveAttributeFromSession() {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
MockHttpSession session = (MockHttpSession) request.getSession(true);
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, emptyContext);
repo.saveContext(emptyContext, request, response);
Object attributeAfterSave = session
.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
assertThat(attributeAfterSave).isNull();
}
@Test
public void saveContextWhenSecurityContextEmptyAndNoSessionThenDoesNotCreateSession() {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
repo.saveContext(emptyContext, request, response);
assertThat(request.getSession(false)).isNull();
}
@Test
public void saveContextWhenSecurityContextThenSaveInSession() {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
SecurityContext context = createSecurityContext(PasswordEncodedUser.user());
repo.saveContext(context, request, response);
Object savedContext = request.getSession()
.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
assertThat(savedContext).isEqualTo(context);
}
@Test
public void saveContextWhenTransientAuthenticationThenDoNotSave() {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(new SomeTransientAuthentication());
repo.saveContext(context, request, response);
assertThat(request.getSession(false)).isNull();
}
private SecurityContext createSecurityContext(UserDetails userDetails) {
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails,
userDetails.getPassword(), userDetails.getAuthorities());
@@ -16,15 +16,22 @@
package org.springframework.security.web.server;
import java.util.ArrayList;
import java.util.List;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@@ -64,4 +71,147 @@ public class ObservationWebFilterChainDecoratorTests {
verifyNoInteractions(handler);
}
// gh-12849
@Test
void decorateWhenCustomAfterFilterThenObserves() {
AccumulatingObservationHandler handler = new AccumulatingObservationHandler();
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
ObservationWebFilterChainDecorator decorator = new ObservationWebFilterChainDecorator(registry);
WebFilter mock = mock(WebFilter.class);
given(mock.filter(any(), any())).willReturn(Mono.empty());
WebFilterChain chain = mock(WebFilterChain.class);
given(chain.filter(any())).willReturn(Mono.empty());
WebFilterChain decorated = decorator.decorate(chain,
List.of((e, c) -> c.filter(e).then(Mono.deferContextual((context) -> {
Observation parentObservation = context.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
Observation observation = Observation.createNotStarted("custom", registry)
.parentObservation(parentObservation).contextualName("custom").start();
return Mono.just("3").doOnSuccess((v) -> observation.stop()).doOnCancel(observation::stop)
.doOnError((t) -> {
observation.error(t);
observation.stop();
}).then(Mono.empty());
}))));
Observation http = Observation.start("http", registry).contextualName("http");
try {
decorated.filter(MockServerWebExchange.from(MockServerHttpRequest.get("/").build()))
.contextWrite((context) -> context.put(ObservationThreadLocalAccessor.KEY, http)).block();
}
finally {
http.stop();
}
handler.assertSpanStart(0, "http", null);
handler.assertSpanStart(1, "spring.security.filterchains", "http");
handler.assertSpanStop(2, "security filterchain before");
handler.assertSpanStart(3, "secured request", "security filterchain before");
handler.assertSpanStop(4, "secured request");
handler.assertSpanStart(5, "spring.security.filterchains", "http");
handler.assertSpanStart(6, "custom", "spring.security.filterchains");
handler.assertSpanStop(7, "custom");
handler.assertSpanStop(8, "security filterchain after");
handler.assertSpanStop(9, "http");
}
static class AccumulatingObservationHandler implements ObservationHandler<Observation.Context> {
List<Event> contexts = new ArrayList<>();
@Override
public boolean supportsContext(Observation.Context context) {
return true;
}
@Override
public void onStart(Observation.Context context) {
this.contexts.add(new Event("start", context));
}
@Override
public void onError(Observation.Context context) {
this.contexts.add(new Event("error", context));
}
@Override
public void onEvent(Observation.Event event, Observation.Context context) {
this.contexts.add(new Event("event", context));
}
@Override
public void onScopeOpened(Observation.Context context) {
this.contexts.add(new Event("opened", context));
}
@Override
public void onScopeClosed(Observation.Context context) {
this.contexts.add(new Event("closed", context));
}
@Override
public void onScopeReset(Observation.Context context) {
this.contexts.add(new Event("reset", context));
}
@Override
public void onStop(Observation.Context context) {
this.contexts.add(new Event("stop", context));
}
private void assertSpanStart(int index, String name, String parentName) {
Event event = this.contexts.get(index);
assertThat(event.event).isEqualTo("start");
if (event.contextualName == null) {
assertThat(event.name).isEqualTo(name);
}
else {
assertThat(event.contextualName).isEqualTo(name);
}
if (parentName == null) {
return;
}
if (event.parentContextualName == null) {
assertThat(event.parentName).isEqualTo(parentName);
}
else {
assertThat(event.parentContextualName).isEqualTo(parentName);
}
}
private void assertSpanStop(int index, String name) {
Event event = this.contexts.get(index);
assertThat(event.event).isEqualTo("stop");
if (event.contextualName == null) {
assertThat(event.name).isEqualTo(name);
}
else {
assertThat(event.contextualName).isEqualTo(name);
}
}
static class Event {
String event;
String name;
String contextualName;
String parentName;
String parentContextualName;
Event(String event, Observation.Context context) {
this.event = event;
this.name = context.getName();
this.contextualName = context.getContextualName();
if (context.getParentObservation() != null) {
this.parentName = context.getParentObservation().getContextView().getName();
this.parentContextualName = context.getParentObservation().getContextView().getContextualName();
}
}
}
}
}