1
0
mirror of synced 2026-07-09 21:00:02 +00:00

Compare commits

...

143 Commits

Author SHA1 Message Date
Rob Winch 1a6915d3c0 rm .github/workflows for unsupported branch 2025-05-02 12:35:31 -05:00
Rob Winch 8325728035 rm merge-dependabot-pr.yml from Unsupported Branch 2025-04-25 13:16:44 -05:00
Josh Cummings 643a3f1206 Test Setting logoutRequestRepository
Issue gh-16093
2024-12-19 08:55:18 -07:00
Steven Williams 7aafe2ed5a Set Saml2RelyingPartyInitiatedLogoutSuccessHandler#logoutRequestRepository
Closes gh-16093
2024-12-19 08:53:02 -07:00
dependabot[bot] e528923878 Bump io.projectreactor:reactor-bom from 2023.0.12 to 2023.0.13
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.12 to 2023.0.13.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2023.0.12...2023.0.13)

---
updated-dependencies:
- dependency-name: io.projectreactor:reactor-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 19:18:45 -08:00
Josh Cummings 2fe7faea87 Merge branch '5.8.x' into 6.2.x 2024-12-09 15:27:06 -07:00
Josh Cummings eb313ec901 Link to Messaging SpEL Migration Details
Issue gh-12650
2024-12-09 15:26:54 -07:00
Josh Cummings 348f064df1 Merge branch '5.8.x' into 6.2.x 2024-12-09 15:25:50 -07:00
Josh Cummings 8b9fe13c88 Document Messaging SpEL Migration
Issue gh-12650
2024-12-09 15:25:33 -07:00
dependabot[bot] cac03995a3 Bump @antora/collector-extension from 1.0.0 to 1.0.1 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 17:07:09 -08:00
dependabot[bot] 6ea7da5178 Bump antora from 3.2.0-alpha.6 to 3.2.0-alpha.8 in /docs
Bumps [antora](https://gitlab.com/antora/antora) from 3.2.0-alpha.6 to 3.2.0-alpha.8.
- [Changelog](https://gitlab.com/antora/antora/blob/main/CHANGELOG.adoc)
- [Commits](https://gitlab.com/antora/antora/compare/v3.2.0-alpha.6...v3.2.0-alpha.8)

---
updated-dependencies:
- dependency-name: antora
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 17:06:45 -08:00
dependabot[bot] 16272f634c Bump @antora/collector-extension from 1.0.0 to 1.0.1 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 16:48:56 -08:00
dependabot[bot] 0b9887505e Bump antora from 3.2.0-alpha.6 to 3.2.0-alpha.8 in /docs
Bumps [antora](https://gitlab.com/antora/antora) from 3.2.0-alpha.6 to 3.2.0-alpha.8.
- [Changelog](https://gitlab.com/antora/antora/blob/main/CHANGELOG.adoc)
- [Commits](https://gitlab.com/antora/antora/compare/v3.2.0-alpha.6...v3.2.0-alpha.8)

---
updated-dependencies:
- dependency-name: antora
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 16:48:02 -08:00
Josh Cummings 87de6cea1b Use Reactive JSON Encoder
Closes gh-16177
2024-12-06 15:14:07 -07:00
Josh Cummings 3d1e4b5f18 Polish Tests
Confirm that responses are a valid JSON map

Issue gh-16177
2024-12-06 15:14:07 -07:00
Josh Cummings 96a9cf0d2d Restore Previous Behavior for Servlet 5
Closes gh-16173
2024-12-05 09:52:06 -07:00
dependabot[bot] a6c3d123ed Bump @antora/collector-extension from 1.0.0-rc.1 to 1.0.0 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 17:08:47 -08:00
dependabot[bot] e79ceaeb75 Bump @antora/collector-extension from 1.0.0-rc.1 to 1.0.0 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 16:27:59 -08:00
Harpreet Singh 68d91916e2 Polish Dark Mode for CSRF and Method Security diagrams
Closes gh-16151
2024-11-26 01:53:56 +05:30
dependabot[bot] 1dbaa08cd4 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-24 16:48:27 -08:00
dependabot[bot] 1d916c35d1 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-24 16:27:14 -08:00
Steve Riesenberg 0eb6acde96 Polish gh-16133 2024-11-20 15:50:29 -06:00
Kai Zander 73f3f75712 Always return current ClientRegistration in loadAuthorizedClient
This changes `InMemoryOAuth2AuthorizedClientService.loadAuthorizedClient`
(and its reactive counterpart) to always return `OAuth2AuthorizedClient`
instances containing the current `ClientRegistration` as obtained from
the `ClientRegistrationRepository`.

Before this change, the first `ClientRegistration` instance was cached,
with the effect that any changes made in the `ClientRegistrationRepository`
(such as a new client secret) would not have taken effect.

Closes gh-15511
2024-11-20 15:50:29 -06:00
Steve Riesenberg b8e9f47dd4 Merge branch '5.8.x' into 6.2.x 2024-11-18 16:19:06 -06:00
github-actions[bot] 9363959566 Update Antora Spring UI to v0.4.18 2024-11-18 16:18:28 -06:00
github-actions[bot] f38129b5a0 Update Antora Spring UI to v0.4.18 2024-11-18 16:18:27 -06:00
Steve Riesenberg 7d55c079c9 Merge branch '5.8.x' into 6.2.x 2024-11-18 16:04:27 -06:00
Steve Riesenberg e44fc3817a Merge branch '5.7.x' into 5.8.x 2024-11-18 16:04:13 -06:00
Josh Cummings ba520db7f7 Merge branch '5.8.x' into 6.2.x 2024-11-18 12:12:46 -07:00
Josh Cummings 85248083c0 Add Dark Mode CSS Style
Closes gh-14834
2024-11-18 12:10:48 -07:00
Josh Cummings be09ed7e2b Merge branch '5.8.x' into 6.2.x 2024-11-18 10:40:07 -07:00
Joe Grandja d224dbe334 Next development version 2024-11-18 12:14:18 -05:00
github-actions[bot] c62168ca5b Next development version 2024-11-18 17:12:34 +00:00
github-actions[bot] d0b2b33dce Release 5.7.14 2024-11-18 16:36:56 +00:00
github-actions[bot] 7881660ca0 Next development version 2024-11-18 16:01:43 +00:00
github-actions[bot] 506e5b7f11 Release 5.8.16 2024-11-18 15:23:30 +00:00
github-actions[bot] a5cd7ce122 Release 6.2.8 2024-11-18 15:23:13 +00:00
Joe Grandja a8c4d6cead Require Locale argument for toLower/toUpperCase usage 2024-11-18 04:22:26 -05:00
Joe Grandja a7bf8f7cc6 Require Locale argument for toLower/toUpperCase usage 2024-11-18 04:03:07 -05:00
dependabot[bot] 5f838b0e93 Bump io.micrometer:micrometer-observation from 1.12.12 to 1.12.13
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.12.12 to 1.12.13.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.12...v1.12.13)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-observation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 19:53:46 -08:00
dependabot[bot] bc3e6f2919 Bump org.springframework.data:spring-data-bom
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.11 to 2023.1.12.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.11...2023.1.12)

---
updated-dependencies:
- dependency-name: org.springframework.data:spring-data-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 19:51:06 -08:00
dependabot[bot] 451fbf0227 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 16:59:48 -08:00
dependabot[bot] 810d83e2f8 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 16:59:03 -08:00
Joe Grandja 0eaffb37e7 Require Locale argument for toLower/toUpperCase usage 2024-11-15 11:01:23 -05:00
dependabot[bot] 127ed4b7cf Bump org.springframework:spring-framework-bom from 6.1.14 to 6.1.15
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.14 to 6.1.15.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.14...v6.1.15)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 20:08:07 -08:00
dependabot[bot] da345a3bac Bump org.springframework.ldap:spring-ldap-core from 3.2.7 to 3.2.8
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 3.2.7 to 3.2.8.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/3.2.7...3.2.8)

---
updated-dependencies:
- dependency-name: org.springframework.ldap:spring-ldap-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 19:56:04 -08:00
dependabot[bot] d985d044bc Bump org.springframework.ldap:spring-ldap-core from 2.4.2 to 2.4.4
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 2.4.2 to 2.4.4.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/2.4.2...2.4.4)

---
updated-dependencies:
- dependency-name: org.springframework.ldap:spring-ldap-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 19:40:38 -08:00
dependabot[bot] 0790978590 Bump io.projectreactor:reactor-bom from 2023.0.11 to 2023.0.12
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.11 to 2023.0.12.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2023.0.11...2023.0.12)

---
updated-dependencies:
- dependency-name: io.projectreactor:reactor-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-12 20:04:22 -08:00
dependabot[bot] 0d8b8ee04b Bump io.micrometer:micrometer-observation from 1.12.11 to 1.12.12
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.12.11 to 1.12.12.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.11...v1.12.12)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-observation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 20:04:38 -08:00
Rob Winch 81e74e65d4 Support ServerExchangeRejectedHandler @Bean
Closes gh-16061
2024-11-11 14:58:00 -06:00
Rob Winch c24b5ebe98 Support ServerExchangeRejectedHandler @Bean
Closes gh-15975
2024-11-11 14:42:19 -06:00
Rob Winch fd900c288e checkExpectedBranchVersion trim version 2024-11-11 14:42:18 -06:00
Rob Winch e86d88d0cf Support ServerExchangeRejectedHandler @Bean
Closes gh-15975
2024-11-08 14:48:40 -06:00
Steve Riesenberg 86f3cd6dc7 Polish gh-15940
Closes gh-15885
2024-11-04 13:34:12 -06:00
Hyeongi Jeong 4c6fef82b9 Fix error when Bearer token is requested with empty string
Issue gh-15885
2024-11-04 13:33:58 -06:00
dependabot[bot] 18129f3af3 Bump org.hsqldb:hsqldb from 2.7.3 to 2.7.4
Bumps org.hsqldb:hsqldb from 2.7.3 to 2.7.4.

---
updated-dependencies:
- dependency-name: org.hsqldb:hsqldb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 20:01:04 -08:00
dependabot[bot] f28c26fd54 Bump org.hsqldb:hsqldb from 2.7.3 to 2.7.4
Bumps org.hsqldb:hsqldb from 2.7.3 to 2.7.4.

---
updated-dependencies:
- dependency-name: org.hsqldb:hsqldb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 19:47:57 -08:00
dependabot[bot] 421430330a Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 16:34:33 -08:00
dependabot[bot] fed3c99c1d Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 16:24:16 -08:00
Rob Winch 5c2106b22e Format 2024-10-25 12:24:54 -05:00
Rob Winch 1ba6301afa Support ServerWebExchangeFirewall @Bean
Closes gh-15987
2024-10-25 12:13:41 -05:00
Rob Winch 48241deba3 Merge branch '5.7.x' into 5.8.x
Support ServerWebExchangeFirewall @Bean

Closes gh-15977
2024-10-25 12:11:57 -05:00
Rob Winch 9cc11be9f3 Merge branch '5.7.x' (early part) into 5.8.x 2024-10-25 12:11:51 -05:00
Josh Cummings addc7c53b2 Merge branch '5.8.x' into 6.2.x
Closes gh-15985
2024-10-24 11:56:16 -06:00
DingHao 1399a82ea9 Return Null Request When Cookie Is Malformed
Closes gh-15905
2024-10-24 10:55:36 -07:00
Steve Riesenberg 3592253b8e Merge branch '5.8.x' into 6.2.x 2024-10-24 11:16:49 -05:00
github-actions[bot] 0b83830fb3 Update Antora Spring UI to v0.4.17 2024-10-24 11:14:43 -05:00
github-actions[bot] ec33e40748 Update Antora Spring UI to v0.4.17 2024-10-24 11:12:30 -05:00
Scott Murphy Heiberg 18dba34bde Make RequestMatcherDelegatingAuthorizationManager Post-Processable
Closes gh-15948
2024-10-23 15:15:10 -07:00
Rob Winch e48d6b039b Support ServerWebExchangeFirewall @Bean
Closes gh-15974
2024-10-22 18:25:28 -05:00
Steve Riesenberg e9bbe31b1a Merge branch '5.8.x' into 6.2.x 2024-10-21 13:52:08 -05:00
github-actions[bot] dd3c6892e9 Next development version 2024-10-21 17:58:56 +00:00
github-actions[bot] f9f533499b Next development version 2024-10-21 17:51:39 +00:00
github-actions[bot] 7209155482 Next development version 2024-10-21 17:42:46 +00:00
github-actions[bot] c552366a78 Release 5.7.13 2024-10-21 17:23:29 +00:00
github-actions[bot] 72a208e02c Release 5.8.15 2024-10-21 17:16:06 +00:00
github-actions[bot] 77afcd3e53 Release 6.2.7 2024-10-21 17:11:06 +00:00
Steve Riesenberg 7215c72373 Migrate slack notifications to GChat
Issue gh-15503

[skip ci]
2024-10-21 11:24:40 -05:00
Rob Winch 0e257b56ce Add Firewall for WebFlux
Closes gh-15967
2024-10-21 09:42:24 -05:00
Rob Winch 542071b1f8 Merge Add Firewall for WebFlux
Closes gh-15967
2024-10-21 08:56:42 -05:00
Rob Winch 4ce7cde155 Add Firewall for WebFlux
Closes gh-15967
2024-10-21 08:46:13 -05:00
dependabot[bot] ee5e11a294 Bump org.springframework.data:spring-data-bom
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.10 to 2023.1.11.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.10...2023.1.11)

---
updated-dependencies:
- dependency-name: org.springframework.data:spring-data-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-20 20:30:49 -07:00
Steve Riesenberg a504b2f125 Merge branch '5.8.x' into 6.2.x 2024-10-18 12:17:06 -05:00
dependabot[bot] 52438b4973 Bump org.springframework:spring-framework-bom from 6.1.13 to 6.1.14
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.13 to 6.1.14.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.13...v6.1.14)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-17 21:15:29 -07:00
dependabot[bot] adf1755064 Bump org.springframework.ldap:spring-ldap-core from 3.2.6 to 3.2.7
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 3.2.6 to 3.2.7.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/3.2.6...3.2.7)

---
updated-dependencies:
- dependency-name: org.springframework.ldap:spring-ldap-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-17 21:03:31 -07:00
dependabot[bot] c66ee4a21a Bump org.springframework.ldap:spring-ldap-core from 2.4.1 to 2.4.2
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/2.4.1...2.4.2)

