1
0
mirror of synced 2026-07-08 20:30:04 +00:00

Compare commits

...

140 Commits

Author SHA1 Message Date
github-actions[bot] e75b210015 Release 5.8.3 2023-04-17 15:21:54 +00: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 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 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 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 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 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 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
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
Marcus Da Coregio d5603a944d Avoid exception if PBKDF2WithHmacSHA256 is not available
Issue gh-12873
2023-04-04 09:33:12 -03:00
Brummolix a513fc0f38 Fix SecurityWebApplicationInitializer.getSecurityDispatcherTypes example in doc #12939 2023-03-30 09:38:17 -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 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 6bda1d2bf3 Document WebExpressionAuthorizationManager
Closes gh-12928
2023-03-27 14:38:09 -06: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 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
Bishakh Ghosh c75ee25a6d Fix documentation code block bug
Fixes #12850
2023-03-22 09:21:09 -03:00
Marcus Da Coregio 8d664bc4c2 DelegatingSecurityContextRepository should call loadContext
Closes gh-12314
2023-03-22 08:53:19 -03: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 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
twosom abd51f7b63 Polished DefaultLoginPageGeneratingFilterTests Validation
Closes gh-12694
2023-03-20 15:31:59 -06:00
el-hopaness-romtic 4154ed543a Fix .access(...) parameter 2023-03-20 15:23:40 -06: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 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 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 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
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 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 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 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
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
Dayan Kodippily 79887fa213 Closes gh-12472 2023-02-22 10:21:46 -07:00
Josh Cummings 35cf52d3bd Add DefaultMethodSecurityExpressionHandler
Closes gh-12356
2023-02-21 16:58:08 -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 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
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
Josh Cummings a93cc3f0a0 Merge branch '5.7.x' into 5.8.x
Closes gh-12739
2023-02-17 15:54:47 -07:00
Josh Cummings 2fcf48a11b Update org.springframework.data to 2021.2.8
Closes gh-12738
2023-02-17 15:53:49 -07:00
Josh Cummings 0398d86f39 Update org.springframework to 5.3.25
Closes gh-12737
2023-02-17 15:53:40 -07:00
Josh Cummings c9112dfaab Update hibernate-entitymanager to 5.6.15.Final
Closes gh-12736
2023-02-17 15:53:32 -07:00
Josh Cummings 4156766fe0 Update org.aspectj to 1.9.19
Closes gh-12735
2023-02-17 15:53:24 -07:00
Josh Cummings f125785328 Update io.spring.nohttp to 0.0.11
Closes gh-12734
2023-02-17 15:53:16 -07:00
Josh Cummings 4643c0d999 Update blockhound to 1.0.7.RELEASE
Closes gh-12733
2023-02-17 15:53:08 -07:00
Josh Cummings d2d82b5e94 Update io.projectreactor to 2020.0.28
Closes gh-12732
2023-02-17 15:52:54 -07:00
Josh Cummings d4d03ea146 Update jackson-bom to 2.13.5
Closes gh-12731
2023-02-17 15:52:32 -07:00
Steve Riesenberg 6ba88e6545 Update org.springframework.data to 2021.2.8
Closes gh-12726
2023-02-17 11:58:43 -06:00
Steve Riesenberg a3855ea092 Update org.springframework to 5.3.25
Closes gh-12725
2023-02-17 11:58:39 -06:00
Steve Riesenberg f57b433fb1 Update junit-bom to 5.9.2
Closes gh-12723
2023-02-17 11:58:32 -06:00
Steve Riesenberg 30566eba9e Update hibernate-entitymanager to 5.6.15.Final
Closes gh-12722
2023-02-17 11:58:28 -06:00
Steve Riesenberg c373f6afd9 Update org.aspectj to 1.9.19
Closes gh-12721
2023-02-17 11:58:24 -06:00
Steve Riesenberg d245a54951 Update io.spring.nohttp to 0.0.11
Closes gh-12720
2023-02-17 11:58:21 -06:00
Steve Riesenberg 5f599d3d77 Update blockhound to 1.0.7.RELEASE
Closes gh-12719
2023-02-17 11:58:17 -06:00
Steve Riesenberg f49a933490 Update io.projectreactor to 2020.0.28
Closes gh-12717
2023-02-17 11:58:11 -06:00
Steve Riesenberg 8961264ec5 Update jackson-bom to 2.13.5
Closes gh-12714
2023-02-17 11:58:00 -06:00
Steve Riesenberg aaa6983fda Exclude mockk 1.13.4 update
Issue gh-12695
2023-02-17 11:55:57 -06:00
Josh Cummings a1b282ff03 Merge branch '5.7.x' into 5.8.x
Closes gh-12693
2023-02-17 10:09:32 -07:00
Josh Cummings 2db4430dcd Preserve OpenSamlAssertingPartyDetails Instance
Closes gh-12667
2023-02-17 10:02:17 -07:00
Josh Cummings 0baf650f38 Merge branch '5.7.x' into 5.8.x
Closes gh-12686
2023-02-16 14:55:22 -07:00
Leonid Rozenblyum 000b4bc495 Fix NPE in HttpSecurity#addFilterBefore, HttpSecurity#addFilterAfter
Before the fix, these methods would throw a NPE in case when the filter class passed as the second parameter, is not registered yet.

In particular, this exception can occur when mixing standard and custom DSL to register filters.

The fix doesn't change the situation that standard DSL for registration of filters cannot refer to filters that are registered via custom DSL even though those calls were done earlier.

It just provides more user-friendly error handling for this and most likely other scenarios of calls of HttpSecurity#addFilterBefore, HttpSecurity#addFilterAfter.

The error handling is implemented similarly to HttpSecurity#addFilter.

Closes gh-12637
2023-02-16 14:54:44 -07:00
Marcus Da Coregio 82c86b822f Polish session-management.adoc
Remove unresolved anchor

Issue gh-12519
2023-02-16 10:57:02 -03:00
Marcus Da Coregio 4f3faa78f7 Revisit Session Management docs
Closes gh-12519
2023-02-16 10:39:59 -03:00
Steve Riesenberg ca1961d35e Link to the latest 6.0.x release
Issue gh-12675
2023-02-15 17:01:28 -06:00
Steve Riesenberg 821db0a1ea Polish migration doc
Issue gh-12675
2023-02-15 17:00:49 -06:00
Steve Riesenberg 45b81b194b Expand migration docs regarding CSRF
Closes gh-12462
2023-02-15 14:53:28 -06:00
Josh Cummings 0d4c619648 Include continue in query string
Closes gh-12665
2023-02-14 08:00:19 -07:00
Josh Cummings 4a8961bc80 Merge branch '5.7.x' into 5.8.x 2023-02-01 15:24:48 -07:00
jongwooo aad3e61f4d chore: Use cache in continuous-integration-workflow.yml
Signed-off-by: jongwooo <jongwooo.han@gmail.com>
2023-02-01 14:26:25 -07:00
Rob Winch eaa9692c0c Merge branch '5.7.x' into 5.8.x 2023-01-30 16:05:56 -06:00
Dan Allen 51d5fb9c03 upgrade the Gradle Antora Plugin to 1.0.0 2023-01-30 16:04:40 -06:00
Steve Riesenberg 33e72b35f9 Add section for migrating WebSocket support
Issue gh-12378
2023-01-23 16:00:36 -06:00
Steve Riesenberg c306df9b46 Add XorCsrfChannelInterceptor
Issue gh-12378
2023-01-23 16:00:35 -06:00
Rob Winch d42405de42 Merge branch '5.7.x' into 5.8.x 2023-01-17 17:06:03 -06:00
Rob Winch 7db357d36a Use io.spring.antora.generate-antora-yml 2023-01-17 17:05:55 -06:00
Rob Winch 5beabbe357 Merge branch '5.7.x' into 5.8.x
Closes gh-12553
2023-01-17 15:03:14 -06:00
Dan Allen f5bc6ce665 fix unclosed block in docs 2023-01-17 15:02:30 -06:00
Marcus Da Coregio 108b03da3a Merge branch '5.7.x' into 5.8.x 2023-01-17 14:43:17 -03:00
Dan Allen 4cbb057b97 enable fetch option in Antora unless Gradle is running in offline mode 2023-01-17 14:42:54 -03:00
Marcus Da Coregio 8758a00e90 Merge branch '5.7.x' into 5.8.x 2023-01-17 09:46:56 -03:00
Marcus Da Coregio c0f7cecc6d Merge branch '5.6.x' into 5.7.x 2023-01-17 09:46:41 -03:00
Dan Allen 22ffa833ca upgrade Antora plugin and configure playbook provider to support local build 2023-01-17 09:46:24 -03:00
Josh Cummings 090c5f96ce Merge branch '5.7.x' into 5.8.x
Closes gh-12526
2023-01-11 12:47:55 -07:00
Josh Cummings f41b77a4db Fix Diagram to Say SecurityContextHolderFilter
Closes gh-11800
2023-01-11 12:47:07 -07:00
Josh Cummings 6f43104eb3 Merge branch '5.7.x' into 5.8.x
Closes gh-12516
2023-01-10 10:42:45 -07:00
Josh Cummings 2028507bf8 Fix Typo in Sample
Closes gh-11095
2023-01-10 10:38:28 -07:00
Marcus Da Coregio ae46032ced Merge branch '5.7.x' into 5.8.x
Closes gh-12510
2023-01-10 09:39:40 -03:00
Marcus Da Coregio ffdb397830 Save the SecurityContext when switching user
Closes gh-12504
2023-01-10 09:27:56 -03:00
Josh Cummings 88a8ef647b Add Details about @Configuration
Closes gh-12486
2023-01-06 13:56:56 -07:00
Josh Cummings 5e1db6a771 Merge branch '5.7.x' into 5.8.x
Closes gh-12494
2023-01-06 12:55:43 -07:00
Jon Kjennbakken 225dc593a8 Polish NimbusJwtDecoderTests
- Add missing mock