---
updated-dependencies:
- dependency-name: org.springframework.ldap:spring-ldap-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-17 20:35:06 -07:00
dependabot[bot] 517ea7f42a Bump io.projectreactor.tools:blockhound
Bumps [io.projectreactor.tools:blockhound](https://github.com/reactor/BlockHound) from 1.0.9.RELEASE to 1.0.10.RELEASE.
- [Release notes](https://github.com/reactor/BlockHound/releases)
- [Commits](https://github.com/reactor/BlockHound/compare/1.0.9.RELEASE...1.0.10.RELEASE)

---
updated-dependencies:
- dependency-name: io.projectreactor.tools:blockhound
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 20:59:35 -07:00
dependabot[bot] ea3cc42f0b Bump io.projectreactor:reactor-bom from 2023.0.10 to 2023.0.11
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.10 to 2023.0.11.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2023.0.10...2023.0.11)

---
updated-dependencies:
- dependency-name: io.projectreactor:reactor-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 20:31:09 -07:00
dependabot[bot] beecb2e06b Bump io.micrometer:micrometer-observation from 1.12.10 to 1.12.11
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.12.10 to 1.12.11.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.10...v1.12.11)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-observation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 21:16:13 -07:00
github-actions[bot] 49e39144d7 Bump Gradle Wrapper from 8.10.1 to 8.10.2 2024-10-14 13:14:02 -07:00
github-actions[bot] f082494e1b Merge branch '5.8.x' into 6.2.x 2024-10-14 01:09:39 +00:00
dependabot[bot] fe79766aa2 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-13 18:08:50 -07:00
dependabot[bot] ef70561ac9 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-13 18:03:15 -07:00
dependabot[bot] 48af71ae5e Bump io.mockk:mockk from 1.13.12 to 1.13.13
Bumps [io.mockk:mockk](https://github.com/mockk/mockk) from 1.13.12 to 1.13.13.
- [Release notes](https://github.com/mockk/mockk/releases)
- [Commits](https://github.com/mockk/mockk/compare/1.13.12...1.13.13)

---
updated-dependencies:
- dependency-name: io.mockk:mockk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-09 21:05:06 -07:00
dependabot[bot] 73ee0cf7ec Bump org.junit:junit-bom from 5.10.4 to 5.10.5
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.10.4 to 5.10.5.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.4...r5.10.5)

---
updated-dependencies:
- dependency-name: org.junit:junit-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-06 20:29:49 -07:00
Josh Cummings c1857c0308 Fix Formatting
Issue gh-15771
2024-09-30 16:19:26 -07:00
chao.wang 690e012fb1 Improve OidcBackChannelLogoutTokenValidator error when provider issuer is missing
Closes gh-15771
2024-09-30 16:19:26 -07:00
Tran Ngoc Nhan f7b85ed314 Fix Broken Resource Server Doc Links 2024-09-26 13:08:12 -06:00
dependabot[bot] 8b63817f02 Bump org.junit:junit-bom from 5.10.3 to 5.10.4
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.10.3 to 5.10.4.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.3...r5.10.4)

---
updated-dependencies:
- dependency-name: org.junit:junit-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-24 21:01:50 -07:00
dependabot[bot] 95cee40224 Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.13 to 1.0.0-alpha.14.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.13...v1.0.0-alpha.14)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-22 18:06:59 -07:00
github-actions[bot] bcf0a7f55d Merge branch '5.8.x' into 6.2.x 2024-09-23 00:55:51 +00:00
dependabot[bot] 755e9f2f69 Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.13 to 1.0.0-alpha.14.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.13...v1.0.0-alpha.14)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-22 17:55:03 -07:00
Steve Riesenberg 551c483ee6 Merge branch '5.8.x' into 6.2.x
Closes gh-15828
2024-09-19 16:22:37 -05:00
Steve Riesenberg 7b7a3044cf Polish gh-15533 2024-09-19 16:13:03 -05:00
陈圳佳 8a791028b1 Fix array values of additionalParameters
Closes gh-15468
2024-09-19 16:10:00 -05:00
Josh Cummings 1a0203ecf6 Fix AuthorizationManager API Documentation Typo
Closes gh-15704
2024-09-17 18:25:08 -06:00
github-actions[bot] c4ce3f61fd Bump Gradle Wrapper from 8.10 to 8.10.1 2024-09-17 07:13:15 -03:00
dependabot[bot] 8a48c6903a Bump org.springframework.data:spring-data-bom from 2023.1.9 to 2023.1.10
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.9 to 2023.1.10.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.9...2023.1.10)

---
updated-dependencies:
- dependency-name: org.springframework.data:spring-data-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-15 21:01:04 -07:00
Steve Riesenberg 1782f17e7f Merge branch '5.8.x' into 6.2.x
Closes gh-15807
2024-09-13 16:08:04 -05:00
Steve Riesenberg 0a4eb0f09a Update credential erasure examples
Closes gh-15683
2024-09-13 16:07:47 -05:00
dependabot[bot] f3061d9c1a Bump org.springframework:spring-framework-bom from 6.1.12 to 6.1.13
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.12 to 6.1.13.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.12...v6.1.13)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-12 20:43:14 -07:00
dependabot[bot] e86dac8a64 Bump io.projectreactor:reactor-bom from 2023.0.9 to 2023.0.10
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.9 to 2023.0.10.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2023.0.9...2023.0.10)

---
updated-dependencies:
- dependency-name: io.projectreactor:reactor-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-10 20:55:48 -07:00
Tran Ngoc Nhan b9f051d15b docs: update 2024-09-10 11:50:11 -07:00
dependabot[bot] 44afdcec64 Bump io.micrometer:micrometer-observation from 1.12.9 to 1.12.10
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.12.9 to 1.12.10.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.9...v1.12.10)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-observation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 20:39:13 -07:00
Marcus Hert Da Coregio a268b78473 Merge branch '5.8.x' into 6.2.x
Closes gh-15766
2024-09-09 08:53:39 -03:00
github-actions[bot] b2c0cc3809 Merge branch '5.8.x' into 6.2.x 2024-09-09 11:51:53 +00:00
Marcus Hert Da Coregio a0e6c17512 Do not log exception if CasJackson2Module is not present
Closes gh-15749
2024-09-09 08:32:40 -03:00
dependabot[bot] e251779b31 Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.12 to 1.0.0-alpha.13.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.12...v1.0.0-alpha.13)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-08 18:12:55 -07:00
dependabot[bot] 51b0f1058b Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.12 to 1.0.0-alpha.13.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.12...v1.0.0-alpha.13)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-08 17:51:19 -07:00
dependabot[bot] c44eed0d15 Bump org-eclipse-jetty from 11.0.23 to 11.0.24
Bumps `org-eclipse-jetty` from 11.0.23 to 11.0.24.

Updates `org.eclipse.jetty:jetty-server` from 11.0.23 to 11.0.24

Updates `org.eclipse.jetty:jetty-servlet` from 11.0.23 to 11.0.24

---
updated-dependencies:
- dependency-name: org.eclipse.jetty:jetty-server
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.eclipse.jetty:jetty-servlet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-03 20:55:25 -07:00
dependabot[bot] 7ad6f38d5b Bump org-eclipse-jetty from 9.4.55.v20240627 to 9.4.56.v20240826
Bumps `org-eclipse-jetty` from 9.4.55.v20240627 to 9.4.56.v20240826.

Updates `org.eclipse.jetty:jetty-server` from 9.4.55.v20240627 to 9.4.56.v20240826

Updates `org.eclipse.jetty:jetty-servlet` from 9.4.55.v20240627 to 9.4.56.v20240826