Closes gh-12238
2023-01-06 12:53:36 -07:00
Josh Cummings fda0e9a2b6 Merge branch '5.7.x' into 5.8.x
Closes gh-12458
2022-12-23 15:54:37 -07:00
Josh Cummings 3cfaf0d11d Avoid LinkedMultiValueMap in Serializable Object
Closes gh-11785
2022-12-23 15:54:00 -07:00
Marcus Da Coregio 892bbcfe0f Add EnableWebFluxSecurity migration step
Closes gh-12434
2022-12-21 10:24:25 -03:00
Marcus Da Coregio 5406fed5dc Merge branch '5.7.x' into 5.8.x 2022-12-19 16:53:05 -03:00
Eleftheria Stein-Kousathana fbfa13bd47 Fix OAuth 2.0 testing docs 2022-12-19 16:52:25 -03:00
Marcus Da Coregio 992bda32f9 Merge branch '5.7.x' into 5.8.x 2022-12-19 15:04:17 -03:00
Marcus Da Coregio 0c6b427132 Merge branch '5.6.x' into 5.7.x 2022-12-19 15:03:50 -03:00
github-actions[bot] 0f7d17bae6 Next development version 2022-12-19 15:59:53 +00:00
github-actions[bot] 44d5cb0a63 Next development version 2022-12-19 15:59:53 +00:00
github-actions[bot] c11248b589 Next development version 2022-12-19 15:57:16 +00:00
github-actions[bot] 972075677f Release 5.6.10 2022-12-19 15:23:12 +00:00
github-actions[bot] 314ae69b70 Release 5.8.1 2022-12-19 15:22:42 +00:00
github-actions[bot] 11614edcfe Release 5.7.6 2022-12-19 15:22:32 +00:00
106 changed files with 3740 additions and 563 deletions
@@ -55,6 +55,7 @@ jobs:
with:
java-version: '11'
distribution: 'adopt'
cache: 'gradle'
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
- name: Set up gradle user name
+1 -1
View File
@@ -57,7 +57,7 @@ See also the https://github.com/spring-projects/spring-framework/wiki/Gradle-bui
== 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://help.github.com/articles/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.
+7 -1
View File
@@ -3,7 +3,7 @@ import io.spring.gradle.IncludeRepoTask
buildscript {
dependencies {
classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion"
classpath 'io.spring.nohttp:nohttp-gradle:0.0.10'
classpath 'io.spring.nohttp:nohttp-gradle:0.0.11'
classpath "io.freefair.gradle:aspectj-plugin:6.5.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.netflix.nebula:nebula-project-plugin:8.2.0"
@@ -119,6 +119,12 @@ updateDependenciesSettings {
selection.reject("nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency");
}
}
components.withModule("io.mockk:mockk") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("mockk updates break tests");
}
}
components.all { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
// Do not compare version due to multiple versions existing
+2 -2
View File
@@ -93,13 +93,13 @@ dependencies {
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
implementation 'com.github.spullara.mustache.java:compiler:0.9.4'
implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
implementation 'io.spring.nohttp:nohttp-gradle:0.0.10'
implementation 'io.spring.nohttp:nohttp-gradle:0.0.11'
implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0'
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.29.0'
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation platform('org.junit:junit-bom:5.9.2')
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.junit.jupiter:junit-jupiter-params"
testImplementation "org.junit.jupiter:junit-jupiter-engine"
@@ -36,7 +36,7 @@ class CheckstylePlugin implements Plugin<Project> {
if (checkstyleDir.exists() && checkstyleDir.directory) {
project.getPluginManager().apply('checkstyle')
project.dependencies.add('checkstyle', 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.15')
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.10')
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.11')
project.checkstyle {
configDirectory = checkstyleDir
@@ -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
}
}
@@ -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.
@@ -3270,7 +3270,12 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
}
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
int order = this.filterOrders.getOrder(registeredFilter) + offset;
Integer registeredFilterOrder = this.filterOrders.getOrder(registeredFilter);
if (registeredFilterOrder == null) {
throw new IllegalArgumentException(
"The Filter class " + registeredFilter.getName() + " does not have a registered order");
}
int order = registeredFilterOrder + offset;
this.filters.add(new OrderedFilter(filter, order));
this.filterOrders.put(filter.getClass(), order);
return 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.
@@ -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;
@@ -303,7 +303,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.
@@ -56,6 +56,8 @@ final class WebSocketMessageBrokerSecurityConfiguration
private static final String SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME = "stompWebSocketHandlerMapping";
private static final String CSRF_CHANNEL_INTERCEPTOR_BEAN_NAME = "csrfChannelInterceptor";
private MessageMatcherDelegatingAuthorizationManager b;
private static final AuthorizationManager<Message<?>> ANY_MESSAGE_AUTHENTICATED = MessageMatcherDelegatingAuthorizationManager
@@ -66,7 +68,7 @@ final class WebSocketMessageBrokerSecurityConfiguration
private final SecurityContextChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor();
private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
private ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor(
ANY_MESSAGE_AUTHENTICATED);
@@ -86,6 +88,12 @@ final class WebSocketMessageBrokerSecurityConfiguration
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
ChannelInterceptor csrfChannelInterceptor = getBeanOrNull(CSRF_CHANNEL_INTERCEPTOR_BEAN_NAME,
ChannelInterceptor.class);
if (csrfChannelInterceptor != null) {
this.csrfChannelInterceptor = csrfChannelInterceptor;
}
this.authorizationChannelInterceptor
.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context));
this.authorizationChannelInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
@@ -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) {
@@ -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.
@@ -30,6 +30,7 @@ import org.assertj.core.api.ListAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@@ -45,12 +46,29 @@ import org.springframework.security.web.context.request.async.WebAsyncManagerInt
import org.springframework.security.web.header.HeaderWriterFilter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ExtendWith(SpringTestContextExtension.class)
public class HttpSecurityAddFilterTest {
public class HttpSecurityAddFilterTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Test
public void addFilterAfterFilterNotRegisteredYetThenThrowIllegalArgument() {
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(
() -> this.spring.register(MyOtherFilterAfterMyFilterNotRegisteredYetConfig.class).autowire())
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
}
@Test
public void addFilterBeforeFilterNotRegisteredYetThenThrowIllegalArgument() {
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(
() -> this.spring.register(MyOtherFilterBeforeMyFilterNotRegisteredYetConfig.class).autowire())
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
}
@Test
public void addFilterAfterWhenSameFilterDifferentPlacesThenOrderCorrect() {
this.spring.register(MyFilterMultipleAfterConfig.class).autowire();
@@ -209,6 +227,34 @@ public class HttpSecurityAddFilterTest {
}
@EnableWebSecurity
static class MyOtherFilterAfterMyFilterNotRegisteredYetConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterAfter(new MyOtherFilter(), MyFilter.class);
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
static class MyOtherFilterBeforeMyFilterNotRegisteredYetConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
// @formatter:on
return http.build();
}
}
@EnableWebSecurity
static class MyOtherFilterRelativeToMyFilterBeforeConfig {
@@ -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,9 @@
package org.springframework.security.config.annotation.web.configurers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpHeaders;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -23,7 +26,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.BeanCreationException;
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.authentication.builders.AuthenticationManagerBuilder;
@@ -33,9 +39,12 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
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;
@@ -45,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;
@@ -321,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
}
}
@EnableWebSecurity
static class NullLogoutSuccessHandlerConfig extends WebSecurityConfigurerAdapter {
@@ -52,7 +52,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-2019 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.
@@ -61,6 +61,7 @@ import org.springframework.security.messaging.context.SecurityContextChannelInte
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.stereotype.Controller;
import org.springframework.test.util.ReflectionTestUtils;
@@ -79,6 +80,7 @@ import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSo
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
@@ -284,7 +286,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
private void assertHandshake(HttpServletRequest request) {
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
}
@@ -306,7 +308,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
request.setAttribute(CsrfToken.class.getName(), this.token);
request.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token));
return request;
}
@@ -0,0 +1,43 @@
/*
* 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.web.socket;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DeferredCsrfToken;
/**
* @author Steve Riesenberg
*/
final class TestDeferredCsrfToken implements DeferredCsrfToken {
private final CsrfToken csrfToken;
TestDeferredCsrfToken(CsrfToken csrfToken) {
this.csrfToken = csrfToken;
}
@Override
public CsrfToken get() {
return this.csrfToken;
}
@Override
public boolean isGenerated() {
return false;
}
}
@@ -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.
@@ -70,6 +70,7 @@ import org.springframework.security.messaging.context.SecurityContextChannelInte
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.stereotype.Controller;
import org.springframework.test.util.ReflectionTestUtils;
@@ -92,6 +93,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
public class WebSocketMessageBrokerSecurityConfigurationTests {
@@ -367,7 +369,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
private void assertHandshake(HttpServletRequest request) {
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
}
@@ -389,7 +391,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
request.setAttribute(CsrfToken.class.getName(), this.token);
request.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token));
return request;
}
@@ -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.
@@ -61,6 +61,7 @@ import org.springframework.security.test.context.annotation.SecurityTestExecutio
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -77,6 +78,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
/**
@@ -381,12 +383,14 @@ public class WebSocketMessageBrokerConfigTests {
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
String csrfAttributeName = CsrfToken.class.getName();
String customAttributeName = this.getClass().getName();
MvcResult result = mvc.perform(get("/app").requestAttr(csrfAttributeName, this.token)
.sessionAttr(customAttributeName, "attributeValue")).andReturn();
MvcResult result = mvc.perform(
get("/app").requestAttr(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token))
.sessionAttr(customAttributeName, "attributeValue"))
.andReturn();
CsrfToken handshakeToken = (CsrfToken) this.testHandshakeHandler.attributes.get(csrfAttributeName);
String handshakeValue = (String) this.testHandshakeHandler.attributes.get(customAttributeName);
String sessionValue = (String) result.getRequest().getSession().getAttribute(customAttributeName);
assertThat(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
assertThatCsrfToken(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
assertThat(handshakeValue).isEqualTo(sessionValue)
.withFailMessage("Explicitly listed session variables are not overridden");
}
@@ -398,12 +402,13 @@ public class WebSocketMessageBrokerConfigTests {
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
String csrfAttributeName = CsrfToken.class.getName();
String customAttributeName = this.getClass().getName();
MvcResult result = mvc.perform(get("/app/289/tpyx6mde/websocket").requestAttr(csrfAttributeName, this.token)
MvcResult result = mvc.perform(get("/app/289/tpyx6mde/websocket")
.requestAttr(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token))
.sessionAttr(customAttributeName, "attributeValue")).andReturn();
CsrfToken handshakeToken = (CsrfToken) this.testHandshakeHandler.attributes.get(csrfAttributeName);
String handshakeValue = (String) this.testHandshakeHandler.attributes.get(customAttributeName);
String sessionValue = (String) result.getRequest().getSession().getAttribute(customAttributeName);
assertThat(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
assertThatCsrfToken(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
assertThat(handshakeValue).isEqualTo(sessionValue)
.withFailMessage("Explicitly listed session variables are not overridden");
}
@@ -526,6 +531,26 @@ public class WebSocketMessageBrokerConfigTests {
return SecurityContextHolder.getContextHolderStrategy();
}
private static final class TestDeferredCsrfToken implements DeferredCsrfToken {
private final CsrfToken csrfToken;
TestDeferredCsrfToken(CsrfToken csrfToken) {
this.csrfToken = csrfToken;
}
@Override
public CsrfToken get() {
return this.csrfToken;
}
@Override
public boolean isGenerated() {
return false;
}
}
@Controller
static class MessageController {
@@ -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;
@@ -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))
+1
View File
@@ -11,5 +11,6 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-engine"
testImplementation "org.mockito:mockito-core"
testImplementation "org.mockito:mockito-junit-jupiter"
testImplementation "org.mockito:mockito-inline"
testImplementation "org.springframework:spring-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.
@@ -16,8 +16,13 @@
package org.springframework.security.crypto.factory;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -34,6 +39,8 @@ import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
*/
public final class PasswordEncoderFactories {
private static final Log logger = LogFactory.getLog(PasswordEncoderFactories.class);
private PasswordEncoderFactories() {
}
@@ -78,7 +85,8 @@ public final class PasswordEncoderFactories {
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
putIfAlgorithmSupported("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder::defaultsForSpringSecurity_v5_8,
encoders);
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
@@ -90,4 +98,22 @@ public final class PasswordEncoderFactories {
return new DelegatingPasswordEncoder(encodingId, encoders);
}
private static void putIfAlgorithmSupported(String encodingId, Supplier<PasswordEncoder> encoderSupplier,
Map<String, PasswordEncoder> encoders) {
try {
PasswordEncoder passwordEncoder = encoderSupplier.get();
encoders.put(encodingId, passwordEncoder);
}
catch (Exception ex) {
if (ex.getCause() instanceof NoSuchAlgorithmException) {
logger.warn(String.format(
"Cannot create PasswordEncoder with encodingId [%s] because the algorithm is not available",
encodingId), ex);
}
else {
throw ex;
}
}
}
}
@@ -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,11 +16,17 @@
package org.springframework.security.crypto.factory;
import java.security.NoSuchAlgorithmException;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.Mockito.mockStatic;
/**
* @author Rob Winch
@@ -123,4 +129,14 @@ public class PasswordEncoderFactoriesTests {
assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
}
@Test
void constructWhenAlgorithmNotAvailableThenSkip() {
try (MockedStatic<Pbkdf2PasswordEncoder> pbkdf2PasswordEncoderMock = mockStatic(Pbkdf2PasswordEncoder.class)) {
pbkdf2PasswordEncoderMock.when(Pbkdf2PasswordEncoder::defaultsForSpringSecurity_v5_8)
.thenThrow(new IllegalArgumentException(new NoSuchAlgorithmException()));
assertThatNoException().isThrownBy(PasswordEncoderFactories::createDelegatingPasswordEncoder);
}
}
}
+9 -9
View File
@@ -8,16 +8,16 @@ javaPlatform {
dependencies {
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
api platform("io.projectreactor:reactor-bom:2020.0.26")
api platform("io.projectreactor:reactor-bom:2020.0.31")
api platform("io.rsocket:rsocket-bom:1.1.3")
api platform("org.junit:junit-bom:5.9.1")
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:2021.2.6")
api platform("org.springframework.data:spring-data-bom:2021.2.11")
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.13.4.20221013")
api platform("com.fasterxml.jackson:jackson-bom:2.13.5")
constraints {
api "ch.qos.logback:logback-classic:1.2.11"
api "ch.qos.logback:logback-classic:1.2.12"
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"
@@ -26,7 +26,7 @@ dependencies {
api "com.unboundid:unboundid-ldapsdk:4.0.14"
api "commons-collections:commons-collections:3.2.2"
api "io.mockk:mockk:1.13.3"
api "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
api "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
api "jakarta.inject:jakarta.inject-api:1.0.5"
api "jakarta.annotation:jakarta.annotation-api:1.3.5"
api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7"
@@ -50,11 +50,11 @@ dependencies {
api "org.assertj:assertj-core:3.23.1"
api "org.bouncycastle:bcpkix-jdk15on:1.70"
api "org.bouncycastle:bcprov-jdk15on:1.70"
api "org.eclipse.jetty:jetty-server:9.4.50.v20221201"
api "org.eclipse.jetty:jetty-servlet:9.4.50.v20221201"
api "org.eclipse.jetty:jetty-server:9.4.51.v20230217"
api "org.eclipse.jetty:jetty-servlet:9.4.51.v20230217"
api "org.eclipse.persistence:javax.persistence:2.2.1"
api "org.hamcrest:hamcrest:2.2"
api "org.hibernate:hibernate-entitymanager:5.6.14.Final"
api "org.hibernate:hibernate-entitymanager:5.6.15.Final"
api "org.hsqldb:hsqldb:2.7.1"
api "org.jasig.cas.client:cas-client-core:3.6.4"
api "org.openid4java:openid4java-nodeps:0.9.6"
+1 -2
View File
@@ -1,2 +1 @@
/package-lock.json
/node_modules/
/*-antora-playbook.yml
+8 -2
View File
@@ -6,7 +6,13 @@ nav:
ext:
collector:
run:
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntora
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntoraYml
local: true
scan:
dir: ./build/generateAntora
dir: ./build/generated-antora-resources
asciidoc:
attributes:
icondir: icons
gh-old-samples-url: 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
gh-samples-url: "https://github.com/spring-projects/spring-security-samples/tree/{gh-tag}"
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
-23
View File
@@ -1,23 +0,0 @@
# The purpose of this Antora playbook is to generate a preview of the docs in the current branch.
antora:
extensions:
- '@antora/collector-extension'
site:
title: Spring Security
url: https://docs.spring.io/spring-security/reference
content:
sources:
- url: ./..
branches: HEAD
start_path: docs
worktrees: true
asciidoc:
attributes:
page-pagination: ''
hide-uri-scheme: '@'
urls:
latest_version_segment: ''
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 89 KiB

+2 -3
View File
@@ -4,12 +4,11 @@
The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0.
Use 5.8 and the steps below to minimize changes when
ifdef::spring-security-version[]
xref:6.0.0@migration/index.adoc[updating to 6.0]
xref:6.0.2@migration/index.adoc[updating to 6.0].
endif::[]
ifndef::spring-security-version[]
updating to 6.0
updating to 6.0.
endif::[]
.
== Update to Spring Security 5.8
@@ -400,6 +400,58 @@ http {
To opt-out of the 6.0 defaults and instead continue to pass `AuthenticationServiceException` on to ``ServerAuthenticationEntryPoint``s, you can follow the same steps as above, except set `rethrowAuthenticationServiceException` to false.
[[add-configuration-annotation]]
== Add `@Configuration` annotation
In 6.0, `@Configuration` is removed from `@EnableWebFluxSecurity` and `@EnableReactiveMethodSecurity`.
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
For example, `@EnableReactiveMethodSecurity` changes from:
====
.Java
[source,java,role="primary"]
----
@EnableReactiveMethodSecurity
public class MyConfiguration {
// ...
}
----
.Kotlin
[source,java,role="primary"]
----
@EnableReactiveMethodSecurity
open class MyConfiguration {
// ...
}
----
====
to:
====
.Java
[source,java,role="primary"]
----
@Configuration
@EnableReactiveMethodSecurity
public class MyConfiguration {
// ...
}
----
.Kotlin
[source,java,role="primary"]
----
@Configuration
@EnableReactiveMethodSecurity
open class MyConfiguration {
// ...
}
----
====
== Address OAuth2 Client Deprecations
=== `ServerOAuth2AuthorizedClientExchangeFilterFunction`
@@ -424,3 +476,47 @@ The method `setAllowMultipleAuthorizationRequests(...)` has no direct replacemen
=== `UnAuthenticatedServerOAuth2AuthorizedClientRepository`
The class `UnAuthenticatedServerOAuth2AuthorizedClientRepository` has no direct replacement. Usage of the class can be replaced with `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`.
== Add `@Configuration` to `@Enable*` annotations
In 6.0, all Spring Security's `@Enable*` annotations had their `@Configuration` removed.
While convenient, it was not consistent with the rest of the Spring projects and most notably Spring Framework's `@Enable*` annotations.
Additionally, the introduction of support for `@Configuration(proxyBeanMethods=false)` in Spring Framework provides another reason to remove `@Configuration` meta-annotation from Spring Security's `@Enable*` annotations and allow users to opt into their preferred configuration mode.
The following annotations had their `@Configuration` removed:
- `@EnableGlobalAuthentication`
- `@EnableGlobalMethodSecurity`
- `@EnableMethodSecurity`
- `@EnableReactiveMethodSecurity`
- `@EnableWebSecurity`
- `@EnableWebFluxSecurity`
For example, if you are using `@EnableWebFluxSecurity`, you will need to change:
====
.Java
[source,java,role="primary"]
----
@EnableWebFluxSecurity
public class SecurityConfig {
// ...
}
----
====
to:
====
.Java
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
// ...
}
----
====
And the same applies to every other annotation listed above.
@@ -104,6 +104,111 @@ should change to:
----
====
=== Use a Custom `@Bean` instead of subclassing `DefaultMethodSecurityExpressionHandler`
As a performance optimization, a new method was introduced to `MethodSecurityExpressionHandler` that takes a `Supplier<Authentication>` instead of an `Authentication`.
This allows Spring Security to defer the lookup of the `Authentication`, and is taken advantage of automatically when you use `@EnableMethodSecurity` instead of `@EnableGlobalMethodSecurity`.
However, let's say that your code extends `DefaultMethodSecurityExpressionHandler` and overrides `createSecurityExpressionRoot(Authentication, MethodInvocation)` to return a custom `SecurityExpressionRoot` instance.
This will no longer work because the arrangement that `@EnableMethodSecurity` sets up calls `createEvaluationContext(Supplier<Authentication>, MethodInvocation)` instead.
Happily, such a level of customization is often unnecessary.
Instead, you can create a custom bean with the authorization methods that you need.
For example, let's say you are wanting a custom evaluation of `@PostAuthorize("hasAuthority('ADMIN')")`.
You can create a custom `@Bean` like this one:
====
.Java
[source,java,role="primary"]
----
class MyAuthorizer {
boolean isAdmin(MethodSecurityExpressionOperations root) {
boolean decision = root.hasAuthority("ADMIN");
// custom work ...
return decision;
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
class MyAuthorizer {
fun isAdmin(val root: MethodSecurityExpressionOperations): boolean {
val decision = root.hasAuthority("ADMIN");
// custom work ...
return decision;
}
}
----
====
and then refer to it in the annotation like so:
====
.Java
[source,java,role="primary"]
----
@PreAuthorize("@authz.isAdmin(#root)")
----
.Kotlin
[source,kotlin,role="secondary"]
----
@PreAuthorize("@authz.isAdmin(#root)")
----
====
==== I'd still prefer to subclass `DefaultMethodSecurityExpressionHandler`
If you must continue subclassing `DefaultMethodSecurityExpressionHandler`, you can still do so.
Instead, override the `createEvaluationContext(Supplier<Authentication>, MethodInvocation)` method like so:
====
.Java
[source,java,role="primary"]
----
@Component
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
@Override
public EvaluationContext createEvaluationContext(
Supplier<Authentication> authentication, MethodInvocation mi) {
StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
MySecurityExpressionRoot root = new MySecurityExpressionRoot(authentication, invocation);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(new AuthenticationTrustResolverImpl());
root.setRoleHierarchy(getRoleHierarchy());
context.setRootObject(root);
return context;
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Component
class MyExpressionHandler: DefaultMethodSecurityExpressionHandler {
override fun createEvaluationContext(val authentication: Supplier<Authentication>,
val mi: MethodInvocation): EvaluationContext {
val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext;
val root = new MySecurityExpressionRoot(authentication, invocation);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(new AuthenticationTrustResolverImpl());
root.setRoleHierarchy(getRoleHierarchy());
context.setRootObject(root);
return context;
}
}
----
====
==== Opt-out Steps
If you need to opt-out of these changes, you can use `@EnableGlobalMethodSecurity` instead of `@EnableMethodSecurity`
[[servlet-replace-permissionevaluator-bean-with-methodsecurityexpression-handler]]
=== Publish a `MethodSecurityExpressionHandler` instead of a `PermissionEvaluator`
@@ -247,12 +352,22 @@ public final class PreAuthorizeAuthorizationManagerAdapter implements Authorizat
Once you have implemented `AuthorizationManager`, please follow the details in the reference manual for xref:servlet/authorization/method-security.adoc#jc-method-security-custom-authorization-manager[adding a custom `AuthorizationManager`].
==== I use a custom `AfterInvocationManager`
==== I use `AfterInvocationManager` or `AfterInvocationProvider`
{security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`] replaces both {security-api-url}org/springframework/security/access/AccessDecisionManager.html[`AccessDecisionManager`] and {security-api-url}org/springframework/security/access/intercept/AfterInvocationManager.html[`AfterInvocationManager`].
The difference is that `AuthorizationManager<MethodInvocation>` replaces `AccessDecisionManager` and `AuthorizationManager<MethodInvocationResult>` replaces `AfterInvocationManager`.
{security-api-url}org/springframework/security/access/intercept/AfterInvocationManager.html;[`AfterInvocationManager`] and {security-api-url}org/springframework/security/access/intercept/AfterInvocationProvider.html[`AfterInvocationProvider`] make an authorization decision about an invocation's result.
For example, in the case of method invocation, these make an authorization decision about a method's return value.
Given that, <<_i_use_a_custom_accessdecisionvoter,the same rules apply for adaptation>>, where the goal this time is to implement `AuthorizationManager<MethodInvocationResult>` instead of `AuthorizationManager<MethodInvocation>` and use `AuthorizationManagerAfterMethodInterceptor` instead of `AuthorizationManagerBeforeMethodInterceptor`.
In Spring Security 3.0, authorization decision-making was standardized into the xref:servlet/authorization/method-security.adoc[`@PostAuthorize` and `@PostFilter` annotations].
`@PostAuthorize` is for deciding whether the return value as a whole was permitted to be returned.
`@PostFilter` is for filtering individual entries from a returned collection, array, or stream.
These two annotations should serve most needs, and you are encouraged to migrate to one or both of them since `AfterInvocationProvider` and `AfterInvocationManager` are now deprecated.
If you've implemented your own `AfterInvocationManager` or `AfterInvocationProvider`, you should first ask yourself what it is trying to do.
If it is trying to authorize the return type, <<_i_use_a_custom_accessdecisionvoter,consider implementing `AuthorizationManager<MethodInvocationResult>` and using `AfterMethodAuthorizationManagerInterceptor`>>. Or publishing a custom bean and using `@PostAuthorize("@myBean.authorize(#root)")`.
If it is trying to filter, then consider publishing a custom bean and using `@PostFilter("@mybean.authorize(#root)")`.
Or, if needed, you can implement your own `MethodInterceptor`, taking a look at `PostFilterAuthorizationMethodInterceptor` and `PrePostMethodSecurityConfiguration` for an example.
==== I use `RunAsManager`
@@ -1042,7 +1157,7 @@ public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplic
@Override
protected EnumSet<DispatcherType> getSecurityDispatcherTypes() {
return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR, DispatcherType.FORWARD,
return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR, DispatcherType.ASYNC,
DispatcherType.FORWARD, DispatcherType.INCLUDE);
}
@@ -2,6 +2,58 @@
The following steps relate to changes around how to configure `HttpSecurity`, `WebSecurity`, and `AuthenticationManager`.
[[add-configuration-annotation]]
== Add `@Configuration` annotation
In 6.0, `@Configuration` is removed from `@EnableWebSecurity`, `@EnableMethodSecurity`, `@EnableGlobalMethodSecurity`, and `@EnableGlobalAuthentication`.
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
For example, `@EnableMethodSecurity` changes from:
====
.Java
[source,java,role="primary"]
----
@EnableMethodSecurity
public class MyConfiguration {
// ...
}
----
.Kotlin
[source,java,role="primary"]
----
@EnableMethodSecurity
open class MyConfiguration {
// ...
}
----
====
to:
====
.Java
[source,java,role="primary"]
----
@Configuration
@EnableMethodSecurity
public class MyConfiguration {
// ...
}
----
.Kotlin
[source,java,role="primary"]
----
@Configuration
@EnableMethodSecurity
open class MyConfiguration {
// ...
}
----
====
[[use-new-requestmatchers]]
== Use the new `requestMatchers` methods
@@ -7,8 +7,20 @@ The following steps relate to changes around how to configure CSRF.
In Spring Security 5, the default behavior is that the `CsrfToken` will be loaded on every request.
This means that in a typical setup, the `HttpSession` must be read for every request even if it is unnecessary.
[NOTE]
====
Some examples of where it should be unnecessary to read the session include endpoints marked `permitAll()` such as static assets, static HTML pages, single-page applications hosted under the same domain/server, etc.
====
In Spring Security 6, the default is that the lookup of the `CsrfToken` will be deferred until it is needed.
[NOTE]
====
The `CsrfToken` is needed whenever a request is made with an HTTP verb that would change the state of the application.
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
Additionally, it is needed by any request that renders the token to the response, such as a web page with a `<form>` tag that includes a hidden `<input>` for the CSRF token.
====
To opt into the new Spring Security 6 default, the following configuration can be used.
[[servlet-opt-in-defer-loading-csrf-token]]
@@ -18,7 +30,7 @@ To opt into the new Spring Security 6 default, the following configuration can b
[source,java,role="primary"]
----
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
public SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName("_csrf");
@@ -61,7 +73,156 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
----
====
If this breaks your application, then you can explicitly opt into the 5.8 defaults using the following configuration:
[NOTE]
====
When the `CsrfToken` is deferred (the default in Spring Security 6), some applications may break due to the fact that they were designed with non-deferred CSRF tokens.
See <<servlet-defer-loading-csrf-token-opt-out,Opt-out Steps>> below for more information.
====
[[servlet-defer-loading-csrf-token-opt-out]]
=== Opt-out Steps
If configuring the `CsrfToken` to be deferred gives you trouble, take a look at these scenarios for optimal opt out behavior:
==== I am using a Single-Page Application with `CookieCsrfTokenRepository`
If you are using a single-page app (SPA) to connect to a backend protected by Spring Security along with `CookieCsrfTokenRepository.withHttpOnlyFalse()`, you may find that the CSRF token is no longer returned to your application as a cookie on the first request to the server.
In this case, you have several options for restoring the behavior your client-side application expects.
One option is to add a `Filter` that eagerly renders the `CsrfToken` to the response regardless of which request is made first, like so:
.Add a `Filter` to return a cookie on the response
====
.Java
[source,java,role="primary"]
----
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName("_csrf");
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(tokenRepository)
.csrfTokenRequestHandler(requestHandler)
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
return http.build();
}
private static final class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
val requestHandler = CsrfTokenRequestAttributeHandler()
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName("_csrf")
http {
csrf {
csrfTokenRepository = tokenRepository
csrfTokenRequestHandler = requestHandler
}
addFilterAfter<BasicAuthenticationFilter>(CsrfCookieFilter())
}
return http.build()
}
class CsrfCookieFilter : OncePerRequestFilter() {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val csrfToken = request.getAttribute(CsrfToken::class.java.name) as CsrfToken
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.token
filterChain.doFilter(request, response)
}
}
----
====
The option above does not require changes to the single-page application, but does cause the `CsrfToken` to be loaded on every request.
If you do not wish to add a `Filter` to eagerly load tokens on every request, additional options are listed below.
==== I am using a Single-Page Application with `HttpSessionCsrfTokenRepository`
If you are using sessions, your application will benefit from deferred tokens.
Instead of opting out, another option is to add a new `@RestController` with a `/csrf` endpoint, like so:
.Add a `/csrf` endpoint
====
.Java
[source,java,role="primary"]
----
@RestController
public class CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@RestController
class CsrfController {
@GetMapping("/csrf")
fun csrf(csrfToken: CsrfToken): CsrfToken {
return csrfToken
}
}
----
====
[NOTE]
====
You may consider adding `.requestMatchers("/csrf").permitAll()` if the endpoint above is required prior to authenticating with the server.
====
The `/csrf` endpoint would need to be consumed by the client-side application in order to bootstrap the application for subsequent requests.
[NOTE]
====
Instructions for calling the `/csrf` endpoint on application launch are specific to your client-side framework and therefore outside the scope of this document.
====
[NOTE]
====
While this requires changes to your single-page application, the benefit is that the CSRF token is only loaded once and the token can continue to be deferred.
This approach works particularly well with applications that use `HttpSessionCsrfTokenRepository` and do benefit from deferred tokens by allowing the `HttpSession` not to be read on every request.
====
If you simply wish to opt out of deferred tokens altogether, that option is listed next.
==== I need to opt out of deferred tokens for another reason
If deferred tokens break your application for another reason, then you can explicitly opt into the 5.8 defaults using the following configuration:
.Explicit Configure `CsrfToken` with 5.8 Defaults
====
@@ -69,7 +230,7 @@ If this breaks your application, then you can explicitly opt into the 5.8 defaul
[source,java,role="primary"]
----
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null);
@@ -86,7 +247,7 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
[source,kotlin,role="secondary"]
----
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val requestHandler = CsrfTokenRequestAttributeHandler()
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null)
@@ -115,6 +276,12 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
----
====
[NOTE]
====
By setting the `csrfRequestAttributeName` to `null`, the `CsrfToken` must first be loaded to determine what attribute name to use.
This causes the `CsrfToken` to be loaded on every request.
====
== Protect against CSRF BREACH
If the steps for <<Defer Loading CsrfToken>> work for you, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` using the following configuration:
@@ -240,6 +407,83 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
----
====
This is the RECOMMENDED way to configure Spring Security to work with a client-side application that uses cookie values, because it continues to allow the response to return a randomized value for the CSRF token in case the application returns HTML or other responses that could be vulnerable to BREACH without your knowledge.
[NOTE]
====
BREACH protection works to protect the token when it is included in a response body that can be GZIP compressed, which generally does not include headers and cookies.
====
[TIP]
====
Any token value returned by the server can be used successfully by the client-side application because the underlying (raw) CSRF token does not change.
It is not required for an AngularJS (or similar) application to refresh the CSRF token before/after every request.
====
If you simply wish to opt out of CSRF BREACH protection altogether, that option is listed next.
==== I need to opt out of CSRF BREACH protection for another reason
If CSRF BREACH protection does not work for you for another reason, you can opt out using the configuration from the <<servlet-opt-in-defer-loading-csrf-token>> section.
== CSRF BREACH with WebSocket support
If the steps for <<Protect against CSRF BREACH>> work for normal HTTP requests and you are using xref:servlet/integrations/websocket.adoc[WebSocket Security] support, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` with xref:servlet/integrations/websocket.adoc#websocket-sameorigin-csrf[Stomp headers].
.WebSocket Security BREACH Protection
====
.Java
[source,java,role="primary"]
----
@Bean
ChannelInterceptor csrfChannelInterceptor() {
return new XorCsrfChannelInterceptor();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun csrfChannelInterceptor(): ChannelInterceptor {
return XorCsrfChannelInterceptor()
}
----
.XML
[source,xml,role="secondary"]
----
<b:bean id="csrfChannelInterceptor"
class="org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor"/>
----
====
If configuring CSRF BREACH protection for WebSocket Security gives you trouble, you can configure the 5.8 default using the following configuration:
.Configure WebSocket Security with 5.8 default
====
.Java
[source,java,role="primary"]
----
@Bean
ChannelInterceptor csrfChannelInterceptor() {
return new CsrfChannelInterceptor();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun csrfChannelInterceptor(): ChannelInterceptor {
return CsrfChannelInterceptor()
}
----
.XML
[source,xml,role="secondary"]
----
<b:bean id="csrfChannelInterceptor"
class="org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor"/>
----
====
@@ -11,6 +11,11 @@ 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].
====
To opt into the new Spring Security 6 default, the following configuration can be used.
include::partial$servlet/architecture/security-context-explicit.adoc[]
@@ -7,6 +7,8 @@ For example:
.Java
[source,java,role="primary"]
----
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
@Test
public void messageWhenNotAuthenticated() throws Exception {
this.rest
@@ -67,6 +69,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
//...
@@ -7,6 +7,8 @@ For example:
.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())
@@ -18,6 +20,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 {
@@ -265,6 +265,60 @@ The code below demonstrates how to customize the `RequestCache` implementation t
include::partial$servlet/architecture/request-cache-continue.adoc[]
[[requestcache-prevent-saved-request]]
==== Prevent the Request From Being Saved
There are a number of reasons you may want to not store the user's unauthenticated request in the session.
You may want to offload that storage onto the user's browser or store it in a database.
Or you may want to shut off this feature since you always want to redirect the user to the home page instead of the page they tried to visit before login.
To do that, you can use {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[the `NullRequestCache` implementation].
.Prevent the Request From Being Saved
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
----
.XML
[source,xml,role="secondary"]
----
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
----
====
[[requestcacheawarefilter]]
=== RequestCacheAwareFilter
@@ -10,6 +10,7 @@ The default is that accessing the URL `/logout` will log the user out by:
- Invalidating the HTTP Session
- Cleaning up any RememberMe authentication that was configured
- Clearing the `SecurityContextHolder`
- Clearing the `SecurityContextRepository`
- Redirect to `/login?logout`
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
@@ -139,6 +140,7 @@ If not configured a status code 200 will be 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()]
File diff suppressed because it is too large Load Diff
@@ -413,3 +413,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`.
@@ -211,7 +211,7 @@ You could refer to the method using:
----
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@webSecurity.checkUserId(authentication,#userId)"))
...
);
----
@@ -548,7 +548,7 @@ class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor customAuthorize(AuthorizationManager<MethodInvocationResult> rules) {
AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
AnnotationMatchingPointcut pattern = new AnnotationMatchingPointcut(MySecurityAnnotation.class);
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
@@ -564,7 +564,7 @@ class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun customAuthorize(rules : AuthorizationManager<MethodInvocationResult>) : Advisor {
val pattern = AnnotationMethodMatcher(MySecurityAnnotation::class.java);
val pattern = AnnotationMatchingPointcut(MySecurityAnnotation::class.java);
val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
@@ -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.
@@ -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
@@ -33,6 +33,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`.
@@ -640,6 +641,16 @@ which in a deployed application would translate 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
@@ -712,56 +723,6 @@ resource.inputStream.use {
[TIP]
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform 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.
There are a number of reasons you may want to customize. Among them:
* You may know that you will never be a multi-tenant application and so want to have 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>``s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ``<saml2:Response>``s], 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
@@ -856,3 +817,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")
----
====
@@ -61,6 +61,7 @@ public class WithMockUserTests {
@ContextConfiguration
class WithMockUserTests {
----
====
This is a basic example of how to setup Spring Security Test. The highlights are:
@@ -296,7 +296,7 @@ fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): String? {
----
====
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2User` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2Login` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
====
.Java
+37 -69
View File
@@ -1,78 +1,25 @@
plugins {
id 'org.antora' version '1.0.0-alpha.3'
id 'org.antora' version '1.0.0'
id 'io.spring.antora.generate-antora-yml' version '0.0.1'
}
apply plugin: 'io.spring.convention.docs'
apply plugin: 'java'
antora {
version = '3.2.0-alpha.2'
playbook = file('local-antora-playbook.yml')
options = ['--clean', '--stacktrace']
environment = [
'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691',
'ALGOLIA_APP_ID': '244V8V9FGG',
'ALGOLIA_INDEX_NAME': 'security-docs'
]
dependencies = [
'@antora/collector-extension': '1.0.0-alpha.3'
]
playbook = 'cached-antora-playbook.yml'
playbookProvider {
repository = 'spring-projects/spring-security'
branch = 'docs-build'
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
checkLocalBranch = true
}
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
}
tasks.register('generateAntora') {
group = 'Documentation'
description = 'Generates the antora.yml for dynamic properties'
doLast {
def docsTag = snapshotBuild ? 'current' : project.version
def ghTag = snapshotBuild ? 'main' : project.version
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
def ghOldSamplesUrl = 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
def securityApiUrl = "$securityDocsUrl/api/"
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
def ymlVersions = resolvedVersions(project.configurations.testRuntimeClasspath).call()
.collect(v -> " ${v.getKey()}: ${v.getValue()}")
.join('\n')
def outputFile = layout.buildDirectory.file('generateAntora/antora.yml').get().asFile
mkdir(outputFile.getParentFile())
def mainVersion = project.version
def prerelease = null
def versionComponents = mainVersion.split(/(?=-)/)
if (versionComponents.length > 1) {
if (versionComponents[1] == '-SNAPSHOT') {
mainVersion = versionComponents[0]
prerelease = "'-SNAPSHOT'"
} else {
prerelease = 'true'
}
}
def antoraYmlText = file('antora.yml').text
layout.buildDirectory.file('.antora.yml').get().asFile.text = antoraYmlText
antoraYmlText = antoraYmlText.lines().collect { l ->
if (l.startsWith('version: ')) {
return prerelease == null ? "version: '${mainVersion}'" : "version: '${mainVersion}'\nprerelease: ${prerelease}"
}
if (l.startsWith('title: ')) return "title: ${project.parent.description}"
return l == 'ext:' || l.getAt(0) == ' ' ? null : l
}.findAll(Objects::nonNull).join('\n')
outputFile.text = """$antoraYmlText
asciidoc:
attributes:
icondir: icons
gh-old-samples-url: $ghOldSamplesUrl
gh-samples-url: $ghSamplesUrl
gh-url: $ghUrl
security-api-url: $securityApiUrl
security-reference-url: $securityReferenceUrl
spring-framework-api-url: $springFrameworkApiUrl
spring-framework-reference-url: $springFrameworkReferenceUrl
spring-security-version: ${project.version}
${ymlVersions}
"""
}
tasks.named("generateAntoraYml") {
asciidocAttributes = project.provider( { generateAttributes() } )
asciidocAttributes.putAll(providers.provider( { resolvedVersions(project.configurations.testRuntimeClasspath) }))
}
dependencies {
@@ -82,12 +29,33 @@ dependencies {
testImplementation 'org.springframework:spring-core'
}
def generateAttributes() {
def docsTag = snapshotBuild ? 'current' : project.version
def ghTag = snapshotBuild ? 'main' : project.version
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
def ghOldSamplesUrl = 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
def securityApiUrl = "$securityDocsUrl/api/"
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
return ['gh-old-samples-url': ghOldSamplesUrl.toString(),
'gh-samples-url': ghSamplesUrl.toString(),
'gh-url': ghUrl.toString(),
'security-api-url': securityApiUrl.toString(),
'security-reference-url': securityReferenceUrl.toString(),
'spring-framework-api-url': springFrameworkApiUrl.toString(),
'spring-framework-reference-url': springFrameworkReferenceUrl.toString(),
'spring-security-version': project.version]
+ resolvedVersions(project.configurations.testRuntimeClasspath)
}
def resolvedVersions(Configuration configuration) {
return {
configuration.resolvedConfiguration
return configuration.resolvedConfiguration
.resolvedArtifacts
.collectEntries { [(it.name + '-version'): it.moduleVersion.id.version] }
}
}
repositories {
+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/.*
+4 -4
View File
@@ -1,9 +1,9 @@
aspectjVersion=1.9.9.1
springJavaformatVersion=0.0.35
aspectjVersion=1.9.19
springJavaformatVersion=0.0.38
springBootVersion=2.4.2
springFrameworkVersion=5.3.24
springFrameworkVersion=5.3.27
openSamlVersion=3.4.6
version=5.8.1-SNAPSHOT
version=5.8.3
kotlinVersion=1.7.22
samplesBranch=5.8.x
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
@@ -0,0 +1,86 @@
/*
* 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.web.csrf;
import java.security.MessageDigest;
import java.util.Map;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
/**
* {@link ChannelInterceptor} that validates a CSRF token masked by the
* {@link org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler} in
* the header of any {@link SimpMessageType#CONNECT} message.
*
* @author Steve Riesenberg
* @since 5.8
*/
public final class XorCsrfChannelInterceptor implements ChannelInterceptor {
private final MessageMatcher<Object> matcher = new SimpMessageTypeMatcher(SimpMessageType.CONNECT);
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
if (!this.matcher.matches(message)) {
return message;
}
Map<String, Object> sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
CsrfToken expectedToken = (sessionAttributes != null)
? (CsrfToken) sessionAttributes.get(CsrfToken.class.getName()) : null;
if (expectedToken == null) {
throw new MissingCsrfTokenException(null);
}
String actualToken = SimpMessageHeaderAccessor.wrap(message)
.getFirstNativeHeader(expectedToken.getHeaderName());
String actualTokenValue = XorCsrfTokenUtils.getTokenValue(actualToken, expectedToken.getToken());
boolean csrfCheckPassed = equalsConstantTime(expectedToken.getToken(), actualTokenValue);
if (!csrfCheckPassed) {
throw new InvalidCsrfTokenException(expectedToken, actualToken);
}
return message;
}
/**
* Constant time comparison to prevent against timing attacks.
* @param expected
* @param actual
* @return
*/
private static boolean equalsConstantTime(String expected, String actual) {
if (expected == actual) {
return true;
}
if (expected == null || actual == null) {
return false;
}
// Encode after ensure that the string is not null
byte[] expectedBytes = Utf8.encode(expected);
byte[] actualBytes = Utf8.encode(actual);
return MessageDigest.isEqual(expectedBytes, actualBytes);
}
}
@@ -0,0 +1,72 @@
/*
* 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.web.csrf;
import java.util.Base64;
import org.springframework.security.crypto.codec.Utf8;
/**
* Copied from
* {@link org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler}.
*
* @see <a href=
* "https://github.com/spring-projects/spring-security/issues/12378">gh-12378</a>
*/
final class XorCsrfTokenUtils {
private XorCsrfTokenUtils() {
}
static String getTokenValue(String actualToken, String token) {
byte[] actualBytes;
try {
actualBytes = Base64.getUrlDecoder().decode(actualToken);
}
catch (Exception ex) {
return null;
}
byte[] tokenBytes = Utf8.encode(token);
int tokenSize = tokenBytes.length;
if (actualBytes.length < tokenSize) {
return null;
}
// extract token and random bytes
int randomBytesSize = actualBytes.length - tokenSize;
byte[] xoredCsrf = new byte[tokenSize];
byte[] randomBytes = new byte[randomBytesSize];
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
return Utf8.decode(csrfBytes);
}
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
int len = Math.min(randomBytes.length, csrfBytes.length);
byte[] xoredCsrf = new byte[len];
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
for (int i = 0; i < len; i++) {
xoredCsrf[i] ^= randomBytes[i];
}
return xoredCsrf;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 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.
@@ -24,15 +24,18 @@ import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
/**
* Copies a CsrfToken from the HttpServletRequest's attributes to the WebSocket
* attributes. This is used as the expected CsrfToken when validating connection requests
* to ensure only the same origin connects.
* Loads a CsrfToken from the HttpServletRequest and HttpServletResponse to populate the
* WebSocket attributes. This is used as the expected CsrfToken when validating connection
* requests to ensure only the same origin connects.
*
* @author Rob Winch
* @author Steve Riesenberg
* @since 4.0
*/
public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor {
@@ -41,11 +44,19 @@ public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
CsrfToken token = (CsrfToken) httpRequest.getAttribute(CsrfToken.class.getName());
if (token == null) {
DeferredCsrfToken deferredCsrfToken = (DeferredCsrfToken) httpRequest
.getAttribute(DeferredCsrfToken.class.getName());
if (deferredCsrfToken == null) {
return true;
}
attributes.put(CsrfToken.class.getName(), token);
CsrfToken csrfToken = deferredCsrfToken.get();
// Ensure the values of the CsrfToken are copied into a new token so the old token
// is available for garbage collection.
// This is required because the original token could hold a reference to the
// HttpServletRequest/Response of the handshake request.
CsrfToken resolvedCsrfToken = new DefaultCsrfToken(csrfToken.getHeaderName(), csrfToken.getParameterName(),
csrfToken.getToken());
attributes.put(CsrfToken.class.getName(), resolvedCsrfToken);
return true;
}
@@ -0,0 +1,148 @@
/*
* 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.web.csrf;
import java.util.HashMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link XorCsrfChannelInterceptor}.
*
* @author Steve Riesenberg
*/
public class XorCsrfChannelInterceptorTests {
private static final String XOR_CSRF_TOKEN_VALUE = "wpe7zB62-NCpcA==";
private static final String INVALID_XOR_CSRF_TOKEN_VALUE = "KneoaygbRZtfHQ==";
private CsrfToken token;
private SimpMessageHeaderAccessor messageHeaders;
private MessageChannel channel;
private XorCsrfChannelInterceptor interceptor;
@BeforeEach
public void setup() {
this.token = new DefaultCsrfToken("header", "param", "token");
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
this.messageHeaders.setSessionAttributes(new HashMap<>());
this.channel = mock(MessageChannel.class);
this.interceptor = new XorCsrfChannelInterceptor();
}
@Test
public void preSendWhenConnectWithValidTokenThenSuccess() {
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenConnectWithInvalidTokenThenThrowsInvalidCsrfTokenException() {
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), INVALID_XOR_CSRF_TOKEN_VALUE);
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenConnectWithNoTokenThenThrowsInvalidCsrfTokenException() {
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
// @formatter:off
assertThatExceptionOfType(InvalidCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenConnectWithMissingTokenThenThrowsMissingCsrfTokenException() {
// @formatter:off
assertThatExceptionOfType(MissingCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenConnectWithNullSessionAttributesThenThrowsMissingCsrfTokenException() {
this.messageHeaders.setSessionAttributes(null);
// @formatter:off
assertThatExceptionOfType(MissingCsrfTokenException.class)
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
// @formatter:on
}
@Test
public void preSendWhenAckThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenDisconnectThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenHeartbeatThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenMessageThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenOtherThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.OTHER);
this.interceptor.preSend(message(), this.channel);
}
@Test
public void preSendWhenUnsubscribeThenIgnores() {
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.UNSUBSCRIBE);
this.interceptor.preSend(message(), this.channel);
}
private Message<String> message() {
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
}
}
@@ -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.
@@ -31,6 +31,7 @@ import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.web.socket.WebSocketHandler;
import static org.assertj.core.api.Assertions.assertThat;
@@ -72,10 +73,38 @@ public class CsrfTokenHandshakeInterceptorTests {
@Test
public void beforeHandshake() throws Exception {
CsrfToken token = new DefaultCsrfToken("header", "param", "token");
this.httpRequest.setAttribute(CsrfToken.class.getName(), token);
this.httpRequest.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(token));
this.interceptor.beforeHandshake(this.request, this.response, this.wsHandler, this.attributes);
assertThat(this.attributes.keySet()).containsOnly(CsrfToken.class.getName());
assertThat(this.attributes.values()).containsOnly(token);
CsrfToken csrfToken = (CsrfToken) this.attributes.get(CsrfToken.class.getName());
assertThat(csrfToken.getHeaderName()).isEqualTo(token.getHeaderName());
assertThat(csrfToken.getParameterName()).isEqualTo(token.getParameterName());
assertThat(csrfToken.getToken()).isEqualTo(token.getToken());
// Ensure the values of the CsrfToken are copied into a new token so the old token
// is available for garbage collection.
// This is required because the original token could hold a reference to the
// HttpServletRequest/Response of the handshake request.
assertThat(csrfToken).isNotSameAs(token);
}
private static final class TestDeferredCsrfToken implements DeferredCsrfToken {
private final CsrfToken csrfToken;
private TestDeferredCsrfToken(CsrfToken csrfToken) {
this.csrfToken = csrfToken;
}
@Override
public CsrfToken get() {
return this.csrfToken;
}
@Override
public boolean isGenerated() {
return false;
}
}
}
@@ -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) {
@@ -60,6 +60,7 @@ import org.mockito.ArgumentCaptor;
import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -700,6 +701,7 @@ public class NimbusJwtDecoderTests {
RestOperations restOperations = mock(RestOperations.class);
Cache cache = mock(Cache.class);
given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET);
given(cache.get(eq(JWK_SET_URI))).willReturn(new SimpleValueWrapper(JWK_SET));
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK));
@@ -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.
@@ -30,6 +30,7 @@ import java.util.function.Function;
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;
@@ -986,6 +987,14 @@ public final class RelyingPartyRegistration {
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
private Builder() {
}
private Builder(AssertingPartyDetails.Builder assertingPartyDetailsBuilder) {
this.assertingPartyDetailsBuilder = assertingPartyDetailsBuilder;
}
/**
* Set the asserting party's <a href=
* "https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor">EntityID</a>.
@@ -1048,7 +1057,7 @@ public final class RelyingPartyRegistration {
public static final class Builder {
private String registrationId;
private Converter<ProviderDetails, String> registrationId = ProviderDetails::getEntityId;
private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
@@ -1068,12 +1077,17 @@ public final class RelyingPartyRegistration {
private String nameIdFormat = null;
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
private ProviderDetails.Builder providerDetails;
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new LinkedHashSet<>();
private Builder(String registrationId) {
this.registrationId = registrationId;
this.registrationId = (party) -> registrationId;
this.providerDetails = new ProviderDetails.Builder();
}
Builder(AssertingPartyDetails.Builder builder) {
this.providerDetails = new ProviderDetails.Builder(builder);
}
/**
@@ -1082,7 +1096,7 @@ public final class RelyingPartyRegistration {
* @return this object
*/
public Builder registrationId(String id) {
this.registrationId = id;
this.registrationId = (party) -> id;
return this;
}
@@ -1405,11 +1419,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.providerDetails.build(), this.nameIdFormat, this.credentials,
this.decryptionX509Credentials, this.signingX509Credentials);
ProviderDetails party = this.providerDetails.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.credentials, 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.
@@ -31,10 +31,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;
@@ -71,6 +73,8 @@ class OpenSamlAuthenticationRequestResolver {
private final NameIDBuilder nameIdBuilder;
private final NameIDPolicyBuilder nameIdPolicyBuilder;
private RequestMatcher requestMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}");
private Converter<HttpServletRequest, String> relayStateResolver = (request) -> UUID.randomUUID().toString();
@@ -95,6 +99,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) {
@@ -130,6 +137,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));
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -674,7 +675,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
attributeMap.addAll(attribute.getName(), attributeValues);
}
}
return attributeMap;
return new LinkedHashMap<>(attributeMap); // gh-11785
}
private static List<String> getSessionIndexes(Assertion assertion) {
@@ -32,6 +32,7 @@ import java.util.function.Consumer;
import javax.xml.namespace.QName;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
@@ -68,6 +69,7 @@ import org.w3c.dom.Element;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.Authentication;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
@@ -349,6 +351,23 @@ public class OpenSaml4AuthenticationProviderTests {
assertThat(principal.getSessionIndexes()).contains("session-index");
}
// gh-11785
@Test
public void deserializeWhenAssertionContainsAttributesThenWorks() throws Exception {
ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
mapper.registerModules(SecurityJackson2Modules.getModules(loader));
Response response = response();
Assertion assertion = assertion();
List<AttributeStatement> attributes = TestOpenSamlObjects.attributeStatements();
assertion.getAttributeStatements().addAll(attributes);
response.getAssertions().add(signed(assertion));
Saml2AuthenticationToken token = token(response, verifying(registration()));
Authentication authentication = this.provider.authenticate(token);
String result = mapper.writeValueAsString(authentication);
mapper.readValue(result, Authentication.class);
}
@Test
public void authenticateWhenAssertionContainsCustomAttributesThenItSucceeds() {
Response response = response();
@@ -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-2020 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).credentials((c) -> c.add(signingCredential))
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
@@ -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())
@@ -75,6 +76,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())
@@ -110,6 +112,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())
@@ -132,6 +135,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>
@@ -42,9 +42,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 javax.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
@@ -28,6 +28,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;
/**
@@ -54,6 +56,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)
@@ -77,6 +81,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() {
@@ -115,4 +121,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,9 @@ 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;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -146,6 +149,8 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
private AuthenticationFailureHandler failureHandler;
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
@Override
public void afterPropertiesSet() {
Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
@@ -183,6 +188,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
context.setAuthentication(targetUser);
this.securityContextHolderStrategy.setContext(context);
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", targetUser));
this.securityContextRepository.saveContext(context, request, response);
// redirect to target url
this.successHandler.onAuthenticationSuccess(request, response, targetUser);
}
@@ -200,6 +206,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
context.setAuthentication(originalUser);
this.securityContextHolderStrategy.setContext(context);
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", originalUser));
this.securityContextRepository.saveContext(context, request, response);
// redirect to target url
this.successHandler.onAuthenticationSuccess(request, response, originalUser);
return;
@@ -525,6 +532,19 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
/**
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
* switch user success. The default is
* {@link RequestAttributeSecurityContextRepository}.
* @param securityContextRepository the {@link SecurityContextRepository} to use.
* Cannot be null.
* @since 5.7.7
*/
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
private static RequestMatcher createMatcher(String pattern) {
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
}
@@ -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 loadContext(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.
@@ -150,13 +150,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);
@@ -393,11 +426,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.
@@ -108,6 +108,7 @@ public final class CsrfFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
this.requestHandler.handle(request, response, deferredCsrfToken::get);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
@@ -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.
@@ -226,7 +226,8 @@ public class DefaultSavedRequest implements SavedRequest {
if (!propertyEquals(this.pathInfo, request.getPathInfo())) {
return false;
}
if (!propertyEquals(this.queryString, request.getQueryString())) {
if (!propertyEquals(createQueryString(this.queryString, this.matchingRequestParameterName),
request.getQueryString())) {
return false;
}
if (!propertyEquals(this.requestURI, request.getRequestURI())) {
@@ -16,6 +16,9 @@
package org.springframework.security.web;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -142,4 +145,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.
@@ -143,12 +143,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,16 @@
package org.springframework.security.web.authentication;
import javax.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 +113,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();
}
}

Some files were not shown because too many files have changed in this diff Show More