---
updated-dependencies:
- dependency-name: org.eclipse.jetty:jetty-server
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.eclipse.jetty:jetty-servlet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-03 20:40:27 -07:00
Josh Cummings b22061d0b6 Merge branch '5.8.x' into 6.2.x 2024-09-03 16:33:22 -06:00
Josh Cummings 97cefa6830 Update Formatting
Issue gh-15714
2024-09-03 15:32:59 -07:00
tugjg f836efb912 Address unnecessary method invocation
Closes gh-15714
2024-09-03 15:32:59 -07:00
Bryan Kelly 69231c5853 Avoid running the VerifyDependenciesVersionsTask and CheckExpecedBranchVersionTask when inputs are stable #15713 2024-09-02 08:45:48 -03:00
Hero Wanders 2ba9b6821a Fix OIDC Logout Code Snippets 2024-08-26 15:32:35 -07:00
Hero Wanders f372f5cf52 Replace OidcSessionStrategy References with OidcSessionRegistry 2024-08-26 15:32:35 -07:00
Junhyunny 47723f6d39 Fix code format in OIDC Logout docs 2024-08-26 15:14:02 -07:00
dependabot[bot] c3a5cf54d4 Bump jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api
Bumps [jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api](https://github.com/eclipse-ee4j/jstl-api) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/eclipse-ee4j/jstl-api/releases)
- [Commits](https://github.com/eclipse-ee4j/jstl-api/commits)

---
updated-dependencies:
- dependency-name: jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 12:40:49 -07:00
Josh Cummings a3b88a8d4b Enable Runtime Method Parameter Reflection
Several method security tests rely on method parameters
being preserved, in order to demonstrate the difference
between relying on runtime reflection and using the @P
annotation.

Closes gh-15680
2024-08-22 19:44:11 -06:00
Josh Cummings 3ee5a96e53 Merge branch '5.8.x' into 6.2.x
Closes gh-15675
2024-08-22 12:24:56 -06:00
Josh Cummings 5c604b95fb Correct PostFilterAuthorizationMethodInterceptor Target Type
Previously, `postFilterAuthorizationMethodInterceptor` mistakenly
was published as an `Advisor`. Because `MethodSecurityAdvisorRegistrar`
re-publishes each pre/post annotation interceptor also as an `Advisor`,
this resulted in a duplicate advisor for `@PostFilter`.

Closes gh-15651
2024-08-22 12:10:25 -06:00
Marcus Hert Da Coregio c06543daf3 Merge branch '5.8.x' into 6.2.x 2024-08-22 11:06:19 -03:00
Marcus Hert Da Coregio e92a945a2d Replace Env Variable with Expression
Issue gh-15659
2024-08-22 10:17:55 -03:00
Steve Riesenberg fb054198af Migrate slack notifications to GChat (2nd attempt)
Closes gh-15667
2024-08-21 16:16:50 -05:00
Steve Riesenberg 035f86bdb3 Merge branch '5.8.x' into 6.2.x
Closes gh-15667
2024-08-21 15:45:39 -05:00
Steve Riesenberg 92809cef01 Migrate slack notifications to GChat (2nd attempt)
Closes gh-15503
2024-08-21 15:44:44 -05:00
dependabot[bot] 439b797eb8 Bump org-eclipse-jetty from 11.0.22 to 11.0.23
Bumps `org-eclipse-jetty` from 11.0.22 to 11.0.23.

Updates `org.eclipse.jetty:jetty-server` from 11.0.22 to 11.0.23

Updates `org.eclipse.jetty:jetty-servlet` from 11.0.22 to 11.0.23

---
updated-dependencies:
- dependency-name: org.eclipse.jetty:jetty-server
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.eclipse.jetty:jetty-servlet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-20 21:02:01 -07:00
github-actions[bot] f4585d8e4a Bump Gradle Wrapper from 8.7 to 8.10 2024-08-20 09:40:53 -03:00
Marcus Hert Da Coregio 83eceea58f Merge branch '5.8.x' into 6.2.x 2024-08-20 09:22:51 -03:00
github-actions[bot] 097c927d8d Next development version 2024-08-19 21:10:35 +00:00
github-actions[bot] 38093090c2 Next development version 2024-08-19 21:06:57 +00:00
github-actions[bot] 0115e8aea1 Release 5.8.14 2024-08-19 18:22:56 +00:00
110 changed files with 3221 additions and 852 deletions
@@ -1,23 +0,0 @@
name: Clean build artifacts
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Delete artifacts in cron job
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
echo "Running clean build artifacts logic"
output=$(curl -X GET -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts | grep '"id"' | cut -d : -f2 | sed 's/,*$//g')
echo Output is $output
for id in $output; do curl -X DELETE -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts/$id; done;
@@ -1,132 +0,0 @@
name: CI
on:
push:
branches-ignore:
- "dependabot/**"
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
permissions:
contents: read
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@v1
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
jdk: [ 17 ]
with:
runs-on: ${{ matrix.os }}
java-version: ${{ matrix.jdk }}
distribution: temurin
secrets: inherit
test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@v1
strategy:
matrix:
include:
- java-version: 21-ea
toolchain: 21
- java-version: 17
toolchain: 17
with:
java-version: ${{ matrix.java-version }}
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=6.1.+ -PreactorVersion=2023.0.+ -PspringDataVersion=2023.1.+ --stacktrace
secrets: inherit
check-samples:
name: Check Samples
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'spring-projects' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: 17
distribution: temurin
- name: Check samples project
env:
LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos
SAMPLES_DIR: ../spring-security-samples
run: |
# Extract version from gradle.properties
version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
# Extract samplesBranch from gradle.properties
samples_branch=$(cat gradle.properties | grep "samplesBranch=" | awk -F'=' '{print $2}')
./gradlew publishMavenJavaPublicationToLocalRepository
./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR"
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" :runAllTests
check-tangles:
name: Check for Package Tangles
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'spring-projects' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: 17
distribution: temurin
- name: Check for package tangles
env:
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
run: |
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
deploy-artifacts:
name: Deploy Artifacts
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-docs:
name: Deploy Docs
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1
with:
should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-schema:
name: Deploy Schema
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
perform-release:
name: Perform Release
needs: [ deploy-artifacts, deploy-docs, deploy-schema ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
with:
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
milestone-repo-url: https://repo.spring.io/artifactory/milestone
release-repo-url: https://repo1.maven.org/maven2
artifact-path: org/springframework/security/spring-security-core
slack-announcing-id: spring-security-announcing
secrets: inherit
notify_result:
name: Check for failures
needs: [ perform-release ]
if: failure()
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Send Slack message
# Workaround while waiting for Gamesight/slack-workflow-status#38 to be fixed
# See https://github.com/Gamesight/slack-workflow-status/issues/38
uses: sjohnr/slack-workflow-status@v1-beta
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
-33
View File
@@ -1,33 +0,0 @@
name: Deploy Docs
on:
push:
branches-ignore:
- "gh-pages"
- "dependabot/**"
tags: '**'
repository_dispatch:
types: request-build-reference # legacy
#schedule:
#- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch:
permissions: read-all
jobs:
build:
runs-on: ubuntu-latest
if: github.repository_owner == 'spring-projects'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: docs-build
fetch-depth: 1
- name: Dispatch (partial build)
if: github.ref_type == 'branch'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
- name: Dispatch (full build)
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
@@ -1,32 +0,0 @@
name: Execute Gradle Wrapper Upgrade
on:
schedule:
- cron: '0 2 * * *' # 2am UTC
workflow_dispatch:
jobs:
upgrade_wrapper:
name: Execution
runs-on: ubuntu-latest
steps:
- name: Set up Git configuration
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global url."https://unused-username:${TOKEN}@github.com/".insteadOf "https://github.com/"
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
- name: Upgrade Wrappers
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
env:
WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,46 +0,0 @@
name: Mark Duplicate Dependabot PRs
on:
pull_request:
types: [closed]
jobs:
check_duplicate_prs:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Extract Dependency Name from PR Title
id: extract
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}')
echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT
- name: Find PRs
id: find_duplicates
env:
DEPENDENCY_NAME: ${{ steps.extract.outputs.dependency_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "$DEPENDENCY_NAME"' --json number --jq 'map(.number) | join(",")')
echo "prs=$PRS" >> $GITHUB_OUTPUT
- name: Label Duplicate PRs
if: steps.find_duplicates.outputs.prs != ''
env:
PRS: ${{ steps.find_duplicates.outputs.prs }}
CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
for i in ${PRS//,/ }
do
if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then
echo "Marking PR $i as duplicate"
gh pr edit "$i" --add-label "status: duplicate"
gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER"
fi
done
-52
View File
@@ -1,52 +0,0 @@
name: Merge Dependabot PR
on: pull_request_target
run-name: Merge Dependabot PR ${{ github.ref_name }}
permissions: write-all
jobs:
merge-dependabot-pr:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Set Milestone to Dependabot Pull Request
id: set-milestone
run: |
if test -f pom.xml
then
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
else
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
fi
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
if [ -z $MILESTONE ]
then
gh run cancel ${{ github.run_id }}
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
else
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
echo mergeEnabled=true >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Dependabot pull request
if: steps.set-milestone.outputs.mergeEnabled
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
@@ -1,40 +0,0 @@
name: Check Milestone
on:
milestone:
types: [created, opened, edited]
env:
DUE_ON: ${{ github.event.milestone.due_on }}
TITLE: ${{ github.event.milestone.title }}
permissions:
contents: read
jobs:
spring-releasetrain-checks:
name: Check DueOn is on a Release Date
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Print Milestone Being Checked
run: echo "Validating DueOn '$DUE_ON' for milestone '$TITLE'"
- name: Validate DueOn
if: env.DUE_ON != ''
run: |
export TOOL_VERSION=0.1.1
wget "https://repo.maven.apache.org/maven2/io/spring/releasetrain/spring-release-train-tools/$TOOL_VERSION/spring-release-train-tools-$TOOL_VERSION.jar"
java -cp "spring-release-train-tools-$TOOL_VERSION.jar" io.spring.releasetrain.CheckMilestoneDueOnMain --dueOn "$DUE_ON" --expectedDayOfWeek MONDAY --expectedMondayCount 3
notify_result:
name: Check for failures
needs: [spring-releasetrain-checks]
if: failure()
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Send Slack message
uses: Gamesight/slack-workflow-status@v1.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
-44
View File
@@ -1,44 +0,0 @@
name: PR Build
on: pull_request
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue
generate-docs:
name: Generate Docs
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '17'
distribution: 'temurin'
- name: Run Antora
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
- name: Upload Docs
id: upload
uses: actions/upload-artifact@v4
with:
name: docs
path: docs/build/site
overwrite: true
-24
View File
@@ -1,24 +0,0 @@
name: Release Scheduler
on:
schedule:
- cron: '15 15 * * MON' # Every Monday at 3:15pm UTC
workflow_dispatch:
permissions: read-all
jobs:
dispatch_scheduled_releases:
name: Dispatch scheduled releases
if: github.repository_owner == 'spring-projects'
strategy:
matrix:
# List of active maintenance branches.
branch: [ main, 6.1.x, 5.8.x ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Dispatch
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run update-scheduled-release-version.yml -r ${{ matrix.branch }}
@@ -1,22 +0,0 @@
name: Trigger Dependabot Auto Merge Forward
on:
push:
branches:
- '*.x'
permissions: read-all
jobs:
trigger-worflow:
name: Trigger Workflow
runs-on: ubuntu-latest
if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- id: trigger
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run dependabot-auto-merge-forward.yml -r main
@@ -1,13 +0,0 @@
name: Update Scheduled Release Version
on:
workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main.
permissions:
contents: read
jobs:
update-scheduled-release-version:
name: Update Scheduled Release Version
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@v1
secrets: inherit
@@ -63,6 +63,7 @@ class EclipsePlugin implements Plugin<Project> {
// test sources. Relax those from error to warning
properties['org.eclipse.jdt.core.circularClasspath'] = 'warning'
properties['org.eclipse.jdt.core.incompleteClasspath'] = 'warning'
properties['org.eclipse.jdt.core.compiler.codegen.methodParameters'] = 'generate'
}
}
}
@@ -16,15 +16,23 @@
package org.springframework.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.gradle.api.DefaultTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.VerificationException;
import java.io.IOException;
import java.nio.file.Files;
/**
* @author Marcus da Coregio
@@ -36,38 +44,60 @@ public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
TaskProvider<CheckExpectedBranchVersionTask> checkExpectedBranchVersionTask = project.getTasks().register("checkExpectedBranchVersion", CheckExpectedBranchVersionTask.class, (task) -> {
task.setGroup("Build");
task.setDescription("Check if the project version matches the branch version");
task.onlyIf("skipCheckExpectedBranchVersion property is false or not present", CheckExpectedBranchVersionPlugin::skipPropertyFalseOrNotPresent);
task.getVersion().convention(project.provider(() -> project.getVersion().toString()));
task.getBranchName().convention(project.getProviders().exec(execSpec -> execSpec.setCommandLine("git", "symbolic-ref", "--short", "HEAD")).getStandardOutput().getAsText());
task.getOutputFile().convention(project.getLayout().getBuildDirectory().file("check-expected-branch-version"));
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkExpectedBranchVersionTask));
}
public static class CheckExpectedBranchVersionTask extends DefaultTask {
private static boolean skipPropertyFalseOrNotPresent(Task task) {
return task.getProject()
.getProviders()
.gradleProperty("skipCheckExpectedBranchVersion")
.orElse("false")
.map("false"::equalsIgnoreCase)
.get();
}
@CacheableTask
public static abstract class CheckExpectedBranchVersionTask extends DefaultTask {
@Input
abstract Property<String> getVersion();
@Input
abstract Property<String> getBranchName();
@OutputFile
abstract RegularFileProperty getOutputFile();
@TaskAction
public void run() throws IOException {
Project project = getProject();
if (project.hasProperty("skipCheckExpectedBranchVersion")) {
return;
}
String version = (String) project.getVersion();
String branchVersion = getBranchVersion(project);
public void run() {
String version = getVersion().get();
String branchVersion = getBranchName().map(String::trim).get();
if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) {
System.out.println("Branch version does not match *.x, ignoring");
String msg = String.format("Branch version [%s] does not match *.x, ignoring", branchVersion);
getLogger().warn(msg);
writeExpectedVersionOutput(msg);
return;
}
if (!versionsMatch(version, branchVersion)) {
throw new IllegalStateException(String.format("Project version [%s] does not match branch version [%s]. " +
"Please verify that the branch contains the right version.", version, branchVersion));
String msg = String.format("Project version [%s] does not match branch version [%s]. " +
"Please verify that the branch contains the right version.", version, branchVersion);
writeExpectedVersionOutput(msg);
throw new VerificationException(msg);
}
writeExpectedVersionOutput(version);
}
private static String getBranchVersion(Project project) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
project.exec((exec) -> {
exec.commandLine("git", "symbolic-ref", "--short", "HEAD");
exec.setErrorOutput(System.err);
exec.setStandardOutput(baos);
});
return baos.toString();
private void writeExpectedVersionOutput(String fileContent) {
try {
Files.writeString(getOutputFile().get().getAsFile().toPath(), fileContent);
} catch (IOException e) {
throw new TaskExecutionException(this, e);
}
}
@@ -19,52 +19,81 @@ package org.springframework.security.convention.versions;
import org.gradle.api.DefaultTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
import org.gradle.api.artifacts.VersionCatalog;
import org.gradle.api.artifacts.VersionCatalogsExtension;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.VerificationException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Optional;
public class VerifyDependenciesVersionsPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
VersionCatalog versionCatalog = project.getExtensions().getByType(VersionCatalogsExtension.class).named("libs");
Optional<Provider<MinimalExternalModuleDependency>> oauth2OidcSdk = versionCatalog.findLibrary("com-nimbusds-oauth2-oidc-sdk");
Optional<Provider<MinimalExternalModuleDependency>> nimbusJoseJwt = versionCatalog.findLibrary("com-nimbusds-nimbus-jose-jwt");
if (oauth2OidcSdk.isEmpty()) {
throw new VerificationException("Library [com-nimbusds-oauth2-oidc-sdk] does not exist in the version catalog named libs.");
}
if (nimbusJoseJwt.isEmpty()) {
throw new VerificationException("Library [com-nimbusds-nimbus-jose-jwt] does not exist in the version catalog named libs.");
}
TaskProvider<VerifyDependenciesVersionsTask> verifyDependenciesVersionsTaskProvider = project.getTasks().register("verifyDependenciesVersions", VerifyDependenciesVersionsTask.class, (task) -> {
task.setGroup("Verification");
task.setDescription("Verify that specific dependencies are using the same version");
VersionCatalog versionCatalog = project.getExtensions().getByType(VersionCatalogsExtension.class).named("libs");
MinimalExternalModuleDependency oauth2OidcSdk = versionCatalog.findLibrary("com-nimbusds-oauth2-oidc-sdk").get().get();
MinimalExternalModuleDependency nimbusJoseJwt = versionCatalog.findLibrary("com-nimbusds-nimbus-jose-jwt").get().get();
task.setOauth2OidcSdkVersion(oauth2OidcSdk.getVersion());
task.setExpectedNimbusJoseJwtVersion(nimbusJoseJwt.getVersion());
task.getOauth2OidcSdkVersion().convention(oauth2OidcSdk.get().map(Dependency::getVersion));
task.getExpectedNimbusJoseJwtVersion().convention(nimbusJoseJwt.get().map(Dependency::getVersion));
task.getOutputFile().convention(project.getLayout().getBuildDirectory().file("verify-dependencies-versions"));
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(verifyDependenciesVersionsTaskProvider));
}
public static class VerifyDependenciesVersionsTask extends DefaultTask {
@CacheableTask
public abstract static class VerifyDependenciesVersionsTask extends DefaultTask {
private String oauth2OidcSdkVersion;
@Input
abstract Property<String> getOauth2OidcSdkVersion();
private String expectedNimbusJoseJwtVersion;
@Input
abstract Property<String> getExpectedNimbusJoseJwtVersion();
public void setOauth2OidcSdkVersion(String oauth2OidcSdkVersion) {
this.oauth2OidcSdkVersion = oauth2OidcSdkVersion;
}
public void setExpectedNimbusJoseJwtVersion(String expectedNimbusJoseJwtVersion) {
this.expectedNimbusJoseJwtVersion = expectedNimbusJoseJwtVersion;
}
@OutputFile
abstract RegularFileProperty getOutputFile();
@TaskAction
public void verify() {
String transitiveNimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(this.oauth2OidcSdkVersion);
if (!transitiveNimbusJoseJwtVersion.equals(this.expectedNimbusJoseJwtVersion)) {
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, but the project contains a different version of nimbus-jose-jwt [%s]. Please align the versions.", transitiveNimbusJoseJwtVersion, this.oauth2OidcSdkVersion, this.expectedNimbusJoseJwtVersion);
throw new IllegalStateException(message);
public void verify() {
String oauth2OidcSdkVersion = this.getOauth2OidcSdkVersion().get();
String transitiveNimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(oauth2OidcSdkVersion);
String expectedNimbusJoseJwtVersion = this.getExpectedNimbusJoseJwtVersion().get();
if (!transitiveNimbusJoseJwtVersion.equals(expectedNimbusJoseJwtVersion)) {
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, but the project contains a different version of nimbus-jose-jwt [%s]. Please align the versions.", transitiveNimbusJoseJwtVersion, oauth2OidcSdkVersion, expectedNimbusJoseJwtVersion);
throw new VerificationException(message);
}
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, the project contains expected version of nimbus-jose-jwt [%s]. Verified all versions align.", transitiveNimbusJoseJwtVersion, oauth2OidcSdkVersion, expectedNimbusJoseJwtVersion);
try {
Files.writeString(getOutputFile().get().getAsFile().toPath(), message);
} catch (IOException e) {
throw new TaskExecutionException(this, e);
}
}
}
}
@@ -18,6 +18,7 @@ package org.springframework.security.cas.userdetails;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apereo.cas.client.validation.Assertion;
@@ -73,7 +74,8 @@ public final class GrantedAuthorityFromAssertionAttributesUserDetailsService
}
private SimpleGrantedAuthority createSimpleGrantedAuthority(Object o) {
return new SimpleGrantedAuthority(this.convertToUpperCase ? o.toString().toUpperCase() : o.toString());
return new SimpleGrantedAuthority(
this.convertToUpperCase ? o.toString().toUpperCase(Locale.ROOT) : o.toString());
}
/**
@@ -509,7 +509,8 @@ public abstract class AbstractRequestMatcherRegistry<C> {
public boolean matches(HttpServletRequest request) {
String name = request.getHttpServletMapping().getServletName();
ServletRegistration registration = this.servletContext.getServletRegistration(name);
Assert.notNull(registration, computeErrorMessage(this.servletContext.getServletRegistrations().values()));
Assert.notNull(registration,
() -> computeErrorMessage(this.servletContext.getServletRegistrations().values()));
try {
Class<?> clazz = Class.forName(registration.getClassName());
return DispatcherServlet.class.isAssignableFrom(clazz);
@@ -171,7 +171,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
Assert.state(this.mappingCount > 0,
"At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())");
ObservationRegistry registry = getObservationRegistry();
RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build());
AuthorizationManager<HttpServletRequest> manager = postProcess(
(AuthorizationManager<HttpServletRequest>) this.managerBuilder.build());
if (registry.isNoop()) {
return manager;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
@@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
Assert.hasText(issuer, "Provider issuer cannot be null");
this.issuer = issuer;
}
@Override
@@ -21,6 +21,7 @@ import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.ApplicationContext;
@@ -33,6 +34,8 @@ import org.springframework.security.web.reactive.result.view.CsrfRequestDataValu
import org.springframework.security.web.server.ObservationWebFilterChainDecorator;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.result.view.AbstractView;
@@ -79,11 +82,14 @@ class WebFluxSecurityConfiguration {
@Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
@Order(WEB_FILTER_CHAIN_FILTER_ORDER)
WebFilterChainProxy springSecurityWebFilterChainFilter() {
WebFilterChainProxy springSecurityWebFilterChainFilter(ObjectProvider<ServerWebExchangeFirewall> firewall,
ObjectProvider<ServerExchangeRejectedHandler> rejectedHandler) {
WebFilterChainProxy proxy = new WebFilterChainProxy(getSecurityWebFilterChains());
if (!this.observationRegistry.isNoop()) {
proxy.setFilterChainDecorator(new ObservationWebFilterChainDecorator(this.observationRegistry));
}
firewall.ifUnique(proxy::setFirewall);
rejectedHandler.ifUnique(proxy::setExchangeRejectedHandler);
return proxy;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.config.http;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.ServletRequest;
@@ -313,7 +314,7 @@ class HttpConfigurationBuilder {
// Needed to account for placeholders
static String createPath(String path, boolean lowerCase) {
return lowerCase ? path.toLowerCase() : path;
return lowerCase ? path.toLowerCase(Locale.ENGLISH) : path;
}
BeanMetadataElement getSecurityContextHolderStrategyForAuthenticationFilters() {
@@ -147,6 +147,7 @@ final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
.addConstructorArgValue(logoutRequestResolver)
.addPropertyValue("logoutRequestRepository", logoutRequestRepository)
.getBeanDefinition();
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
.addConstructorArgValue(saml2LogoutRequestSuccessHandler)
@@ -0,0 +1,65 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.ClassUtils;
/**
* Utility methods for {@link HttpMessageConverter}'s.
*
* @author Joe Grandja
* @author luamas
* @since 5.1
*/
final class HttpMessageConverters {
private static final boolean jackson2Present;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
static {
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
}
private HttpMessageConverters() {
}
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
if (jackson2Present) {
return new MappingJackson2HttpMessageConverter();
}
if (gsonPresent) {
return new GsonHttpMessageConverter();
}
if (jsonbPresent) {
return new JsonbHttpMessageConverter();
}
return null;
}
}
@@ -0,0 +1,95 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageEncoder;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.MimeType;
class OAuth2ErrorEncoder implements HttpMessageEncoder<OAuth2Error> {
private final HttpMessageConverter<Object> messageConverter = HttpMessageConverters.getJsonMessageConverter();
@NotNull
@Override
public List<MediaType> getStreamingMediaTypes() {
return List.of();
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
return getEncodableMimeTypes().contains(mimeType);
}
@NotNull
@Override
public Flux<DataBuffer> encode(Publisher<? extends OAuth2Error> error, DataBufferFactory bufferFactory,
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
return Mono.from(error).flatMap((data) -> {
ByteArrayHttpOutputMessage bytes = new ByteArrayHttpOutputMessage();
try {
this.messageConverter.write(data, MediaType.APPLICATION_JSON, bytes);
return Mono.just(bytes.getBody().toByteArray());
}
catch (IOException ex) {
return Mono.error(ex);
}
}).map(bufferFactory::wrap).flux();
}
@NotNull
@Override
public List<MimeType> getEncodableMimeTypes() {
return List.of(MediaType.APPLICATION_JSON);
}
private static class ByteArrayHttpOutputMessage implements HttpOutputMessage {
private final ByteArrayOutputStream body = new ByteArrayOutputStream();
@NotNull
@Override
public ByteArrayOutputStream getBody() {
return this.body;
}
@NotNull
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
@@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
Assert.hasText(issuer, "Provider issuer cannot be null");
this.issuer = issuer;
}
@Override
@@ -16,16 +16,17 @@
package org.springframework.security.config.web.server;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
@@ -62,6 +63,9 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
new OAuth2ErrorEncoder());
/**
* Construct an {@link OidcBackChannelLogoutWebFilter}
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
@@ -84,7 +88,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
})
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap(this.authenticationManager::authenticate)
@@ -93,7 +97,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
})
.flatMap((authentication) -> {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
@@ -101,19 +105,12 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
});
}
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
private Mono<Void> handleAuthenticationFailure(ServerWebExchange exchange, Exception ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
OAuth2Error error = oauth2Error(ex);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
return this.errorHttpMessageConverter.write(Mono.just(oauth2Error(ex)), ResolvableType.forClass(Object.class),
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
exchange.getResponse(), Collections.emptyMap());
}
private OAuth2Error oauth2Error(Exception ex) {
@@ -16,8 +16,8 @@
package org.springframework.security.config.web.server;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -25,14 +25,15 @@ import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
@@ -44,6 +45,7 @@ import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -63,6 +65,9 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
new OAuth2ErrorEncoder());
private WebClient web = WebClient.create();
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
@@ -97,7 +102,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
totalCount.intValue()));
}
if (!list.isEmpty()) {
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
return handleLogoutFailure(exchange.getExchange(), oauth2Error(list));
}
else {
return Mono.empty();
@@ -148,17 +153,11 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
private Mono<Void> handleLogoutFailure(ServerWebExchange exchange, OAuth2Error error) {
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
return this.errorHttpMessageConverter.write(Mono.just(error), ResolvableType.forClass(Object.class),
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
exchange.getResponse(), Collections.emptyMap());
}
/**
@@ -73,6 +73,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
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;
/**
@@ -447,6 +449,18 @@ public class PrePostMethodSecurityConfigurationTests {
.autowire();
}
// gh-15651
@Test
@WithMockUser(roles = "ADMIN")
public void adviseWhenPrePostEnabledThenEachInterceptorRunsExactlyOnce() {
this.spring.register(MethodSecurityServiceConfig.class, CustomMethodSecurityExpressionHandlerConfig.class)
.autowire();
MethodSecurityExpressionHandler expressionHandler = this.spring.getContext()
.getBean(MethodSecurityExpressionHandler.class);
this.methodSecurityService.manyAnnotations(new ArrayList<>(Arrays.asList("harold", "jonathan", "tim", "bo")));
verify(expressionHandler, times(4)).createEvaluationContext(any(Supplier.class), any());
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
@@ -523,6 +537,19 @@ public class PrePostMethodSecurityConfigurationTests {
}
@Configuration
@EnableMethodSecurity
static class CustomMethodSecurityExpressionHandlerConfig {
private final MethodSecurityExpressionHandler expressionHandler = spy(
new DefaultMethodSecurityExpressionHandler());
@Bean
MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
return this.expressionHandler;
}
}
@EnableMethodSecurity
static class CustomPermissionEvaluatorConfig {
@@ -153,6 +153,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.spring.register(ObjectPostProcessorConfig.class).autowire();
ObjectPostProcessor objectPostProcessor = this.spring.getContext().getBean(ObjectPostProcessor.class);
verify(objectPostProcessor).postProcess(any(RequestMatcherDelegatingAuthorizationManager.class));
verify(objectPostProcessor).postProcess(any(AuthorizationManager.class));
verify(objectPostProcessor).postProcess(any(AuthorizationFilter.class));
}
@@ -650,7 +650,7 @@ public class OAuth2LoginConfigurerTests {
}
@Test
public void configureWhenOidcSessionStrategyThenUses() {
public void configureWhenOidcSessionRegistryThenUses() {
this.spring.register(OAuth2LoginWithOidcSessionRegistry.class).autowire();
OidcSessionRegistry registry = this.spring.getContext().getBean(OidcSessionRegistry.class);
this.spring.getContext().publishEvent(new HttpSessionDestroyedEvent(this.request.getSession()));
@@ -91,6 +91,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willThrow;
@@ -259,6 +260,22 @@ public class OidcLogoutConfigurerTests {
verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class));
}
@Test
void logoutWhenProviderIssuerMissingThenThrowIllegalArgumentException() throws Exception {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
.autowire();
String registrationId = this.clientRegistration.getRegistrationId();
MockHttpSession session = login();
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
assertThatIllegalArgumentException().isThrownBy(
() -> this.mvc.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
.param("logout_token", logoutToken)));
}
private MockHttpSession login() throws Exception {
MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher();
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
@@ -410,6 +427,54 @@ public class OidcLogoutConfigurerTests {
}
@Configuration
static class ProviderIssuerMissingRegistrationConfig {
@Autowired(required = false)
MockWebServer web;
@Bean
ClientRegistration clientRegistration() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
}
String issuer = this.web.url("/").toString();
return TestClientRegistrations.clientRegistration()
.issuerUri(null)
.jwkSetUri(issuer + "jwks")
.tokenUri(issuer + "token")
.userInfoUri(issuer + "user")
.scope("openid")
.build();
}
@Bean
ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
return new InMemoryClientRegistrationRepository(clientRegistration);
}
}
@Configuration
@EnableWebSecurity
@Import(ProviderIssuerMissingRegistrationConfig.class)
static class ProviderIssuerMissingConfig {
@Bean
@Order(1)
SecurityFilterChain filters(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
@@ -448,6 +513,9 @@ public class OidcLogoutConfigurerTests {
@Autowired
ClientRegistration registration;
@Autowired(required = false)
MockWebServer web;
@Bean
@Order(0)
SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception {
@@ -484,7 +552,7 @@ public class OidcLogoutConfigurerTests {
HttpSession session = request.getSession();
JwtEncoderParameters parameters = JwtEncoderParameters
.from(JwtClaimsSet.builder().id("id").subject(this.username)
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
.issuer(getIssuerUri()).issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
String token = this.encoder.encode(parameters).getTokenValue();
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
@@ -492,7 +560,7 @@ public class OidcLogoutConfigurerTests {
}
String idToken(String sessionId) {
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
.claim(LogoutTokenClaimNames.SID, sessionId).build();
@@ -501,6 +569,13 @@ public class OidcLogoutConfigurerTests {
return this.encoder.encode(parameters).getTokenValue();
}
private String getIssuerUri() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
}
return this.web.url("/").toString();
}
@GetMapping("/user")
Map<String, Object> userinfo() {
return Map.of("sub", this.username, "id", this.username);
@@ -484,6 +484,7 @@ public class Saml2LogoutConfigurerTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
@@ -16,14 +16,26 @@
package org.springframework.security.config.annotation.web.reactive;
import java.util.Collections;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.firewall.HttpStatusExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
@@ -47,6 +59,46 @@ public class WebFluxSecurityConfigurationTests {
assertThat(webFilterChainProxy).isNotNull();
}
@Test
void loadConfigWhenDefaultThenFirewalled() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
void loadConfigWhenCustomRejectedHandler() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class, CustomServerExchangeRejectedHandlerConfig.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode())
.isEqualTo(CustomServerExchangeRejectedHandlerConfig.EXPECTED_STATUS);
}
@Test
void loadConfigWhenFirewallBeanThenCustomized() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class, NoOpFirewallConfig.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode()).isNotEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void loadConfigWhenBeanProxyingEnabledAndSubclassThenWebFilterChainProxyExists() {
this.spring
@@ -57,6 +109,32 @@ public class WebFluxSecurityConfigurationTests {
assertThat(webFilterChainProxy).isNotNull();
}
private static @NotNull DefaultWebFilterChain emptyChain() {
return new DefaultWebFilterChain((webExchange) -> Mono.empty(), Collections.emptyList());
}
@Configuration
static class NoOpFirewallConfig {
@Bean
ServerWebExchangeFirewall noOpFirewall() {
return ServerWebExchangeFirewall.INSECURE_NOOP;
}
}
@Configuration
static class CustomServerExchangeRejectedHandlerConfig {
static HttpStatus EXPECTED_STATUS = HttpStatus.I_AM_A_TEAPOT;
@Bean
ServerExchangeRejectedHandler rejectedHandler() {
return new HttpStatusExchangeRejectedHandler(EXPECTED_STATUS);
}
}
@Configuration
static class SubclassConfig extends WebFluxSecurityConfiguration {
@@ -63,6 +63,7 @@ import org.springframework.web.util.UriUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
@@ -380,6 +381,22 @@ public class Saml2LogoutBeanDefinitionParserTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.configLocations(this.xml("CustomComponents")).autowire();
RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(this.rpLogoutRequest)
.id(this.rpLogoutRequestId)
.relayState(this.rpLogoutRequestRelayState)
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature))
.build();
given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest);
this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf()));
verify(getBean(Saml2LogoutRequestRepository.class)).saveLogoutRequest(eq(logoutRequest), any(), any());
}
private <T> T getBean(Class<T> clazz) {
return this.spring.getContext().getBean(clazz);
}
@@ -50,6 +50,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -97,6 +98,7 @@ import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
@@ -195,7 +197,10 @@ public class OidcLogoutSpecTests {
.body(BodyInserters.fromFormData("logout_token", "invalid"))
.exchange()
.expectStatus()
.isBadRequest();
.isBadRequest()
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
})
.value(hasValue("invalid_request"));
this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk();
}
@@ -262,9 +267,10 @@ public class OidcLogoutSpecTests {
.exchange()
.expectStatus()
.isBadRequest()
.expectBody(String.class)
.value(containsString("partial_logout"))
.value(containsString("not all sessions were terminated"));
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
})
.value(hasValue("partial_logout"))
.value(hasValue(containsString("not all sessions were terminated")));
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
}
@@ -323,6 +329,30 @@ public class OidcLogoutSpecTests {
verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class));
}
@Test
void logoutWhenProviderIssuerMissingThen5xxServerError() {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
.autowire();
String registrationId = this.clientRegistration.getRegistrationId();
String session = login();
String logoutToken = this.test.mutateWith(session(session))
.get()
.uri("/token/logout")
.exchange()
.expectStatus()
.isOk()
.returnResult(String.class)
.getResponseBody()
.blockFirst();
this.test.post()
.uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
.body(BodyInserters.fromFormData("logout_token", logoutToken))
.exchange()
.expectStatus()
.is5xxServerError();
this.test.mutateWith(session(session)).get().uri("/token/logout").exchange().expectStatus().isOk();
}
private String login() {
this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized();
String registrationId = this.clientRegistration.getRegistrationId();
@@ -499,6 +529,54 @@ public class OidcLogoutSpecTests {
}
@Configuration
static class ProviderIssuerMissingRegistrationConfig {
@Autowired(required = false)
MockWebServer web;
@Bean
ClientRegistration clientRegistration() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
}
String issuer = this.web.url("/").toString();
return TestClientRegistrations.clientRegistration()
.issuerUri(null)
.jwkSetUri(issuer + "jwks")
.tokenUri(issuer + "token")
.userInfoUri(issuer + "user")
.scope("openid")
.build();
}
@Bean
ReactiveClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}
}
@Configuration
@EnableWebFluxSecurity
@Import(ProviderIssuerMissingRegistrationConfig.class)
static class ProviderIssuerMissingConfig {
@Bean
@Order(1)
SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
.oauth2Login(Customizer.withDefaults())
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebFluxSecurity
@EnableWebFlux
@@ -537,6 +615,9 @@ public class OidcLogoutSpecTests {
@Autowired
ClientRegistration registration;
@Autowired(required = false)
MockWebServer web;
static ServerWebExchangeMatcher or(String... patterns) {
List<ServerWebExchangeMatcher> matchers = new ArrayList<>();
for (String pattern : patterns) {
@@ -581,7 +662,7 @@ public class OidcLogoutSpecTests {
Map<String, Object> accessToken(WebSession session) {
JwtEncoderParameters parameters = JwtEncoderParameters
.from(JwtClaimsSet.builder().id("id").subject(this.username)
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
.issuer(getIssuerUri()).issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
String token = this.encoder.encode(parameters).getTokenValue();
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
@@ -589,7 +670,7 @@ public class OidcLogoutSpecTests {
}
String idToken(String sessionId) {
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
.claim(LogoutTokenClaimNames.SID, sessionId).build();
@@ -598,6 +679,13 @@ public class OidcLogoutSpecTests {
return this.encoder.encode(parameters).getTokenValue();
}
private String getIssuerUri() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
}
return this.web.url("/").toString();
}
@GetMapping("/user")
Map<String, Object> userinfo() {
return Map.of("sub", this.username, "id", this.username);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.security.authentication;
import java.util.Locale;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
@@ -53,7 +55,7 @@ public final class AuthenticationObservationConvention
if (authenticationType.endsWith("Authentication")) {
authenticationType = authenticationType.substring(0, authenticationType.lastIndexOf("Authentication"));
}
return "authenticate " + authenticationType.toLowerCase();
return "authenticate " + authenticationType.toLowerCase(Locale.ENGLISH);
}
return "authenticate";
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -79,10 +79,10 @@ public class SimpleAttributes2GrantedAuthoritiesMapper
*/
private GrantedAuthority getGrantedAuthority(String attribute) {
if (isConvertAttributeToLowerCase()) {
attribute = attribute.toLowerCase(Locale.getDefault());
attribute = attribute.toLowerCase(Locale.ROOT);
}
else if (isConvertAttributeToUpperCase()) {
attribute = attribute.toUpperCase(Locale.getDefault());
attribute = attribute.toUpperCase(Locale.ROOT);
}
if (isAddPrefixIfAlreadyExisting() || !attribute.startsWith(getAttributePrefix())) {
return new SimpleGrantedAuthority(getAttributePrefix() + attribute);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.core.authority.mapping;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.springframework.beans.factory.InitializingBean;
@@ -71,10 +72,10 @@ public final class SimpleAuthorityMapper implements GrantedAuthoritiesMapper, In
private GrantedAuthority mapAuthority(String name) {
if (this.convertToUpperCase) {
name = name.toUpperCase();
name = name.toUpperCase(Locale.ROOT);
}
else if (this.convertToLowerCase) {
name = name.toLowerCase();
name = name.toLowerCase(Locale.ROOT);
}
if (this.prefix.length() > 0 && !name.startsWith(this.prefix)) {
name = this.prefix + name;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.core.userdetails;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -91,7 +92,7 @@ public class MapReactiveUserDetailsService implements ReactiveUserDetailsService
}
private String getKey(String username) {
return username.toLowerCase();
return username.toLowerCase(Locale.ROOT);
}
}
@@ -19,6 +19,7 @@ package org.springframework.security.core.userdetails.memory;
import java.beans.PropertyEditorSupport;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.util.StringUtils;
@@ -45,10 +46,10 @@ public class UserAttributeEditor extends PropertyEditorSupport {
userAttrib.setPassword(currentToken);
}
else {
if (currentToken.toLowerCase().equals("enabled")) {
if (currentToken.toLowerCase(Locale.ENGLISH).equals("enabled")) {
userAttrib.setEnabled(true);
}
else if (currentToken.toLowerCase().equals("disabled")) {
else if (currentToken.toLowerCase(Locale.ENGLISH).equals("disabled")) {
userAttrib.setEnabled(false);
}
else {
@@ -75,7 +75,6 @@ public final class SecurityJackson2Modules {
private static final List<String> securityJackson2ModuleClasses = Arrays.asList(
"org.springframework.security.jackson2.CoreJackson2Module",
"org.springframework.security.cas.jackson2.CasJackson2Module",
"org.springframework.security.web.jackson2.WebJackson2Module",
"org.springframework.security.web.server.jackson2.WebServerJackson2Module");
@@ -89,6 +88,8 @@ public final class SecurityJackson2Modules {
private static final String saml2Jackson2ModuleClass = "org.springframework.security.saml2.jackson2.Saml2Jackson2Module";
private static final String casJackson2ModuleClass = "org.springframework.security.cas.jackson2.CasJackson2Module";
private static final boolean webServletPresent;
private static final boolean oauth2ClientPresent;
@@ -99,6 +100,8 @@ public final class SecurityJackson2Modules {
private static final boolean saml2JacksonPresent;
private static final boolean casJacksonPresent;
static {
ClassLoader classLoader = SecurityJackson2Modules.class.getClassLoader();
webServletPresent = ClassUtils.isPresent("jakarta.servlet.http.Cookie", classLoader);
@@ -107,6 +110,7 @@ public final class SecurityJackson2Modules {
javaTimeJacksonPresent = ClassUtils.isPresent(javaTimeJackson2ModuleClass, classLoader);
ldapJacksonPresent = ClassUtils.isPresent(ldapJackson2ModuleClass, classLoader);
saml2JacksonPresent = ClassUtils.isPresent(saml2Jackson2ModuleClass, classLoader);
casJacksonPresent = ClassUtils.isPresent(casJackson2ModuleClass, classLoader);
}
private SecurityJackson2Modules() {
@@ -160,6 +164,9 @@ public final class SecurityJackson2Modules {
if (saml2JacksonPresent) {
addToModulesList(loader, modules, saml2Jackson2ModuleClass);
}
if (casJacksonPresent) {
addToModulesList(loader, modules, casJackson2ModuleClass);
}
return modules;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.security.provisioning;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
@@ -96,23 +97,23 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
@Override
public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
}
@Override
public void deleteUser(String username) {
this.users.remove(username.toLowerCase());
this.users.remove(username.toLowerCase(Locale.ROOT));
}
@Override
public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()), "user should exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
}
@Override
public boolean userExists(String username) {
return this.users.containsKey(username.toLowerCase());
return this.users.containsKey(username.toLowerCase(Locale.ROOT));
}
@Override
@@ -143,14 +144,14 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
MutableUserDetails mutableUser = this.users.get(username.toLowerCase(Locale.ROOT));
mutableUser.setPassword(newPassword);
return mutableUser;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = this.users.get(username.toLowerCase());
UserDetails user = this.users.get(username.toLowerCase(Locale.ROOT));
if (user == null) {
throw new UsernameNotFoundException(username);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.crypto.password;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Locale;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
@@ -50,11 +51,11 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
private static final String SSHA_PREFIX = "{SSHA}";
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase();
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase(Locale.ENGLISH);
private static final String SHA_PREFIX = "{SHA}";
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase();
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase(Locale.ENGLISH);
private BytesKeyGenerator saltGenerator;
+1 -1
View File
@@ -30,7 +30,7 @@ urls:
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
snapshot: true
runtime:
log:
+1
View File
@@ -153,6 +153,7 @@
*** xref:reactive/exploits/csrf.adoc[CSRF]
*** xref:reactive/exploits/headers.adoc[Headers]
*** xref:reactive/exploits/http.adoc[HTTP Requests]
*** xref:reactive/exploits/firewall.adoc[]
** Integrations
*** xref:reactive/integrations/cors.adoc[CORS]
*** xref:reactive/integrations/rsocket.adoc[RSocket]
@@ -0,0 +1,234 @@
[[webflux-serverwebexchangefirewall]]
= ServerWebExchangeFirewall
There are various ways a request can be created by malicious users that can exploit applications.
Spring Security provides the `ServerWebExchangeFirewall` to allow rejecting requests that look malicious.
The default implementation is `StrictServerWebExchangeFirewall` which rejects malicious requests.
For example a request could contain path-traversal sequences (such as `/../`) or multiple forward slashes (`//`) that could also cause pattern-matches to fail.
Some containers normalize these out before performing the servlet mapping, but others do not.
To protect against issues like these, `WebFilterChainProxy` uses a `ServerWebExchangeFirewall` strategy to check and wrap the request.
By default, un-normalized requests are automatically rejected, and path parameters are removed for matching purposes.
(So, for example, an original request path of `/secure;hack=1/somefile.html;hack=2` is returned as `/secure/somefile.html`.)
It is, therefore, essential that a `WebFilterChainProxy` is used.
In practice, we recommend that you use method security at your service layer, to control access to your application, rather than rely entirely on the use of security constraints defined at the web-application level.
URLs change, and it is difficult to take into account all the possible URLs that an application might support and how requests might be manipulated.
You should restrict yourself to using a few simple patterns that are simple to understand.
Always try to use a "`deny-by-default`" approach, where you have a catch-all wildcard (`/**` or `**`) defined last to deny access.
Security defined at the service layer is much more robust and harder to bypass, so you should always take advantage of Spring Security's method security options.
You can customize the `ServerWebExchangeFirewall` by exposing it as a Bean.
.Allow Matrix Variables
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowSemicolon(true);
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowSemicolon(true)
return firewall
}
----
======
To protect against https://www.owasp.org/index.php/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP Verb Tampering], the `StrictServerWebExchangeFirewall` provides an allowed list of valid HTTP methods that are allowed.
The default valid methods are `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, and `PUT`.
If your application needs to modify the valid methods, you can configure a custom `StrictServerWebExchangeFirewall` bean.
The following example allows only HTTP `GET` and `POST` methods:
.Allow Only GET & POST
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowedHttpMethods(listOf("GET", "POST"))
return firewall
}
----
======
If you must allow any HTTP method (not recommended), you can use `StrictServerWebExchangeFirewall.setUnsafeAllowAnyHttpMethod(true)`.
Doing so entirely disables validation of the HTTP method.
[[webflux-serverwebexchangefirewall-headers-parameters]]
`StrictServerWebExchangeFirewall` also checks header names and values and parameter names.
It requires that each character have a defined code point and not be a control character.
This requirement can be relaxed or adjusted as necessary by using the following methods:
* `StrictServerWebExchangeFirewall#setAllowedHeaderNames(Predicate)`
* `StrictServerWebExchangeFirewall#setAllowedHeaderValues(Predicate)`
* `StrictServerWebExchangeFirewall#setAllowedParameterNames(Predicate)`
[NOTE]
====
Parameter values can be also controlled with `setAllowedParameterValues(Predicate)`.
====
For example, to switch off this check, you can wire your `StrictServerWebExchangeFirewall` with `Predicate` instances that always return `true`:
.Allow Any Header Name, Header Value, and Parameter Name
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowedHeaderNames((header) -> true);
firewall.setAllowedHeaderValues((header) -> true);
firewall.setAllowedParameterNames((parameter) -> true);
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowedHeaderNames { true }
firewall.setAllowedHeaderValues { true }
firewall.setAllowedParameterNames { true }
return firewall
}
----
======
Alternatively, there might be a specific value that you need to allow.
For example, iPhone Xʀ uses a `User-Agent` that includes a character that is not in the ISO-8859-1 charset.
Due to this fact, some application servers parse this value into two separate characters, the latter being an undefined character.
You can address this with the `setAllowedHeaderValues` method:
.Allow Certain User Agents
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
Pattern allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
Pattern userAgent = ...;
firewall.setAllowedHeaderValues((header) -> allowed.matcher(header).matches() || userAgent.matcher(header).matches());
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
val allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*")
val userAgent = Pattern.compile(...)
firewall.setAllowedHeaderValues { allowed.matcher(it).matches() || userAgent.matcher(it).matches() }
return firewall
}
----
======
In the case of header values, you may instead consider parsing them as UTF-8 at verification time:
.Parse Headers As UTF-8
[tabs]
======
Java::
+
[source,java,role="primary"]
----
firewall.setAllowedHeaderValues((header) -> {
String parsed = new String(header.getBytes(ISO_8859_1), UTF_8);
return allowed.matcher(parsed).matches();
});
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
firewall.setAllowedHeaderValues {
val parsed = String(header.getBytes(ISO_8859_1), UTF_8)
return allowed.matcher(parsed).matches()
}
----
======
The `ServerExchangeRejectedHandler` interface is used to handle `ServerExchangeRejectedException` throw by Spring Security's `ServerWebExchangeFirewall`.
By default `HttpStatusExchangeRejectedHandler` is used to send an HTTP 400 response to clients when a request is rejected.
To customize the behavior, users can expose a `ServerExchangeRejectedHandler` Bean.
For example, the following will send an HTTP 404 when the request is rejected:
.Send 404 on Request Rejected
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ServerExchangeRejectedHandler rejectedHandler() {
return new HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND);
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun rejectedHandler(): ServerExchangeRejectedHandler {
return HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND)
}
----
======
Handling can be completely customized by creating a custom `ServerExchangeRejectedHandler` implementation.
@@ -17,7 +17,7 @@ Java::
[source,java,role="primary"]
----
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
@@ -32,7 +32,7 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("https://example.com")
configuration.allowedMethods = listOf("GET", "POST")
@@ -135,7 +135,7 @@ To enable this, you can stand up the Back-Channel Logout endpoint in the DSL lik
======
Java::
+
[source=java,role="primary"]
[source,java,role="primary"]
----
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
@@ -153,7 +153,7 @@ public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Except
Kotlin::
+
[source=kotlin,role="secondary"]
[source,kotlin,role="secondary"]
----
@Bean
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
@@ -187,7 +187,7 @@ Consider a `ClientRegistration` whose identifier is `registrationId`.
The overall flow for a Back-Channel logout is like this:
1. At login time, Spring Security correlates the ID Token, CSRF Token, and Provider Session ID (if any) to your application's session id in its `ReactiveOidcSessionStrategy` implementation.
1. At login time, Spring Security correlates the ID Token, CSRF Token, and Provider Session ID (if any) to your application's session id in its `ReactiveOidcSessionRegistry` implementation.
2. Then at logout time, your OIDC Provider makes an API call to `/logout/connect/back-channel/registrationId` including a Logout Token that indicates either the `sub` (the End User) or the `sid` (the Provider Session ID) to logout.
3. Spring Security validates the token's signature and claims.
4. If the token contains a `sid` claim, then only the Client's session that correlates to that provider session is terminated.
@@ -197,38 +197,38 @@ The overall flow for a Back-Channel logout is like this:
Remember that Spring Security's OIDC support is multi-tenant.
This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
=== Customizing the OIDC Provider Session Strategy
=== Customizing the OIDC Provider Session Registry
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
There are a number of circumstances, like a clustered application, where it would be nice to store this instead in a separate location, like a database.
You can achieve this by configuring a custom `ReactiveOidcSessionStrategy`, like so:
You can achieve this by configuring a custom `ReactiveOidcSessionRegistry`, like so:
[tabs]
======
Java::
+
[source=java,role="primary"]
[source,java,role="primary"]
----
@Component
public final class MySpringDataOidcSessionStrategy implements OidcSessionStrategy {
public final class MySpringDataOidcSessionRegistry implements ReactiveOidcSessionRegistry {
private final OidcProviderSessionRepository sessions;
// ...
@Override
public void saveSessionInformation(OidcSessionInformation info) {
this.sessions.save(info);
public Mono<void> saveSessionInformation(OidcSessionInformation info) {
return this.sessions.save(info);
}
@Override
public OidcSessionInformation(String clientSessionId) {
public Mono<OidcSessionInformation> removeSessionInformation(String clientSessionId) {
return this.sessions.removeByClientSessionId(clientSessionId);
}
@Override
public Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
public Flux<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
return token.getSessionId() != null ?
this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
this.sessions.removeBySubjectAndIssuerAndAudience(...);
@@ -238,10 +238,10 @@ public final class MySpringDataOidcSessionStrategy implements OidcSessionStrateg
Kotlin::
+
[source=kotlin,role="secondary"]
[source,kotlin,role="secondary"]
----
@Component
class MySpringDataOidcSessionStrategy: ReactiveOidcSessionStrategy {
class MySpringDataOidcSessionRegistry: ReactiveOidcSessionRegistry {
val sessions: OidcProviderSessionRepository
// ...
@@ -14,6 +14,7 @@ The following image shows the typical layering of the handlers for a single HTTP
.FilterChain
[[servlet-filterchain-figure]]
[.invert-dark]
image::{figures}/filterchain.png[]
The client sends a request to the application, and the container creates a `FilterChain`, which contains the `Filter` instances and `Servlet` that should process the `HttpServletRequest`, based on the path of the request URI.
@@ -66,6 +67,7 @@ Here is a picture of how `DelegatingFilterProxy` fits into the <<servlet-filters
.DelegatingFilterProxy
[[servlet-delegatingfilterproxy-figure]]
[.invert-dark]
image::{figures}/delegatingfilterproxy.png[]
`DelegatingFilterProxy` looks up __Bean Filter~0~__ from the `ApplicationContext` and then invokes __Bean Filter~0~__.
@@ -113,6 +115,7 @@ The following image shows the role of `FilterChainProxy`.
.FilterChainProxy
[[servlet-filterchainproxy-figure]]
[.invert-dark]
image::{figures}/filterchainproxy.png[]
[[servlet-securityfilterchain]]
@@ -124,6 +127,7 @@ The following image shows the role of `SecurityFilterChain`.
.SecurityFilterChain
[[servlet-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/securityfilterchain.png[]
The <<servlet-security-filters,Security Filters>> in `SecurityFilterChain` are typically Beans, but they are registered with `FilterChainProxy` instead of <<servlet-delegatingfilterproxy>>.
@@ -145,6 +149,7 @@ The following image shows multiple `SecurityFilterChain` instances:
.Multiple SecurityFilterChain
[[servlet-multi-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/multi-securityfilterchain.png[]
In the <<servlet-multi-securityfilterchain-figure>> figure, `FilterChainProxy` decides which `SecurityFilterChain` should be used.
@@ -398,6 +403,7 @@ The {security-api-url}org/springframework/security/web/access/ExceptionTranslati
The following image shows the relationship of `ExceptionTranslationFilter` to other components:
[.invert-dark]
image::{figures}/exceptiontranslationfilter.png[]
@@ -22,6 +22,7 @@ This also gives a good idea of the high level flow of authentication and how pie
At the heart of Spring Security's authentication model is the `SecurityContextHolder`.
It contains the <<servlet-authentication-securitycontext>>.
[.invert-dark]
image::{figures}/securitycontextholder.png[]
The `SecurityContextHolder` is where Spring Security stores the details of who is xref:features/authentication/index.adoc#authentication[authenticated].
@@ -175,6 +176,7 @@ While the implementation of `AuthenticationManager` could be anything, the most
Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide.
If none of the configured `AuthenticationProvider` instances can authenticate, authentication fails with a `ProviderNotFoundException`, which is a special `AuthenticationException` that indicates that the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it.
[.invert-dark]
image::{figures}/providermanager.png[]
In practice each `AuthenticationProvider` knows how to perform a specific type of authentication.
@@ -184,11 +186,13 @@ This lets each `AuthenticationProvider` do a very specific type of authenticatio
`ProviderManager` also allows configuring an optional parent `AuthenticationManager`, which is consulted in the event that no `AuthenticationProvider` can perform authentication.
The parent can be any type of `AuthenticationManager`, but it is often an instance of `ProviderManager`.
[.invert-dark]
image::{figures}/providermanager-parent.png[]
In fact, multiple `ProviderManager` instances might share the same parent `AuthenticationManager`.
This is somewhat common in scenarios where there are multiple xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] instances that have some authentication in common (the shared parent `AuthenticationManager`), but also different authentication mechanisms (the different `ProviderManager` instances).
[.invert-dark]
image::{figures}/providermanagers-parent.png[]
[[servlet-authentication-providermanager-erasing-credentials]]
@@ -234,6 +238,7 @@ Before the credentials can be authenticated, Spring Security typically requests
Next, the `AbstractAuthenticationProcessingFilter` can authenticate any authentication requests that are submitted to it.
[.invert-dark]
image::{figures}/abstractauthenticationprocessingfilter.png[]
image:{icondir}/number_1.png[] When the user submits their credentials, the `AbstractAuthenticationProcessingFilter` creates an <<servlet-authentication-authentication,`Authentication`>> from the `HttpServletRequest` to be authenticated.
@@ -9,6 +9,7 @@ This section describes how HTTP Basic Authentication works within Spring Securit
First, we see the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/basicauthenticationentrypoint.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -26,6 +27,7 @@ The following image shows the flow for the username and password being processed
[[servlet-authentication-basicauthenticationfilter]]
.Authenticating Username and Password
[.invert-dark]
image::{figures}/basicauthenticationfilter.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -8,6 +8,7 @@ This section examines how `DaoAuthenticationProvider` works within Spring Securi
The following figure explains the workings of the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section.
.`DaoAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/daoauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section passes a `UsernamePasswordAuthenticationToken` to the `AuthenticationManager`, which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -10,6 +10,7 @@ This section examines how form-based login works within Spring Security.
First, we see how the user is redirected to the login form:
.Redirecting to the Login Page
[.invert-dark]
image::{figures}/loginurlauthenticationentrypoint.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -30,6 +31,7 @@ When the username and password are submitted, the `UsernamePasswordAuthenticatio
The `UsernamePasswordAuthenticationFilter` extends xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[AbstractAuthenticationProcessingFilter], so the following diagram should look pretty similar:
.Authenticating Username and Password
[.invert-dark]
image::{figures}/usernamepasswordauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -326,156 +326,7 @@ Normally, Spring Security builds an `AuthenticationManager` internally composed
In certain cases, it may still be desired to customize the instance of `AuthenticationManager` used by Spring Security.
For example, you may need to simply disable xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager-erasing-credentials[credential erasure] for cached users.
The recommended way to do this is to simply publish your own `AuthenticationManager` bean, and Spring Security will use it.
You can publish an `AuthenticationManager` using the following configuration:
.Publish `AuthenticationManager` bean for Spring Security
[tabs]
=====
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
----
XML::
+
[source,xml,role="secondary"]
----
<http>
<intercept-url pattern="/login" access="permitAll"/>
<intercept-url pattern="/**" access="authenticated"/>
<form-login />
<http-basic />
<bean id="authenticationManager"
class="org.springframework.security.authentication.ProviderManager">
<constructor-arg>
<bean class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService" />
<property name="passwordEncoder" ref="passwordEncoder" />
</bean>
</constructor-arg>
</bean>
<user-service id="userDetailsService">
<user name="user"
password="{noop}password"
authorities="ROLE_USER" />
</user-service>
<bean id="passwordEncoder"
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
</http>
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/login", permitAll)
authorize(anyRequest, authenticated)
}
formLogin { }
httpBasic { }
}
return http.build()
}
@Bean
fun authenticationManager(
userDetailsService: UserDetailsService,
passwordEncoder: PasswordEncoder): AuthenticationManager {
val authenticationProvider = DaoAuthenticationProvider()
authenticationProvider.setUserDetailsService(userDetailsService)
authenticationProvider.setPasswordEncoder(passwordEncoder)
val providerManager = ProviderManager(authenticationProvider)
providerManager.eraseCredentialsAfterAuthentication = false
return providerManager
}
@Bean
fun userDetailsService(): UserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
}
----
=====
Alternatively, you can take advantage of the fact that the `AuthenticationManagerBuilder` used to build Spring Security's global `AuthenticationManager` is published as a bean.
To do this, you can take advantage of the fact that the `AuthenticationManagerBuilder` used to build Spring Security's global `AuthenticationManager` is published as a bean.
You can configure the builder as follows:
.Configure global `AuthenticationManagerBuilder`
@@ -539,3 +390,142 @@ class SecurityConfig {
}
----
=====
Alternatively, you may configure a local `AuthenticationManager` to override the global one.
.Configure local `AuthenticationManager` for Spring Security
[tabs]
=====
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authenticationManager(authenticationManager());
return http.build();
}
private AuthenticationManager authenticationManager() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}
private UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
private PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
----
XML::
+
[source,xml,role="secondary"]
----
<http authentication-manager-ref="authenticationManager">
<intercept-url pattern="/**" access="authenticated"/>
<form-login />
<http-basic />
<bean id="authenticationManager"
class="org.springframework.security.authentication.ProviderManager">
<constructor-arg>
<bean class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService" />
<property name="passwordEncoder" ref="passwordEncoder" />
</bean>
</constructor-arg>
</bean>
<user-service id="userDetailsService">
<user name="user"
password="{noop}password"
authorities="ROLE_USER" />
</user-service>
<bean id="passwordEncoder"
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
</http>
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
formLogin { }
httpBasic { }
authenticationManager = authenticationManager()
}
return http.build()
}
@Bean
fun authenticationManager(): AuthenticationManager {
val authenticationProvider = DaoAuthenticationProvider()
authenticationProvider.setUserDetailsService(userDetailsService())
authenticationProvider.setPasswordEncoder(passwordEncoder())
val providerManager = ProviderManager(authenticationProvider)
providerManager.eraseCredentialsAfterAuthentication = false
return providerManager
}
private fun userDetailsService(): UserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
private fun passwordEncoder(): PasswordEncoder {
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
}
----
=====
@@ -191,6 +191,7 @@ In Spring Security 6, the example shown above is the default configuration.
The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextpersistencefilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -212,6 +213,7 @@ To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `
The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextholderfilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -101,7 +101,7 @@ The `AuthorizationManager` interface contains two methods:
----
AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
default void verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
// ...
}
@@ -126,6 +126,7 @@ For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` a
[[authz-authorization-manager-implementations]]
.Authorization Manager Implementations
[.invert-dark]
image::{figures}/authorizationhierarchy.png[]
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
@@ -345,6 +346,7 @@ The following image shows the `AccessDecisionManager` interface:
[[authz-access-voting]]
.Voting Decision Manager
[.invert-dark]
image::{figures}/access-decision-voting.png[]
By using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
@@ -405,6 +407,7 @@ For example, on the Spring web site, you can find a https://spring.io/blog/2009/
[[authz-after-invocation]]
.After Invocation Implementation
[.invert-dark]
image::{figures}/after-invocation.png[]
Like many other parts of Spring Security, `AfterInvocationManager` has a single concrete implementation, `AfterInvocationProviderManager`, which polls a list of ``AfterInvocationProvider``s.
@@ -65,6 +65,7 @@ In many cases, your authorization rules will be more sophisticated than that, so
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works at the request level in Servlet-based applications.
.Authorize HttpServletRequest
[.invert-dark]
image::{figures}/authorizationfilter.png[]
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` constructs a `Supplier` that retrieves an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
@@ -114,6 +114,7 @@ open class MyCustomerService {
A given invocation to `MyCustomerService#readCustomer` may look something like this when Method Security <<activate-method-security,is activated>>:
[.invert-dark]
image::{figures}/methodsecurity.png[]
1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PreAuthorize` pointcut>>
@@ -82,6 +82,7 @@ To learn more about CSRF protection for your application, consider the following
CSRF protection is provided by several components that are composed within the {security-api-url}org/springframework/security/web/csrf/CsrfFilter.html[`CsrfFilter`]:
.`CsrfFilter` Components
[.invert-dark]
image::{figures}/csrf.png[]
CSRF protection is divided into two parts:
@@ -90,6 +91,7 @@ CSRF protection is divided into two parts:
2. Determine if the request requires CSRF protection, load and validate the token, and <<csrf-access-denied-handler,handle `AccessDeniedException`>>.
.`CsrfFilter` Processing
[.invert-dark]
image::{figures}/csrf-processing.png[]
* image:{icondir}/number_1.png[] First, the {security-api-url}org/springframework/security/web/csrf/DeferredCsrfToken.html[`DeferredCsrfToken`] is loaded, which holds a reference to the <<csrf-token-repository,`CsrfTokenRepository`>> so that the persisted `CsrfToken` can be loaded later (in image:{icondir}/number_4.png[]).
@@ -16,7 +16,7 @@ Java::
[source,java,role="primary"]
----
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
@@ -31,7 +31,7 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("https://example.com")
configuration.allowedMethods = listOf("GET", "POST")
@@ -147,7 +147,7 @@ public class WebSecurityConfig {
return http.build();
}
CorsConfigurationSource apiConfigurationSource() {
UrlBasedCorsConfigurationSource apiConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://api.example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
@@ -156,7 +156,7 @@ public class WebSecurityConfig {
return source;
}
CorsConfigurationSource myWebsiteConfigurationSource() {
UrlBasedCorsConfigurationSource myWebsiteConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
@@ -173,7 +173,7 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("https://example.com")
configuration.allowedMethods = listOf("GET", "POST")
@@ -205,6 +205,78 @@ This will ensure that:
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
[[migrating-spel-expressions]]
=== Migrating SpEL Expressions
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
However, to ease migration, you can also use a class like the following:
[source,java]
----
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
private Expression expression;
public MessageExpressionAuthorizationManager(String expressionString) {
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
return new ExpressionAuthorizationDecision(granted, this.expression);
}
}
----
And specify an instance for each matcher that you cannot get migrate:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
// ...
.simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
// ...
return messages.build();
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
messages
// ..
.simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
// ...
return messages.build()
}
}
----
======
[[websocket-authorization-notes]]
=== WebSocket Authorization Notes
@@ -134,7 +134,7 @@ To enable this, you can stand up the Back-Channel Logout endpoint in the DSL lik
======
Java::
+
[source=java,role="primary"]
[source,java,role="primary"]
----
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -152,7 +152,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Kotlin::
+
[source=kotlin,role="secondary"]
[source,kotlin,role="secondary"]
----
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
@@ -176,7 +176,7 @@ Then, you need a way listen to events published by Spring Security to remove old
======
Java::
+
[source=java,role="primary"]
[source,java,role="primary"]
----
@Bean
public HttpSessionEventPublisher sessionEventPublisher() {
@@ -186,7 +186,7 @@ public HttpSessionEventPublisher sessionEventPublisher() {
Kotlin::
+
[source=kotlin,role="secondary"]
[source,kotlin,role="secondary"]
----
@Bean
open fun sessionEventPublisher(): HttpSessionEventPublisher {
@@ -213,7 +213,7 @@ Consider a `ClientRegistration` whose identifier is `registrationId`.
The overall flow for a Back-Channel logout is like this:
1. At login time, Spring Security correlates the ID Token, CSRF Token, and Provider Session ID (if any) to your application's session id in its `OidcSessionStrategy` implementation.
1. At login time, Spring Security correlates the ID Token, CSRF Token, and Provider Session ID (if any) to your application's session id in its `OidcSessionRegistry` implementation.
2. Then at logout time, your OIDC Provider makes an API call to `/logout/connect/back-channel/registrationId` including a Logout Token that indicates either the `sub` (the End User) or the `sid` (the Provider Session ID) to logout.
3. Spring Security validates the token's signature and claims.
4. If the token contains a `sid` claim, then only the Client's session that correlates to that provider session is terminated.
@@ -223,22 +223,22 @@ The overall flow for a Back-Channel logout is like this:
Remember that Spring Security's OIDC support is multi-tenant.
This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
=== Customizing the OIDC Provider Session Strategy
=== Customizing the OIDC Provider Session Registry
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
There are a number of circumstances, like a clustered application, where it would be nice to store this instead in a separate location, like a database.
You can achieve this by configuring a custom `OidcSessionStrategy`, like so:
You can achieve this by configuring a custom `OidcSessionRegistry`, like so:
[tabs]
======
Java::
+
[source=java,role="primary"]
[source,java,role="primary"]
----
@Component
public final class MySpringDataOidcSessionStrategy implements OidcSessionStrategy {
public final class MySpringDataOidcSessionRegistry implements OidcSessionRegistry {
private final OidcProviderSessionRepository sessions;
// ...
@@ -249,7 +249,7 @@ public final class MySpringDataOidcSessionStrategy implements OidcSessionStrateg
}
@Override
public OidcSessionInformation(String clientSessionId) {
public OidcSessionInformation removeSessionInformation(String clientSessionId) {
return this.sessions.removeByClientSessionId(clientSessionId);
}
@@ -264,10 +264,10 @@ public final class MySpringDataOidcSessionStrategy implements OidcSessionStrateg
Kotlin::
+
[source=kotlin,role="secondary"]
[source,kotlin,role="secondary"]
----
@Component
class MySpringDataOidcSessionStrategy: OidcSessionStrategy {
class MySpringDataOidcSessionRegistry: OidcSessionRegistry {
val sessions: OidcProviderSessionRepository
// ...
@@ -21,6 +21,7 @@ Now we can consider how Bearer Token Authentication works within Spring Security
First, we see that, as with xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/bearerauthenticationentrypoint.png[]
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -38,6 +39,7 @@ The following image shows the flow for the bearer token being processed:
[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
.Authenticating Bearer Token
[.invert-dark]
image::{figures}/bearertokenauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -89,12 +89,13 @@ Next, let's see the architectural components that Spring Security uses to suppor
{security-api-url}org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.html[`JwtAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <<oauth2resourceserver-jwt-decoder,`JwtDecoder`>> and <<oauth2resourceserver-jwt-authorization-extraction,`JwtAuthenticationConverter`>> to authenticate a JWT.
Let's take a look at how `JwtAuthenticationProvider` works within Spring Security.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> works.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
.`JwtAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/jwtauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `JwtAuthenticationProvider`.
@@ -85,12 +85,13 @@ Next, let's see the architectural components that Spring Security uses to suppor
{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[`OpaqueTokenAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <<oauth2resourceserver-opaque-introspector,`OpaqueTokenIntrospector`>> to authenticate an opaque token.
Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring Security.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> works.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
.`OpaqueTokenAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/opaquetokenauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `OpaqueTokenAuthenticationProvider`.
@@ -7,6 +7,7 @@ First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes
It does this through a series of redirects:
.Redirecting to Asserting Party Authentication
[.invert-dark]
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
[NOTE]
@@ -34,6 +35,7 @@ The following image shows how Spring Security authenticates a `<saml2:Response>`
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
.Authenticating a `<saml2:Response>`
[.invert-dark]
image::{figures}/saml2webssoauthenticationfilter.png[]
[NOTE]
+3 -3
View File
@@ -1,10 +1,10 @@
{
"dependencies": {
"antora": "3.2.0-alpha.6",
"antora": "3.2.0-alpha.8",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-beta.2",
"@antora/collector-extension": "1.0.1",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.12"
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
}
}
@@ -40,4 +40,8 @@
<!-- Lambdas that we can't replace with a method reference because a closure is required -->
<suppress files="BearerTokenAuthenticationFilter\.java" checks="SpringLambda"/>
<!-- Ignore String.toUpperCase() and String.toLowerCase() checks in tests -->
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toLowerCaseWithoutLocale"/>
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toUpperCaseWithoutLocale"/>
</suppressions>
+16
View File
@@ -30,5 +30,21 @@
<property name="message" value="Please use assertThatExceptionOfType." />
<property name="ignoreComments" value="true" />
</module>
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
<property name="id" value="toLowerCaseWithoutLocale"/>
<property name="format" value="\.toLowerCase\(\)"/>
<property name="maximum" value="0"/>
<property name="message"
value="String.toLowerCase() should be String.toLowerCase(Locale.ROOT) or String.toLowerCase(Locale.ENGLISH)"/>
<property name="ignoreComments" value="true"/>
</module>
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
<property name="id" value="toUpperCaseWithoutLocale"/>
<property name="format" value="\.toUpperCase\(\)"/>
<property name="maximum" value="0"/>
<property name="message"
value="String.toUpperCase() should be String.toUpperCase(Locale.ROOT) or String.toUpperCase(Locale.ENGLISH)"/>
<property name="ignoreComments" value="true"/>
</module>
</module>
</module>
+1
View File
@@ -10,6 +10,7 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
+1 -1
View File
@@ -1,5 +1,5 @@
springBootVersion=3.1.1
version=6.2.6
version=6.2.9-SNAPSHOT
samplesBranch=6.2.x
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+10 -10
View File
@@ -8,12 +8,12 @@ org-apache-directory-server = "1.5.5"
org-apache-maven-resolver = "1.9.22"
org-aspectj = "1.9.22.1"
org-bouncycastle = "1.70"
org-eclipse-jetty = "11.0.22"
org-eclipse-jetty = "11.0.24"
org-jetbrains-kotlin = "1.9.25"
org-jetbrains-kotlinx = "1.7.3"
org-mockito = "5.5.0"
org-opensaml = "4.3.2"
org-springframework = "6.1.12"
org-springframework = "6.1.15"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14"
@@ -26,9 +26,9 @@ com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebser
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
commons-collections = "commons-collections:commons-collections:3.2.2"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.9"
io-mockk = "io.mockk:mockk:1.13.12"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.9"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.13"
io-mockk = "io.mockk:mockk:1.13.13"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.13"
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
@@ -40,7 +40,7 @@ jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
jakarta-servlet-jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:6.0.0"
jakarta-servlet-jsp-jakarta-servlet-jsp-api = "jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.1"
jakarta-servlet-jsp-jstl-jakarta-servlet-jsp-jstl-api = "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.1"
jakarta-servlet-jsp-jstl-jakarta-servlet-jsp-jstl-api = "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2"
jakarta-websocket-jakarta-websocket-api = { module = "jakarta.websocket:jakarta.websocket-api", version.ref = "jakarta-websocket" }
jakarta-websocket-jakarta-websocket-client-api = { module = "jakarta.websocket:jakarta.websocket-client-api", version.ref = "jakarta-websocket" }
jakarta-xml-bind-jakarta-xml-bind-api = "jakarta.xml.bind:jakarta.xml.bind-api:4.0.2"
@@ -68,11 +68,11 @@ org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", ve
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.3.2.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.3"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
org-junit-junit-bom = "org.junit:junit-bom:5.10.3"
org-junit-junit-bom = "org.junit:junit-bom:5.10.5"
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
org-opensaml-opensaml-core = { module = "org.opensaml:opensaml-core", version.ref = "org-opensaml" }
org-opensaml-opensaml-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml" }
@@ -84,8 +84,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.9"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.6"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.12"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.8"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+5 -2
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
Vendored
+2
View File
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -1,5 +1,5 @@
/*
* Copyright 2005-2010 the original author or authors.
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.security.ldap;
import java.util.Locale;
import org.springframework.ldap.BadLdapGrammarException;
/**
@@ -72,7 +74,7 @@ final class LdapEncoder {
}
protected static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase();
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
return (raw.length() > 1) ? raw : "0" + raw;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2005-2010 the original author or authors.
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.security.ldap.authentication;
import java.util.Locale;
import org.springframework.ldap.BadLdapGrammarException;
/**
@@ -72,7 +74,7 @@ final class LdapEncoder {
}
protected static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase();
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
return (raw.length() > 1) ? raw : "0" + raw;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -144,9 +145,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
this.url = url;
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null;
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase(Locale.ROOT) : null;
}
/**
@@ -155,7 +156,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
this.url = url;
this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null;
}
@@ -350,7 +351,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
}
String createBindPrincipal(String username) {
if (this.domain == null || username.toLowerCase().endsWith(this.domain)) {
if (this.domain == null || username.toLowerCase(Locale.ROOT).endsWith(this.domain)) {
return username;
}
return username + "@" + this.domain;
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -179,7 +180,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
return null;
}
if (this.convertToUpperCase) {
role = role.toUpperCase();
role = role.toUpperCase(Locale.ROOT);
}
return new SimpleGrantedAuthority(this.rolePrefix + role);
};
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
@@ -124,7 +125,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
NamingEnumeration<?> ne = roleAttr.getAll();
Object group = ne.next();
String role = group.toString();
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase());
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase(Locale.ROOT));
};
private String[] attributesToRetrieve;
@@ -289,7 +290,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
@Deprecated
protected DistinguishedName buildGroupDn(String group) {
DistinguishedName dn = new DistinguishedName(this.groupSearchBase);
dn.add(this.groupRoleAttributeName, group.toLowerCase());
dn.add(this.groupRoleAttributeName, group.toLowerCase(Locale.ROOT));
return dn;
}
@@ -17,6 +17,7 @@
package org.springframework.security.ldap.userdetails;
import java.util.Collection;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -127,7 +128,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
protected GrantedAuthority createAuthority(Object role) {
if (role instanceof String) {
if (this.convertToUpperCase) {
role = ((String) role).toUpperCase();
role = ((String) role).toUpperCase(Locale.ROOT);
}
return new SimpleGrantedAuthority(this.rolePrefix + role);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.ldap.userdetails;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -193,7 +194,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
}
for (String role : roles) {
if (isConvertToUpperCase()) {
role = role.toUpperCase();
role = role.toUpperCase(Locale.ROOT);
}
role = getRolePrefix() + role;
// if the group already exist, we will not search for it's parents again.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -80,7 +80,13 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
if (registration == null) {
return null;
}
return (T) this.authorizedClients.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients
.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
if (cachedAuthorizedClient == null) {
return null;
}
return (T) new OAuth2AuthorizedClient(registration, cachedAuthorizedClient.getPrincipalName(),
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,8 +62,15 @@ public final class InMemoryReactiveOAuth2AuthorizedClientService implements Reac
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return (Mono<T>) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
.mapNotNull((clientRegistration) -> {
OAuth2AuthorizedClientId id = new OAuth2AuthorizedClientId(clientRegistrationId, principalName);
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients.get(id);
if (cachedAuthorizedClient == null) {
return null;
}
return new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(),
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
});
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
@@ -539,7 +540,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
// @formatter:off
return Stream.of(wwwAuthenticateHeader)
.filter((header) -> StringUtils.hasLength(header))
.filter((header) -> header.toLowerCase().startsWith("bearer"))
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
.map((header) -> header.substring("bearer".length()))
.map((header) -> header.split(","))
.flatMap(Stream::of)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -654,7 +655,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
private Map<String, String> parseAuthParameters(String wwwAuthenticateHeader) {
// @formatter:off
return Stream.of(wwwAuthenticateHeader).filter((header) -> StringUtils.hasLength(header))
.filter((header) -> header.toLowerCase().startsWith("bearer"))
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
.map((header) -> header.substring("bearer".length()))
.map((header) -> header.split(","))
.flatMap(Stream::of)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,22 +18,25 @@ package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatObject;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link InMemoryOAuth2AuthorizedClientService}.
@@ -79,9 +82,11 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
@Test
public void constructorWhenAuthorizedClientsProvidedThenUseProvidedAuthorizedClients() {
String registrationId = this.registration3.getRegistrationId();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration3, this.principalName1,
mock(OAuth2AccessToken.class));
Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients = Collections.singletonMap(
new OAuth2AuthorizedClientId(this.registration3.getRegistrationId(), this.principalName1),
mock(OAuth2AuthorizedClient.class));
authorizedClient);
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(eq(registrationId))).willReturn(this.registration3);
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
@@ -124,7 +129,35 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
}
@Test
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnAuthorizedClientWithUpdatedClientRegistration() {
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.registration1)
.clientSecret("updated secret")
.build();
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(this.registration1.getRegistrationId()))
.willReturn(this.registration1, updatedRegistration);
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
clientRegistrationRepository);
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.registration1,
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
authorizedClientService.saveAuthorizedClient(cachedAuthorizedClient,
new TestingAuthenticationToken(this.principalName1, null));
OAuth2AuthorizedClient authorizedClientWithUpdatedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
OAuth2AuthorizedClient firstLoadedClient = authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
OAuth2AuthorizedClient secondLoadedClient = authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
assertThat(firstLoadedClient).satisfies(isEqualTo(cachedAuthorizedClient));
assertThat(secondLoadedClient).satisfies(isEqualTo(authorizedClientWithUpdatedRegistration));
}
@Test
@@ -148,7 +181,7 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
.loadAuthorizedClient(this.registration3.getRegistrationId(), this.principalName2);
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
}
@Test
@@ -180,4 +213,38 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
assertThat(loadedAuthorizedClient).isNull();
}
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
return (actual) -> {
assertThat(actual).isNotNull();
assertThat(actual.getClientRegistration().getRegistrationId())
.isEqualTo(expected.getClientRegistration().getRegistrationId());
assertThat(actual.getClientRegistration().getClientName())
.isEqualTo(expected.getClientRegistration().getClientName());
assertThat(actual.getClientRegistration().getRedirectUri())
.isEqualTo(expected.getClientRegistration().getRedirectUri());
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
assertThat(actual.getClientRegistration().getClientId())
.isEqualTo(expected.getClientRegistration().getClientId());
assertThat(actual.getClientRegistration().getClientSecret())
.isEqualTo(expected.getClientRegistration().getClientSecret());
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
if (expected.getRefreshToken() != null) {
assertThat(actual.getRefreshToken()).isNotNull();
assertThat(actual.getRefreshToken().getTokenValue())
.isEqualTo(expected.getRefreshToken().getTokenValue());
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
assertThat(actual.getRefreshToken().getExpiresAt())
.isEqualTo(expected.getRefreshToken().getExpiresAt());
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,12 +18,14 @@ package org.springframework.security.oauth2.client;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -34,7 +36,9 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
@@ -56,8 +60,9 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
private Authentication principal = new TestingAuthenticationToken(this.principalName, "notused");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", Instant.now(),
Instant.now().plus(Duration.ofDays(1)));
private OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;
// @formatter:off
private ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(this.clientRegistrationId)
@@ -79,6 +84,11 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
public void setup() {
this.authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(
this.clientRegistrationRepository);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofDays(1));
this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt);
this.refreshToken = new OAuth2RefreshToken("refresh", issuedAt, expiresAt);
}
@Test
@@ -153,11 +163,37 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
.saveAuthorizedClient(authorizedClient, this.principal)
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
StepVerifier.create(saveAndLoad)
.expectNext(authorizedClient)
.assertNext(isEqualTo(authorizedClient))
.verifyComplete();
// @formatter:on
}
@Test
@SuppressWarnings("unchecked")
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnsAuthorizedClientWithUpdatedClientRegistration() {
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.clientRegistration)
.clientSecret("updated secret")
.build();
given(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId))
.willReturn(Mono.just(this.clientRegistration), Mono.just(updatedRegistration));
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.clientRegistration,
this.principalName, this.accessToken, this.refreshToken);
OAuth2AuthorizedClient authorizedClientWithChangedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
this.principalName, this.accessToken, this.refreshToken);
Flux<OAuth2AuthorizedClient> saveAndLoadTwice = this.authorizedClientService
.saveAuthorizedClient(cachedAuthorizedClient, this.principal)
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
.concatWith(
this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
StepVerifier.create(saveAndLoadTwice)
.assertNext(isEqualTo(cachedAuthorizedClient))
.assertNext(isEqualTo(authorizedClientWithChangedRegistration))
.verifyComplete();
}
@Test
public void saveAuthorizedClientWhenAuthorizedClientNullThenIllegalArgumentException() {
OAuth2AuthorizedClient authorizedClient = null;
@@ -246,4 +282,38 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
// @formatter:on
}
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
return (actual) -> {
assertThat(actual).isNotNull();
assertThat(actual.getClientRegistration().getRegistrationId())
.isEqualTo(expected.getClientRegistration().getRegistrationId());
assertThat(actual.getClientRegistration().getClientName())
.isEqualTo(expected.getClientRegistration().getClientName());
assertThat(actual.getClientRegistration().getRedirectUri())
.isEqualTo(expected.getClientRegistration().getRedirectUri());
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
assertThat(actual.getClientRegistration().getClientId())
.isEqualTo(expected.getClientRegistration().getClientId());
assertThat(actual.getClientRegistration().getClientSecret())
.isEqualTo(expected.getClientRegistration().getClientSecret());
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
if (expected.getRefreshToken() != null) {
assertThat(actual.getRefreshToken()).isNotNull();
assertThat(actual.getRefreshToken().getTokenValue())
.isEqualTo(expected.getRefreshToken().getTokenValue());
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
assertThat(actual.getRefreshToken().getExpiresAt())
.isEqualTo(expected.getRefreshToken().getExpiresAt());
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -440,7 +440,21 @@ public final class OAuth2AuthorizationRequest implements Serializable {
Map<String, Object> parameters = getParameters(); // Not encoded
this.parametersConsumer.accept(parameters);
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
parameters.forEach((k, v) -> queryParams.set(encodeQueryParam(k), encodeQueryParam(String.valueOf(v)))); // Encoded
parameters.forEach((k, v) -> {
String key = encodeQueryParam(k);
if (v instanceof Iterable) {
((Iterable<?>) v).forEach((value) -> queryParams.add(key, encodeQueryParam(String.valueOf(value))));
}
else if (v != null && v.getClass().isArray()) {
Object[] values = (Object[]) v;
for (Object value : values) {
queryParams.add(key, encodeQueryParam(String.valueOf(value)));
}
}
else {
queryParams.set(key, encodeQueryParam(String.valueOf(v)));
}
});
UriBuilder uriBuilder = this.uriBuilderFactory.uriString(this.authorizationUri).queryParams(queryParams);
return this.authorizationRequestUriFunction.apply(uriBuilder).toString();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.core.endpoint;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
@@ -319,4 +320,49 @@ public class OAuth2AuthorizationRequestTests {
+ "item%20amount=19.95%E2%82%AC&%C3%A2ge=4%C2%BD&item%20name=H%C3%85M%C3%96");
}
@Test
public void buildWhenAdditionalParametersContainsArrayThenProperlyEncoded() {
Map<String, Object> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("item1", new String[] { "1", "2" });
additionalParameters.put("item2", "value2");
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
.additionalParameters(additionalParameters)
.build();
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
assertThat(authorizationRequest.getAuthorizationRequestUri())
.isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&"
+ "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&"
+ "item1=1&item1=2&item2=value2");
}
@Test
public void buildWhenAdditionalParametersContainsIterableThenProperlyEncoded() {
Map<String, Object> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("item1", Arrays.asList("1", "2"));
additionalParameters.put("item2", "value2");
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
.additionalParameters(additionalParameters)
.build();
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
assertThat(authorizationRequest.getAuthorizationRequestUri())
.isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&"
+ "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&"
+ "item1=1&item1=2&item2=value2");
}
@Test
public void buildWhenAdditionalParametersContainsNullThenAuthorizationRequestUriContainsNull() {
Map<String, Object> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("item1", null);
additionalParameters.put("item2", "value2");
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
.additionalParameters(additionalParameters)
.build();
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
assertThat(authorizationRequest.getAuthorizationRequestUri())
.isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&"
+ "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&"
+ "item1=null&item2=value2");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -57,13 +57,18 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
? resolveFromRequestParameters(request) : null;
if (authorizationHeaderToken != null) {
if (parameterToken != null) {
final BearerTokenError error = BearerTokenErrors
BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
if (!StringUtils.hasText(parameterToken)) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
}
return null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -78,6 +78,11 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
if (!StringUtils.hasText(parameterToken)) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
}
return null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,8 +21,11 @@ import java.util.Base64;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -258,4 +261,35 @@ public class DefaultBearerTokenResolverTests {
assertThat(this.resolver.resolve(request)).isNull();
}
@Test
public void resolveWhenQueryParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
this.resolver.setAllowUriQueryParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.addParameter("access_token", "");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.resolver.resolve(request))
.withMessageContaining("The requested token parameter is an empty string")
.satisfies((e) -> {
BearerTokenError error = (BearerTokenError) e.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
}
@Test
public void resolveWhenFormParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
this.resolver.setAllowFormEncodedBodyParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", "");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.resolver.resolve(request))
.withMessageContaining("The requested token parameter is an empty string")
.satisfies((e) -> {
BearerTokenError error = (BearerTokenError) e.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -187,9 +187,9 @@ public class ServerBearerTokenAuthenticationConverterTests {
.isThrownBy(() -> convertToToken(request))
.satisfies((ex) -> {
BearerTokenError error = (BearerTokenError) ex.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_TOKEN);
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
// @formatter:on
}
@@ -1,5 +1,5 @@
/*
* Copyright 2004-2022 the original author or authors.
* Copyright 2004-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.security.taglibs.authz;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.ServletContext;
@@ -169,7 +170,7 @@ public abstract class AbstractAuthorizeTag {
}
public void setMethod(String method) {
this.method = (method != null) ? method.toUpperCase() : null;
this.method = (method != null) ? method.toUpperCase(Locale.ENGLISH) : null;
}
private SecurityContext getContext() {
@@ -16,6 +16,8 @@
package org.springframework.security.web;
import java.util.Locale;
import jakarta.servlet.ServletRequest;
import org.springframework.util.Assert;
@@ -45,7 +47,7 @@ public class PortResolverImpl implements PortResolver {
@Override
public int getServerPort(ServletRequest request) {
int serverPort = request.getServerPort();
String scheme = request.getScheme().toLowerCase();
String scheme = request.getScheme().toLowerCase(Locale.ENGLISH);
Integer mappedPort = getMappedPort(serverPort, scheme);
return (mappedPort != null) ? mappedPort : serverPort;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -97,8 +98,18 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
this.cookieCustomizer.accept(cookieBuilder);
Cookie cookie = mapToCookie(cookieBuilder.build());
response.addCookie(cookie);
ResponseCookie responseCookie = cookieBuilder.build();
if (!StringUtils.hasLength(responseCookie.getSameSite())) {
Cookie cookie = mapToCookie(responseCookie);
response.addCookie(cookie);
}
else if (request.getServletContext().getMajorVersion() > 5) {
Cookie cookie = mapToCookie(responseCookie);
response.addCookie(cookie);
}
else {
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
}
// Set request attribute to signal that response has blank cookie value,
// which allows loadToken to return null when token has been removed
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -73,6 +73,9 @@ public class CookieRequestCache implements RequestCache {
return null;
}
String originalURI = decodeCookie(savedRequestCookie.getValue());
if (originalURI == null) {
return null;
}
UriComponents uriComponents = UriComponentsBuilder.fromUriString(originalURI).build();
DefaultSavedRequest.Builder builder = new DefaultSavedRequest.Builder();
int port = getPort(uriComponents);
@@ -122,8 +125,14 @@ public class CookieRequestCache implements RequestCache {
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
}
private static String decodeCookie(String encodedCookieValue) {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
private String decodeCookie(String encodedCookieValue) {
try {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
catch (IllegalArgumentException ex) {
this.logger.debug("Failed decode cookie value " + encodedCookieValue);
return null;
}
}
private static String getCookiePath(HttpServletRequest request) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,11 +20,15 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import jakarta.servlet.FilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.server.firewall.HttpStatusExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedException;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.security.web.server.firewall.StrictServerWebExchangeFirewall;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
@@ -43,6 +47,10 @@ public class WebFilterChainProxy implements WebFilter {
private WebFilterChainDecorator filterChainDecorator = new DefaultWebFilterChainDecorator();
private ServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
private ServerExchangeRejectedHandler exchangeRejectedHandler = new HttpStatusExchangeRejectedHandler();
public WebFilterChainProxy(List<SecurityWebFilterChain> filters) {
this.filters = filters;
}
@@ -53,14 +61,41 @@ public class WebFilterChainProxy implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.firewall.getFirewalledExchange(exchange)
.flatMap((firewalledExchange) -> filterFirewalledExchange(firewalledExchange, chain))
.onErrorResume(ServerExchangeRejectedException.class,
(rejected) -> this.exchangeRejectedHandler.handle(exchange, rejected).then(Mono.empty()));
}
private Mono<Void> filterFirewalledExchange(ServerWebExchange firewalledExchange, WebFilterChain chain) {
return Flux.fromIterable(this.filters)
.filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(exchange))
.filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(firewalledExchange))
.next()
.switchIfEmpty(
Mono.defer(() -> this.filterChainDecorator.decorate(chain).filter(exchange).then(Mono.empty())))
.switchIfEmpty(Mono
.defer(() -> this.filterChainDecorator.decorate(chain).filter(firewalledExchange).then(Mono.empty())))
.flatMap((securityWebFilterChain) -> securityWebFilterChain.getWebFilters().collectList())
.map((filters) -> this.filterChainDecorator.decorate(chain, filters))
.flatMap((securedChain) -> securedChain.filter(exchange));
.flatMap((securedChain) -> securedChain.filter(firewalledExchange));
}
/**
* Protects the application using the provided
* {@link StrictServerWebExchangeFirewall}.
* @param firewall the {@link StrictServerWebExchangeFirewall} to use. Cannot be null.
* @since 6.4
*/
public void setFirewall(ServerWebExchangeFirewall firewall) {
Assert.notNull(firewall, "firewall cannot be null");
this.firewall = firewall;
}
/**
* Handles {@link ServerExchangeRejectedException} when the
* {@link ServerWebExchangeFirewall} rejects the provided {@link ServerWebExchange}.
* @param exchangeRejectedHandler the {@link ServerExchangeRejectedHandler} to use.
*/
public void setExchangeRejectedHandler(ServerExchangeRejectedHandler exchangeRejectedHandler) {
this.exchangeRejectedHandler = exchangeRejectedHandler;
}
/**
@@ -0,0 +1,66 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.server.firewall;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
/**
* A simple implementation of {@link ServerExchangeRejectedHandler} that sends an error
* with configurable status code.
*
* @author Rob Winch
* @since 6.4
*/
public class HttpStatusExchangeRejectedHandler implements ServerExchangeRejectedHandler {
private static final Log logger = LogFactory.getLog(HttpStatusExchangeRejectedHandler.class);
private final HttpStatus status;
/**
* Constructs an instance which uses {@code 400} as response code.
*/
public HttpStatusExchangeRejectedHandler() {
this(HttpStatus.BAD_REQUEST);
}
/**
* Constructs an instance which uses a configurable http code as response.
* @param status http status code to use
*/
public HttpStatusExchangeRejectedHandler(HttpStatus status) {
this.status = status;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange,
ServerExchangeRejectedException serverExchangeRejectedException) {
return Mono.fromRunnable(() -> {
logger.debug(
LogMessage.format("Rejecting request due to: %s", serverExchangeRejectedException.getMessage()),
serverExchangeRejectedException);
exchange.getResponse().setStatusCode(this.status);
});
}
}

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