1
0
mirror of synced 2026-08-04 17:27:13 +00:00

Compare commits

...

138 Commits

Author SHA1 Message Date
Rob Winch b56a9666c8 Revert "Mark targetDomainObject as @Nullable in PermissionEvaluator"
This reverts commit 9f1381c382.
2026-02-24 09:40:13 -06:00
Andrey Litvitski 9f1381c382 Mark targetDomainObject as @Nullable in PermissionEvaluator
Closes: gh-18259

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-02-24 08:27:44 -06:00
dependabot[bot] f700aeac0f Bump tools.jackson:jackson-bom from 3.0.4 to 3.1.0
Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.0.4 to 3.1.0.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.0.4...jackson-bom-3.1.0)

---
updated-dependencies:
- dependency-name: tools.jackson:jackson-bom
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-24 03:17:58 +00:00
dependabot[bot] 41a8d6aca5 Bump org-apache-maven-resolver from 1.9.26 to 1.9.27
Bumps `org-apache-maven-resolver` from 1.9.26 to 1.9.27.

Updates `org.apache.maven.resolver:maven-resolver-connector-basic` from 1.9.26 to 1.9.27
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.26...maven-resolver-1.9.27)

Updates `org.apache.maven.resolver:maven-resolver-impl` from 1.9.26 to 1.9.27
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.26...maven-resolver-1.9.27)

Updates `org.apache.maven.resolver:maven-resolver-transport-http` from 1.9.26 to 1.9.27

---
updated-dependencies:
- dependency-name: org.apache.maven.resolver:maven-resolver-connector-basic
  dependency-version: 1.9.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-impl
  dependency-version: 1.9.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-transport-http
  dependency-version: 1.9.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-24 03:16:37 +00:00
Rob Winch e81c64b94d Merge Update servlet/architecture.adoc to use include-code
Update servlet/architecture.adoc to use include-code
2026-02-23 17:16:28 -06:00
Robert Winch 0c394696ce Fix servlet/architecture.adoc disable Sample
- Switch `include-java` (does not exist) to `include-code`
- Update kotlin to have the `disable` tag
- Update to suppress deprecation use for User builder (allowed for samples)

Signed-off-by: Robert Winch <362503+rwinch@users.noreply.github.com>
2026-02-23 17:03:17 -06:00
Joe Kuhel 62d1bc86e3 Update servlet architecture docs to use include-code
Also update antora.xml to include-xml in docs/src/test/resources

Signed-off-by: Joe Kuhel <4983938+jkuhel@users.noreply.github.com>
2026-02-23 16:37:16 -06:00
Rob Winch a4cadb5cc5 Merge Make PublicKeyCredentialCreationOptions Serializable
Make PublicKeyCredentialCreationOptions Serializable
2026-02-23 16:01:34 -06:00
Robert Winch 701736da5d Fix checkstyle
Issue gh-18354

Signed-off-by: Robert Winch <362503+rwinch@users.noreply.github.com>
2026-02-23 15:43:55 -06:00
Mohammad Amin Pahlevani 9e5a425859 Make PublicKeyCredentialCreationOptions Serializable
Closes gh-16431

Signed-off-by: Mohammad Amin Pahlevani <pahlevani@live.com>
2026-02-23 15:43:40 -06:00
Rob Winch 1ab17d941a Merge Improve error message for missing access attribute in intercept-url
Improve error message for missing access attribute in intercept-url
2026-02-23 15:31:34 -06:00
Robert Winch 53300be8d7 Fix checkstyle
Issue gh-18530
2026-02-23 15:16:02 -06:00
CHANHAN d5ba9dcada Add tests for intercept-url access attribute validation
Fixes gh-18503

Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com>
2026-02-23 15:16:02 -06:00
CHANHAN fa87c78edb fix missing access attribute validation in FilterInvocationSecurityMetadataSourceParser
Fixes gh-18503

Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com>
2026-02-23 15:16:02 -06:00
CHANHAN f1e367f93d fix missing access attribute validation in AuthorizationFilterParser
Fixes gh-18503

Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com>
2026-02-23 15:16:02 -06:00
Rob Winch 4d0627e6c0 Merge pull request #18721 from coehgns/main
Add tests for PathPatternRequestMatcher request path caching
2026-02-23 11:58:27 -06:00
Rob Winch 3106f2be7b Merge pull request #18757 from wonderfulrosemari/gh-4265-csrf-multipart-header
Document multipart CSRF header option
2026-02-23 11:51:54 -06:00
Rob Winch b3e5f09eb3 Merge Document Keberose Dependency Coordinates
Document Keberose Dependency Coordinates
2026-02-23 11:47:06 -06:00
Robert Winch e1436c39f0 Merge Document Keberose Dependency Coordinates 2026-02-23 11:33:25 -06:00
Robert Winch 311235f39e Document Keberose Dependency Coordinates
Closes gh-18773
2026-02-23 11:32:37 -06:00
Robert Winch fec988c82d Add Kerberos Migration Section
This links to the updated dependency coordinates

Issue gh-18773

Signed-off-by: Robert Winch <362503+rwinch@users.noreply.github.com>
2026-02-23 11:29:50 -06:00
busoco-sjb 17b434c1c1 Document the change in dependency coordinates with Spring Security 7
Signed-off-by: busoco-sjb <169069865+busoco-sjb@users.noreply.github.com>
2026-02-23 11:21:59 -06:00
Rob Winch b451739b5c Merge pull request Fix GrantedAuthority.authority null in AuthoritiesAuthorizationManager
Fix GrantedAuthority.authority null in AuthoritiesAuthorizationManager
2026-02-23 11:17:21 -06:00
Rob Winch 0bb65411be Merge pull request Fix GrantedAuthority.authority null in AuthoritiesAuthorizationManager
Fix GrantedAuthority.authority null in AuthoritiesAuthorizationManager
2026-02-23 11:17:06 -06:00
Robert Winch cfb3bf38d8 Merge Fix: Handle null authority string in AuthoritiesAuthorizationManager into main 2026-02-23 10:54:00 -06:00
Robert Winch 151bcf3b0b Merge Fix: Handle null authority string in AuthoritiesAuthorizationManager into 7.0.x 2026-02-23 10:53:40 -06:00
Robert Winch 1116241ee3 Fix Checks for NullPointerException in AuthoritiesAuthorizationManager
- Fix checkstyle
- Fix the test to use Collection that throws NullPointerException on .contains(null) to replicate the reported issue

Closes gh-18544

Signed-off-by: Robert Winch <362503+rwinch@users.noreply.github.com>
2026-02-23 10:47:11 -06:00
Khyojae d87dc9ae57 Fix: Handle null authority string in AuthoritiesAuthorizationManager
This prevents NPE when GrantedAuthority.getAuthority() returns null. Closes gh-18543

Signed-off-by: Khyojae <khjae201@gmail.com>
2026-02-23 09:30:28 -06:00
Robert Winch ea1b3d819b Merge branch '7.0.x' 2026-02-23 08:17:27 -06:00
Robert Winch 2eb948d9b5 Ensure tests clear AuthorizationServerContextHolder
Closes gh-18768
2026-02-23 08:17:02 -06:00
Robert Winch 881ddf796a Merge branch '7.0.x' 2026-02-23 08:13:58 -06:00
Robert Winch f2aef5168c Merge branch '6.5.x' into 7.0.x 2026-02-23 08:13:38 -06:00
dependabot[bot] ac556a45f9 Bump org.hibernate.orm:hibernate-core from 6.6.42.Final to 6.6.43.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.42.Final to 6.6.43.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.43/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.42...6.6.43)

---
updated-dependencies:
- dependency-name: org.hibernate.orm:hibernate-core
  dependency-version: 6.6.43.Final
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 08:12:23 -06:00
dependabot[bot] c8731a8dc0 Bump com.fasterxml.jackson:jackson-bom from 2.18.5 to 2.18.6
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.5 to 2.18.6.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.5...jackson-bom-2.18.6)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  dependency-version: 2.18.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 08:12:09 -06:00
dependabot[bot] d62cce5bfb Bump com.fasterxml.jackson:jackson-bom from 2.21.0 to 2.21.1
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.21.0 to 2.21.1.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.21.0...jackson-bom-2.21.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  dependency-version: 2.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 03:19:50 +00:00
dependabot[bot] eca68b6cb3 Bump org.seleniumhq.selenium:selenium-java from 4.40.0 to 4.41.0
Bumps [org.seleniumhq.selenium:selenium-java](https://github.com/SeleniumHQ/selenium) from 4.40.0 to 4.41.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.40.0...selenium-4.41.0)

---
updated-dependencies:
- dependency-name: org.seleniumhq.selenium:selenium-java
  dependency-version: 4.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 03:18:09 +00:00
dependabot[bot] 3e60eeb74c Bump org.hibernate.orm:hibernate-core from 7.2.4.Final to 7.2.5.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 7.2.4.Final to 7.2.5.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/7.2.5/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/7.2.4...7.2.5)

---
updated-dependencies:
- dependency-name: org.hibernate.orm:hibernate-core
  dependency-version: 7.2.5.Final
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 03:17:25 +00:00
Robert Winch 21978cab22 Fix Build Errors for Improve AOT RuntimeHits
- Saml2RuntimeHints consistently uses String in separate method for
  to ensure no classpath issues
- Fix Whitespace/Checkstyle
- Add Missing Nullability Annotations
2026-02-20 17:28:35 -06:00
Josh Long 2dd2863550 aot improvements
Signed-off-by: Josh Long <54473+joshlong@users.noreply.github.com>
2026-02-20 17:28:35 -06:00
dependabot[bot] 1fdfd45782 Bump spring-io/spring-security-release-tools/.github/workflows/test.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/test.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/7d42d82298553f123a9dad622e0eac725aaf52ef...729fed56d42122f88583aff1be35c0800b7d77e9)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/test.yml
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 09:42:07 -06:00
Robert Winch 0dc1aa1126 Ensure tests clear AuthorizationServerContextHolder 2026-02-19 15:20:44 -06:00
Stefano Cordio 3cc707621f Prevent execution of scheduled GitHub Actions on forks
Signed-off-by: Stefano Cordio <stefano.cordio@gmail.com>
2026-02-19 14:41:12 -06:00
Robert Winch f8ac095d48 Add nullability contract to PasswordEncoder#encode implementations
Signed-off-by: Stefano Cordio <stefano.cordio@gmail.com>AbstractValidatingPasswordEncoder.java
2026-02-19 14:36:48 -06:00
Chen He d65625e399 Update BCryptPasswordEncoder example in password-storage.adoc
Replaces the usage of BCryptPasswordEncoder with
BcryptPassword4jPasswordEncoder in documentation samples.

Signed-off-by: Chen He <nidhogg55555@gmail.com>
2026-02-19 14:29:53 -06:00
Minu Kim 18068c9099 fix compile warning in spring-security-test
Signed-off-by: Minu Kim <kmw106933@naver.com>
2026-02-19 14:26:20 -06:00
Tran Ngoc Nhan a539f056f7 Add javadoc-warnings-error
Closes gh-18452

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-19 13:48:28 -06:00
Robert Winch 2b04177151 Merge branch '7.0.x' 2026-02-19 13:30:21 -06:00
Robert Winch a4a6e9124c Merge branch '6.5.x' into 7.0.x 2026-02-19 13:30:13 -06:00
Robert Winch b21159f453 Bump org.junit:junit-bom from 6.0.2 to 6.0.3 2026-02-19 13:29:42 -06:00
Robert Winch 6f7c8cb352 Bump org-apache-maven-resolver from 1.9.25 to 1.9.26 2026-02-19 13:29:36 -06:00
Robert Winch 5973a66bb1 Bump ch.qos.logback:logback-classic from 1.5.29 to 1.5.32 2026-02-19 13:29:30 -06:00
Robert Winch 3e3eeda560 Bump ch.qos.logback:logback-classic from 1.5.29 to 1.5.32 2026-02-19 13:28:49 -06:00
Robert Winch cc6a005aa5 Add InetAddressMatcher
Co-authored-by: Gábor Vaspöri <gabor.vaspori@gmail.com>
Co-authored-by: Kian Jamali <kianjamali123@gmail.com>
Co-authored-by: Rossen Stoyanchev <rstoyanchev@users.noreply.github.com>
2026-02-19 11:44:19 -06:00
Robert Winch d4589c0fcb Merge branch '7.0.x' 2026-02-19 11:00:16 -06:00
dependabot[bot] e2486a2590 Bump org.springframework:spring-framework-bom from 7.0.4 to 7.0.5
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 7.0.4 to 7.0.5.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v7.0.4...v7.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-19 10:58:10 -06:00
Tran Ngoc Nhan dbf7f4cfe5 Remove unused @Nullable
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-19 10:56:54 -06:00
dependabot[bot] f27738c1f8 Bump org.springframework:spring-framework-bom from 7.0.4 to 7.0.5
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 7.0.4 to 7.0.5.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v7.0.4...v7.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-19 03:18:04 +00:00
wonderfulrosemari 8d76ae0844 Document multipart CSRF header option
Closes gh-4265

Signed-off-by: wonderfulrosemari <whwlsgur1419@naver.com>
2026-02-19 02:07:25 +09:00
dependabot[bot] 73b055baa9 Bump org-opensaml5 from 5.2.0 to 5.2.1
Bumps `org-opensaml5` from 5.2.0 to 5.2.1.

Updates `org.opensaml:opensaml-saml-api` from 5.2.0 to 5.2.1

Updates `org.opensaml:opensaml-saml-impl` from 5.2.0 to 5.2.1

---
updated-dependencies:
- dependency-name: org.opensaml:opensaml-saml-api
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.opensaml:opensaml-saml-impl
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-18 03:15:22 +00:00
earlgrey02 5194826606 implement single-line RSA key support
Signed-off-by: earlgrey02 <san06036@naver.com>
2026-02-17 17:29:04 -07:00
Josh Cummings fc48088759 Polish
- Add @since version
- Change variable name to match method name

Issue gh-18623

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-17 17:19:42 -07:00
itsmevichu d528be38bb Add postProcessor to Introspector Builder
Closes gh-18623

Signed-off-by: itsmevichu <vishnutheep@gmail.com>
2026-02-17 17:19:42 -07:00
Tran Ngoc Nhan da0cd0bc68 Remove unused import
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Josh Cummings 410812c5bc Reduce Diff Size
This commit reorders the originally changed boolean logic so that it returns
false early, as it did before. This allows the change to remain small and also
keeps the most complex logical statements outside of the if statement.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan 329d9e2dfd Revert code
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan dc8ed8b168 Fix checkstyle
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan 17933ddab3 Resolve feedback
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan deb6416c93 Fix checkstyle
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan 9323775c5f Update javadoc and apply StringUtils#hasLength
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan 4cc5f543ab Add author
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan 67bc1d8d4a Polish some methods
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan 17b5cdde55 Remove redundant check and exception
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan e91b098c7c Update javadoc
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Tran Ngoc Nhan 21bef947b0 Use String#isEmpty
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-17 16:59:55 -07:00
Andrey Litvitski 6fcca39500 Mark CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName as Nullable
Closes: gh-18617

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-02-17 16:57:15 -07:00
Joe Grandja 544f635e9b Polish gh-17246 2026-02-17 16:35:57 -05:00
Evgeniy Cheban e4dcffae8a Ensure ID Token is updated after refresh token (Reactive)
Closes gh-17188

Signed-off-by: Evgeniy Cheban <mister.cheban@gmail.com>
2026-02-17 14:38:15 -05:00
dependabot[bot] f52f097a4d Bump ch.qos.logback:logback-classic from 1.5.31 to 1.5.32
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.31 to 1.5.32.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.31...v_1.5.32)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.5.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 03:16:46 +00:00
dependabot[bot] 1cd145d026 Bump org-apache-maven-resolver from 1.9.25 to 1.9.26
Bumps `org-apache-maven-resolver` from 1.9.25 to 1.9.26.

Updates `org.apache.maven.resolver:maven-resolver-connector-basic` from 1.9.25 to 1.9.26
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.25...maven-resolver-1.9.26)

Updates `org.apache.maven.resolver:maven-resolver-impl` from 1.9.25 to 1.9.26
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.25...maven-resolver-1.9.26)

Updates `org.apache.maven.resolver:maven-resolver-transport-http` from 1.9.25 to 1.9.26

---
updated-dependencies:
- dependency-name: org.apache.maven.resolver:maven-resolver-connector-basic
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-impl
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-transport-http
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 03:16:31 +00:00
dependabot[bot] 3c55f057b1 Bump ch.qos.logback:logback-classic from 1.5.29 to 1.5.32
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.29 to 1.5.32.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.29...v_1.5.32)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.5.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 03:10:25 +00:00
dependabot[bot] 6d2a414022 Bump org-apache-maven-resolver from 1.9.25 to 1.9.26
Bumps `org-apache-maven-resolver` from 1.9.25 to 1.9.26.

Updates `org.apache.maven.resolver:maven-resolver-connector-basic` from 1.9.25 to 1.9.26
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.25...maven-resolver-1.9.26)

Updates `org.apache.maven.resolver:maven-resolver-impl` from 1.9.25 to 1.9.26
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.25...maven-resolver-1.9.26)

Updates `org.apache.maven.resolver:maven-resolver-transport-http` from 1.9.25 to 1.9.26

---
updated-dependencies:
- dependency-name: org.apache.maven.resolver:maven-resolver-connector-basic
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-impl
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-transport-http
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 03:09:56 +00:00
dependabot[bot] 58df50c3a3 Bump org-apache-maven-resolver from 1.9.25 to 1.9.26
Bumps `org-apache-maven-resolver` from 1.9.25 to 1.9.26.

Updates `org.apache.maven.resolver:maven-resolver-connector-basic` from 1.9.25 to 1.9.26
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.25...maven-resolver-1.9.26)

Updates `org.apache.maven.resolver:maven-resolver-impl` from 1.9.25 to 1.9.26
- [Release notes](https://github.com/apache/maven-resolver/releases)
- [Commits](https://github.com/apache/maven-resolver/compare/maven-resolver-1.9.25...maven-resolver-1.9.26)

Updates `org.apache.maven.resolver:maven-resolver-transport-http` from 1.9.25 to 1.9.26

---
updated-dependencies:
- dependency-name: org.apache.maven.resolver:maven-resolver-connector-basic
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-impl
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.resolver:maven-resolver-transport-http
  dependency-version: 1.9.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 03:06:35 +00:00
dependabot[bot] 79156b2387 Bump ch.qos.logback:logback-classic from 1.5.29 to 1.5.32
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.29 to 1.5.32.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.29...v_1.5.32)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.5.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 03:06:15 +00:00
dependabot[bot] 4aa9ff99f5 Bump org.junit:junit-bom from 6.0.2 to 6.0.3
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r6.0.2...r6.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 03:18:27 +00:00
dependabot[bot] 9b978ae70b Bump ch.qos.logback:logback-classic from 1.5.29 to 1.5.31
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.29 to 1.5.31.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.29...v_1.5.31)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.5.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 03:17:59 +00:00
dependabot[bot] 3abb69d5a9 Bump org.junit:junit-bom from 6.0.2 to 6.0.3
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r6.0.2...r6.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 03:15:44 +00:00
github-actions[bot] 6c2b2a7611 Next development version 2026-02-13 18:24:26 +00:00
github-actions[bot] 0fab34f359 Release 6.5.8 2026-02-13 17:54:05 +00:00
Joe Grandja 73dff1f8a3 Next development version 2026-02-13 12:23:48 -05:00
Josh Cummings e5906d97ea Remove Typo in Java Version Declaration
Issue gh-18512

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-13 10:04:03 -07:00
github-actions[bot] 928ad9600c Release 7.1.0-M2 2026-02-13 16:25:52 +00:00
github-actions[bot] c0da8b390b Next development version 2026-02-13 15:57:31 +00:00
github-actions[bot] ffe73b4920 Release 7.0.3 2026-02-13 15:26:51 +00:00
Joe Grandja f0ffda89e0 Update to spring-data-bom 2025.1.3
Closes gh-18735
2026-02-13 08:18:47 -05:00
dependabot[bot] f9b2c86e1d Bump org.springframework.data:spring-data-bom from 2025.1.2 to 2025.1.3
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2025.1.2 to 2025.1.3.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2025.1.2...2025.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 12:19:41 +00:00
dependabot[bot] 746c6e124e Bump org.springframework:spring-framework-bom from 7.0.3 to 7.0.4
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 7.0.3 to 7.0.4.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v7.0.3...v7.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 06:48:23 -05:00
dependabot[bot] 08e5b375ac Bump io.projectreactor:reactor-bom from 2024.0.14 to 2024.0.15
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.14 to 2024.0.15.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2024.0.14...2024.0.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 06:41:17 -05:00
dependabot[bot] 123a2d79cf Bump io.projectreactor:reactor-bom from 2025.0.2 to 2025.0.3
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2025.0.2 to 2025.0.3.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2025.0.2...2025.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 06:40:14 -05:00
dependabot[bot] f9c32afb6f Bump org.springframework:spring-framework-bom from 6.2.15 to 6.2.16
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.15 to 6.2.16.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 06:33:08 -05:00
dependabot[bot] 0c3e483432 Bump org.springframework.ldap:spring-ldap-core from 4.0.1 to 4.0.2
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 4.0.1 to 4.0.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/4.0.1...4.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 06:31:35 -05:00
dependabot[bot] d77e48f9ef Bump org.springframework.ldap:spring-ldap-core from 4.0.1 to 4.0.2
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 4.0.1 to 4.0.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/4.0.1...4.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 03:17:06 +00:00
dependabot[bot] 25da472d67 Bump org.springframework:spring-framework-bom from 7.0.3 to 7.0.4
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 7.0.3 to 7.0.4.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v7.0.3...v7.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 03:16:43 +00:00
dependabot[bot] 9d3e217b79 Bump spring-io/spring-security-release-tools from 1.0.13 to 1.0.14
Bumps [spring-io/spring-security-release-tools](https://github.com/spring-io/spring-security-release-tools) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.13...729fed56d42122f88583aff1be35c0800b7d77e9)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 00:49:00 +00:00
dependabot[bot] 7bbfc09f49 Bump spring-io/spring-security-release-tools/.github/workflows/perform-release.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/perform-release.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.13...729fed56d42122f88583aff1be35c0800b7d77e9)

---
updated-dependencies:
- dependency-name: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 00:47:48 +00:00
Josh Cummings 85e2d9298b Merge remote-tracking branch 'origin/7.0.x' 2026-02-12 16:46:14 -07:00
Josh Cummings b804da974d Update Test to Align with webauthn4j
The latest webauthn4j exposes Jackson 3 instead of Jackson 2,
as such this test now uses Jackson 3 where needed.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-12 16:45:13 -07:00
dependabot[bot] b9bb5e0b52 Bump com.webauthn4j:webauthn4j-core
Bumps [com.webauthn4j:webauthn4j-core](https://github.com/webauthn4j/webauthn4j) from 0.29.7.RELEASE to 0.31.0.RELEASE.
- [Release notes](https://github.com/webauthn4j/webauthn4j/releases)
- [Commits](https://github.com/webauthn4j/webauthn4j/compare/0.29.7.RELEASE...0.31.0.RELEASE)

---
updated-dependencies:
- dependency-name: com.webauthn4j:webauthn4j-core
  dependency-version: 0.31.0.RELEASE
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 16:45:13 -07:00
Josh Cummings 4fd8e1d596 Remove Trailing Bytes from AttestationStatement
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-12 16:45:13 -07:00
Josh Cummings c59fb0cd35 Add Jackson 2 Databind as Optional Dependency
Since spring-security-webauthn has Jackson 2 Mixins, it would
be clearer to set Jackson 2 explicitly as an optional dependency

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-12 16:45:13 -07:00
Josh Cummings bc6ce0d346 Merge branch '7.0.x' 2026-02-12 10:36:20 -07:00
dependabot[bot] 50aba3aaf3 Bump io.spring.gradle:spring-security-release-plugin
Bumps [io.spring.gradle:spring-security-release-plugin](https://github.com/spring-io/spring-security-release-tools) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.13...v1.0.14)

---
updated-dependencies:
- dependency-name: io.spring.gradle:spring-security-release-plugin
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 10:35:14 -07:00
dependabot[bot] 3d61276a1a Bump io.spring.gradle:spring-security-release-plugin
Bumps [io.spring.gradle:spring-security-release-plugin](https://github.com/spring-io/spring-security-release-tools) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.13...v1.0.14)

---
updated-dependencies:
- dependency-name: io.spring.gradle:spring-security-release-plugin
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 10:31:30 -07:00
Josh Cummings 25aec8c5e0 Update Release Steps to JDK 25
Issue gh-18512

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-12 10:29:43 -07:00
Josh Cummings 6eb0af9912 Merge branch '7.0.x' 2026-02-12 10:28:00 -07:00
Josh Cummings 6cbbf6c561 Merge branch '6.5.x' into 7.0.x 2026-02-12 10:27:46 -07:00
Josh Cummings 10cb6f7003 Update spring-security-release-tools 1.0.14 2026-02-12 10:25:47 -07:00
Josh Cummings 7fdff6a907 Use spring-github-workflows Auto-Merge 2026-02-12 10:21:32 -07:00
github-actions[bot] 117af3bc2b Merge pull request #18723 from spring-projects/dependabot/gradle/main/io.spring.gradle-spring-security-release-plugin-1.0.14
Bump io.spring.gradle:spring-security-release-plugin from 1.0.13 to 1.0.14
2026-02-12 03:17:42 +00:00
dependabot[bot] c7f781423f Bump io.spring.gradle:spring-security-release-plugin
Bumps [io.spring.gradle:spring-security-release-plugin](https://github.com/spring-io/spring-security-release-tools) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.13...v1.0.14)

---
updated-dependencies:
- dependency-name: io.spring.gradle:spring-security-release-plugin
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 03:06:08 +00:00
coehgns 0d3a5d210a Add tests for PathPatternRequestMatcher path caching
Verify parsed request path is cleared when matcher parses it, and preserved when already present.

Signed-off-by: coehgns <modooboiroo@gmail.com>
2026-02-12 11:13:02 +09:00
github-actions[bot] fb2f0d5c38 Merge pull request #18715 from spring-projects/dependabot/gradle/main/io.projectreactor-reactor-bom-2025.0.3
Bump io.projectreactor:reactor-bom from 2025.0.2 to 2025.0.3
2026-02-11 03:18:39 +00:00
github-actions[bot] 10b9cc8c2b Merge pull request #18713 from spring-projects/dependabot/gradle/main/io.micrometer-micrometer-observation-1.16.3
Bump io.micrometer:micrometer-observation from 1.16.2 to 1.16.3
2026-02-11 03:18:24 +00:00
github-actions[bot] 5240878272 Merge pull request #18714 from spring-projects/dependabot/gradle/main/ch.qos.logback-logback-classic-1.5.29
Bump ch.qos.logback:logback-classic from 1.5.28 to 1.5.29
2026-02-11 03:18:04 +00:00
dependabot[bot] ba4bd61c5b Bump io.projectreactor:reactor-bom from 2025.0.2 to 2025.0.3
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2025.0.2 to 2025.0.3.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2025.0.2...2025.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 03:06:26 +00:00
dependabot[bot] c25ec70374 Bump ch.qos.logback:logback-classic from 1.5.28 to 1.5.29
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.28 to 1.5.29.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.28...v_1.5.29)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.5.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 03:06:19 +00:00
dependabot[bot] 8e1e0ca9d2 Bump io.micrometer:micrometer-observation from 1.16.2 to 1.16.3
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.16.2 to 1.16.3.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.16.2...v1.16.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 03:05:06 +00:00
Josh Cummings 705fa60a01 Document Method Security hasScope Support
Issue gh-18013

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-10 15:23:32 -07:00
Tran Ngoc Nhan f2b7cb2de5 Support hasScope in Method Security
Closes gh-18013

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-10 15:23:32 -07:00
coehgns 8652950fb2 Fix typos in contributing guide Tidy up wording in CONTRIBUTING.adoc to improve readability.
Signed-off-by: coehgns <modooboiroo@gmail.com>
2026-02-10 13:54:55 -07:00
Josh Cummings 07ba3e623f Merge branch '7.0.x' 2026-02-10 13:41:47 -07:00
Josh Cummings 252c69460e Merge remote-tracking branch 'origin/6.5.x' into 7.0.x 2026-02-10 13:41:29 -07:00
dependabot[bot] 3131642aae Bump io.micrometer:context-propagation from 1.1.3 to 1.1.4
Bumps [io.micrometer:context-propagation](https://github.com/micrometer-metrics/context-propagation) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/micrometer-metrics/context-propagation/releases)
- [Commits](https://github.com/micrometer-metrics/context-propagation/compare/v1.1.3...v1.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 13:41:09 -07:00
dependabot[bot] 552d8d1d29 Bump ch.qos.logback:logback-classic from 1.5.28 to 1.5.29
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.28 to 1.5.29.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.28...v_1.5.29)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-version: 1.5.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 13:39:59 -07:00
dependabot[bot] f240f29433 Bump gradle-wrapper from 8.14 to 8.14.4
Bumps gradle-wrapper from 8.14 to 8.14.4.

---
updated-dependencies:
- dependency-name: gradle-wrapper
  dependency-version: 8.14.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 13:39:38 -07:00
github-actions[bot] f91b5f33fc Merge pull request #18701 from spring-projects/dependabot/gradle/main/com.nimbusds-oauth2-oidc-sdk-11.33
Bump com.nimbusds:oauth2-oidc-sdk from 11.26.1 to 11.33
2026-02-10 17:51:23 +00:00
Josh Cummings 5418ab2081 Update nimbus-jose-jwt from 10.4 to 10.6
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-10 10:37:44 -07:00
Josh Cummings e8e4110334 Wrap RuntimeException in fromOidcConfiguration
This commit makes so that fromOidcConfiguration throws the same exception
caused by chain as other configuration methods. Specifically, if parsing
throws a RuntimeException, this method will now wrap it in an
IllegalArgumentException as other configuration methods do.

This makes specific sense here since the RuntimeException is almost certainly
caused by a malformed configuration set handed in as a method parameter.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-10 10:37:40 -07:00
dependabot[bot] 17e368435d Bump com.nimbusds:oauth2-oidc-sdk from 11.26.1 to 11.33
Bumps [com.nimbusds:oauth2-oidc-sdk](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions) from 11.26.1 to 11.33.
- [Changelog](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/src/master/CHANGELOG.txt)
- [Commits](https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/branches/compare/11.33..11.26.1)

---
updated-dependencies:
- dependency-name: com.nimbusds:oauth2-oidc-sdk
  dependency-version: '11.33'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 03:07:15 +00:00
150 changed files with 5466 additions and 559 deletions
+10 -19
View File
@@ -1,25 +1,16 @@
name: Dependabot Auto-Merge
name: Merge Dependabot PR
on:
pull_request:
branches:
- 'main'
- main
- '*.x'
permissions:
contents: write
pull-requests: write
run-name: Merge Dependabot PR ${{ github.ref_name }}
jobs:
dependabot:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7
with:
github-token: ${{ github.token }}
- name: Enable auto-merge for Dependabot PRs
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ github.token }}
merge-dependabot-pr:
permissions: write-all
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v7
with:
mergeArguments: --auto --rebase
+2 -2
View File
@@ -14,7 +14,7 @@ permissions:
jobs:
snapshot-test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
strategy:
matrix:
include:
@@ -31,6 +31,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -17,7 +17,7 @@ permissions:
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
@@ -30,22 +30,24 @@ jobs:
deploy-artifacts:
name: Deploy Artifacts
needs: [ build]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
default-publish-milestones-central: true
java-version: 25
secrets: inherit
deploy-schema:
name: Deploy Schema
needs: [ build ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
java-version: 25
secrets: inherit
perform-release:
name: Perform Release
needs: [ deploy-artifacts, deploy-schema ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
@@ -53,6 +55,7 @@ jobs:
release-repo-url: https://repo1.maven.org/maven2
artifact-path: org/springframework/security/spring-security-core
slack-announcing-id: spring-security-announcing
java-version: 25
secrets: inherit
send-notification:
name: Send Notification
@@ -61,6 +64,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+1 -1
View File
@@ -16,7 +16,7 @@ permissions:
jobs:
perform-release:
name: Perform Release
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
should-perform-release: true
project-version: ${{ inputs.version }}
@@ -9,6 +9,7 @@ permissions:
jobs:
upgrade_wrapper:
name: Execution
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- name: Set up Git configuration
@@ -30,6 +30,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+1 -1
View File
@@ -46,6 +46,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -12,8 +12,9 @@ permissions:
jobs:
update-antora-ui-spring:
runs-on: ubuntu-latest
name: Update on Supported Branches
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
strategy:
matrix:
branch: [ '6.5.x', '7.0.x', 'main' ]
@@ -25,8 +26,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
antora-file-path: 'docs/antora-playbook.yml'
update-antora-ui-spring-docs-build:
runs-on: ubuntu-latest
name: Update on docs-build
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
name: Update
@@ -9,7 +9,7 @@ permissions:
jobs:
update-scheduled-release-version:
name: Update Scheduled Release Version
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
secrets: inherit
send-notification:
name: Send Notification
@@ -18,6 +18,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@7d42d82298553f123a9dad622e0eac725aaf52ef # v1.0.13
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+2 -2
View File
@@ -31,7 +31,7 @@ If you have a question, check Stack Overflow using
https://stackoverflow.com/questions/tagged/spring-security+or+spring-ldap+or+spring-authorization-server+or+spring-session?tab=Newest[this list of tags].
Find an existing discussion, or start a new one if necessary.
If you believe there is an issue, search through https://github.com/spring-projects/spring-security/issues[existing issues] trying a few different ways to find discussions, past or current, that are related to the issue.
If you believe there is an issue, search through https://github.com/spring-projects/spring-security/issues[existing issues] trying a few different ways to find discussions, past or current, that are related to the issue.
Reading those discussions helps you to learn about the issue, and helps us to make a decision.
[[find-an-issue]]
@@ -94,7 +94,7 @@ Don't worry if you don't get them all correct the first time, we will help you.
1. [[sign-cla]] All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring].
2. [[create-an-issue-list]] Must you https://github.com/spring-projects/spring-security/issues/new/choose[create an issue] first? No, but it is recommended for features and larger bug fixes. It's easier discuss with the team first to determine the right fix or enhancement.
2. [[create-an-issue-list]] Must you https://github.com/spring-projects/spring-security/issues/new/choose[create an issue] first? No, but it is recommended for features and larger bug fixes. It's easier to discuss with the team first to determine the right fix or enhancement.
For typos and straightforward bug fixes, starting with a pull request is encouraged.
Please include a description for context and motivation.
Note that the team may close your pull request if it's not a fit for the project.
@@ -31,6 +31,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.method.AbstractFallbackMethodSecurityMetadataSource;
import org.springframework.util.StringUtils;
/**
* Sources method security metadata from major JSR 250 security annotations.
@@ -108,7 +109,7 @@ public class Jsr250MethodSecurityMetadataSource extends AbstractFallbackMethodSe
if (role == null) {
return role;
}
if (this.defaultRolePrefix == null || this.defaultRolePrefix.length() == 0) {
if (!StringUtils.hasLength(this.defaultRolePrefix)) {
return role;
}
if (role.startsWith(this.defaultRolePrefix)) {
@@ -0,0 +1,79 @@
/*
* Copyright 2004-present 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.acls.aot.hint;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.acls.domain.AclImpl;
import org.springframework.security.acls.domain.AuditLogger;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.AccessControlEntry;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.AuditableAccessControlEntry;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.Sid;
/**
* {@link RuntimeHintsRegistrar} for ACL (Access Control List) classes.
*
* @author Josh Long
*/
class AclRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
registerAclDomainHints(hints);
registerJdbcSchemaHints(hints);
}
private void registerAclDomainHints(RuntimeHints hints) {
// Register core ACL domain types
Stream
.of(Acl.class, AccessControlEntry.class, AuditableAccessControlEntry.class, ObjectIdentity.class, Sid.class,
AclImpl.class, AccessControlEntry.class, AuditLogger.class, ObjectIdentityImpl.class,
PrincipalSid.class, GrantedAuthoritySid.class, BasePermission.class)
.forEach((c) -> hints.reflection()
.registerType(TypeReference.of(c),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS)));
}
private void registerJdbcSchemaHints(RuntimeHints hints) {
String[] sqlFiles = new String[] { "createAclSchema.sql", "createAclSchemaMySQL.sql",
"createAclSchemaOracle.sql", "createAclSchemaPostgres.sql", "createAclSchemaSqlServer.sql",
"createAclSchemaWithAclClassIdType.sql", "select.sql" };
for (String sqlFile : sqlFiles) {
Resource sqlResource = new ClassPathResource(sqlFile);
if (sqlResource.exists()) {
hints.resources().registerResource(sqlResource);
}
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* AOT and native image hint support for ACLs.
*/
@NullMarked
package org.springframework.security.acls.aot.hint;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,2 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.security.acls.aot.hint.AclRuntimeHints
@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
* and using the current URL minus the artifact and the corresponding value.
*
* @author Rob Winch
* @author Ngoc Nhan
*/
final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
implements ServiceAuthenticationDetails {
@@ -74,10 +75,9 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
if (this == obj) {
return true;
}
if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails)) {
if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails that)) {
return false;
}
ServiceAuthenticationDetails that = (ServiceAuthenticationDetails) obj;
return this.serviceUrl.equals(that.getServiceUrl());
}
@@ -101,7 +101,11 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
/**
* If present, removes the artifactParameterName and the corresponding value from the
* query String.
* @param request
* @param request the current {@link HttpServletRequest} to obtain the
* {@link #getServiceUrl()} from.
* @param artifactPattern the {@link Pattern} that will be used to clean up the query
* string from containing the artifact name and value. This can be created using
* {@link #createArtifactPattern(String)}.
* @return the query String minus the artifactParameterName and the corresponding
* value.
*/
@@ -111,7 +115,7 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
return null;
}
String result = artifactPattern.matcher(query).replaceFirst("");
if (result.length() == 0) {
if (result.isEmpty()) {
return null;
}
// strip off the trailing & only if the artifact was the first query param
@@ -122,8 +126,9 @@ final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails
* Creates a {@link Pattern} that can be passed into the constructor. This allows the
* {@link Pattern} to be reused for every instance of
* {@link DefaultServiceAuthenticationDetails}.
* @param artifactParameterName
* @return
* @param artifactParameterName the artifactParameterName that is removed from the
* current URL. The result becomes the service url. Cannot be null or an empty String.
* @return a {@link Pattern}
*/
static Pattern createArtifactPattern(String artifactParameterName) {
Assert.hasLength(artifactParameterName, "artifactParameterName is expected to have a length");
@@ -36,6 +36,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.log.LogMessage;
import org.springframework.lang.Contract;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
@@ -302,6 +303,7 @@ public class AuthenticationConfiguration {
}
@Override
@Contract("!null -> !null; null -> null")
public String encode(CharSequence rawPassword) {
return getPasswordEncoder().encode(rawPassword);
}
@@ -30,6 +30,7 @@ import org.springframework.context.annotation.Scope;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Contract;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
@@ -293,6 +294,7 @@ class HttpSecurityConfiguration {
}
@Override
@Contract("!null -> !null; null -> null")
public String encode(CharSequence rawPassword) {
return getPasswordEncoder().encode(rawPassword);
}
@@ -124,6 +124,10 @@ class AuthorizationFilterParser implements BeanDefinitionParser {
List<Element> interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_URL);
for (Element interceptMessage : interceptMessages) {
String accessExpression = interceptMessage.getAttribute(ATT_ACCESS);
if (!StringUtils.hasText(accessExpression)) {
parserContext.getReaderContext().error("access attribute cannot be empty or null", interceptMessage);
continue;
}
BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder
.rootBeanDefinition(WebExpressionAuthorizationManager.class);
authorizationManager.addPropertyReference("expressionHandler", expressionHandlerRef);
@@ -142,10 +142,11 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
ManagedMap<BeanMetadataElement, BeanDefinition> filterInvocationDefinitionMap = new ManagedMap<>();
for (Element urlElt : urlElts) {
String access = urlElt.getAttribute(ATT_ACCESS);
String path = urlElt.getAttribute(ATT_PATTERN);
if (!StringUtils.hasText(access)) {
parserContext.getReaderContext().error("access attribute cannot be empty or null", urlElt);
continue;
}
String path = urlElt.getAttribute(ATT_PATTERN);
String matcherRef = urlElt.getAttribute(HttpSecurityBeanDefinitionParser.ATT_REQUEST_MATCHER_REF);
boolean hasMatcherRef = StringUtils.hasText(matcherRef);
if (!hasMatcherRef && !StringUtils.hasText(path)) {
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
@@ -245,12 +246,15 @@ import org.springframework.security.web.savedrequest.SimpleSavedRequest;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedException;
import org.springframework.security.web.session.HttpSessionCreatedEvent;
import org.springframework.security.web.session.HttpSessionIdChangedEvent;
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInput;
@@ -258,12 +262,17 @@ import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExte
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialParameters;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.ResidentKeyRequirement;
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
import org.springframework.security.web.webauthn.api.TestBytes;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentials;
@@ -271,6 +280,7 @@ import org.springframework.security.web.webauthn.api.UserVerificationRequirement
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationRequestToken;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
import org.springframework.security.web.webauthn.management.TestPublicKeyCredentialRpEntities;
import org.springframework.util.ReflectionUtils;
final class SerializationSamples {
@@ -879,6 +889,36 @@ final class SerializationSamples {
generatorByClassName.put(CredentialPropertiesOutput.ExtensionOutput.class,
(r) -> new CredentialPropertiesOutput(true).getOutput());
AttestationConveyancePreference attestationConveyancePreference = AttestationConveyancePreference.DIRECT;
ResidentKeyRequirement residentKeyRequirement = ResidentKeyRequirement.REQUIRED;
AuthenticatorSelectionCriteria authenticatorSelectionCriteria = AuthenticatorSelectionCriteria.builder()
.authenticatorAttachment(AuthenticatorAttachment.PLATFORM)
.residentKey(residentKeyRequirement)
.userVerification(UserVerificationRequirement.REQUIRED)
.build();
PublicKeyCredentialParameters publicKeyCredentialParameters = PublicKeyCredentialParameters.RS256;
PublicKeyCredentialRpEntity publicKeyCredentialRpEntity = TestPublicKeyCredentialRpEntities.createRpEntity()
.build();
generatorByClassName.put(AttestationConveyancePreference.class, (r) -> attestationConveyancePreference);
generatorByClassName.put(ResidentKeyRequirement.class, (r) -> residentKeyRequirement);
generatorByClassName.put(AuthenticatorSelectionCriteria.class, (r) -> authenticatorSelectionCriteria);
generatorByClassName.put(COSEAlgorithmIdentifier.class, ((r) -> COSEAlgorithmIdentifier.RS256));
generatorByClassName.put(PublicKeyCredentialParameters.class, (r) -> publicKeyCredentialParameters);
generatorByClassName.put(PublicKeyCredentialRpEntity.class, (r) -> publicKeyCredentialRpEntity);
generatorByClassName.put(PublicKeyCredentialCreationOptions.class,
(o) -> TestPublicKeyCredentialCreationOptions.createPublicKeyCredentialCreationOptions()
.extensions(inputs)
.attestation(attestationConveyancePreference)
.authenticatorSelection(authenticatorSelectionCriteria)
.challenge(TestBytes.get())
.excludeCredentials(List.of(descriptor))
.rp(publicKeyCredentialRpEntity)
.pubKeyCredParams(publicKeyCredentialParameters)
.timeout(Duration.ofMinutes(5))
.user(TestPublicKeyCredentialUserEntities.userEntity().id(TestBytes.get()).build())
.build());
// One-Time Token
DefaultOneTimeToken oneTimeToken = new DefaultOneTimeToken(UUID.randomUUID().toString(), "user",
Instant.now().plusSeconds(300));
@@ -222,7 +222,8 @@ public class WebSecurityConfigurationTests {
// SEC-2773
@Test
public void getMethodDelegatingApplicationListenerWhenWebSecurityConfigurationThenIsStatic() {
Method method = ClassUtils.getMethod(WebSecurityConfiguration.class, "delegatingApplicationListener", null);
Method method = ClassUtils.getMethod(WebSecurityConfiguration.class, "delegatingApplicationListener",
(Class<?>[]) null);
assertThat(Modifier.isStatic(method.getModifiers())).isTrue();
}
@@ -337,6 +337,54 @@ public class InterceptUrlConfigTests {
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
}
/**
* gh-18503
*/
@Test
public void configWhenInterceptUrlMissingAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("MissingAccess")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
/**
* gh-18503
*/
@Test
public void configWhenInterceptUrlEmptyAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("EmptyAccess")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
/**
* gh-18503
*/
@Test
public void configWhenInterceptUrlValidAccessThenLoads() {
assertThatNoException().isThrownBy(() -> this.spring.configLocations(this.xml("ValidAccess")).autowire());
}
/**
* gh-18503
*/
@Test
public void configWhenUseAuthorizationManagerFalseAndMissingAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("MissingAccessLegacy")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
/**
* gh-18503
*/
@Test
public void configWhenUseAuthorizationManagerFalseAndEmptyAccessThenException() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(this.xml("EmptyAccessLegacy")).autowire())
.withMessageContaining("access attribute cannot be empty or null");
}
private static RequestPostProcessor adminCredentials() {
return httpBasic("admin", "password");
}
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<http>
<intercept-url pattern="/admin/**" access="" />
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<http use-authorization-manager="false">
<intercept-url pattern="/admin/**" access=""/>
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<http>
<intercept-url pattern="/admin/**"/>
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<http use-authorization-manager="false">
<intercept-url pattern="/admin/**"/>
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<http>
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')" />
<http-basic />
</http>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
@@ -38,6 +38,7 @@ import org.springframework.util.function.SingletonSupplier;
* @author Luke Taylor
* @author Evgeniy Cheban
* @author Steve Riesenberg
* @author Ngoc Nhan
* @since 3.0
*/
public abstract class SecurityExpressionRoot<T extends @Nullable Object> implements SecurityExpressionOperations {
@@ -208,7 +209,8 @@ public abstract class SecurityExpressionRoot<T extends @Nullable Object> impleme
/**
* Convenience method to access {@link Authentication#getPrincipal()} from
* {@link #getAuthentication()}
* @return
* @return the {@code Principal} being authenticated or the authenticated principal
* after authentication.
*/
public @Nullable Object getPrincipal() {
return getAuthentication().getPrincipal();
@@ -35,6 +35,8 @@ import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.authentication.event.AuthenticationFailureCredentialsExpiredEvent;
import org.springframework.security.authentication.event.AuthenticationFailureDisabledEvent;
@@ -43,6 +45,7 @@ import org.springframework.security.authentication.event.AuthenticationFailureLo
import org.springframework.security.authentication.event.AuthenticationFailureProviderNotFoundEvent;
import org.springframework.security.authentication.event.AuthenticationFailureProxyUntrustedEvent;
import org.springframework.security.authentication.event.AuthenticationFailureServiceExceptionEvent;
import org.springframework.security.authentication.ott.OneTimeTokenAuthentication;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
@@ -60,6 +63,7 @@ class CoreSecurityRuntimeHints implements RuntimeHintsRegistrar {
registerExceptionEventsHints(hints);
registerExpressionEvaluationHints(hints);
registerMethodSecurityHints(hints);
registerAdditionalAuthenticationTypes(hints);
hints.resources().registerResourceBundle("org.springframework.security.messages");
registerDefaultJdbcSchemaFileHint(hints);
registerSecurityContextHints(hints);
@@ -114,4 +118,17 @@ class CoreSecurityRuntimeHints implements RuntimeHintsRegistrar {
(builder) -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
}
private void registerAdditionalAuthenticationTypes(RuntimeHints hints) {
// RememberMeAuthenticationToken can be stored in the HTTP session and
// deserialized via Jackson (RememberMeAuthenticationTokenMixin exists for both
// Jackson 2 and 3), so it needs reflection hints in all native image scenarios.
Stream
.of(RememberMeAuthenticationToken.class, OneTimeTokenAuthentication.class,
UsernamePasswordAuthenticationToken.class)
.map(TypeReference::of)
.forEach((it) -> hints.reflection()
.registerType(it, (builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS)));
}
}
@@ -69,7 +69,11 @@ public final class AuthoritiesAuthorizationManager implements AuthorizationManag
private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
if (authorities.contains(grantedAuthority.getAuthority())) {
String authority = grantedAuthority.getAuthority();
if (authority == null) {
continue;
}
if (authorities.contains(authority)) {
return true;
}
}
@@ -87,9 +87,17 @@ public final class RsaKeyConverters {
"Key is not in PEM-encoded PKCS#8 format, please check that the header begins with "
+ PKCS8_PEM_HEADER);
StringBuilder base64Encoded = new StringBuilder();
for (String line : lines) {
if (RsaKeyConverters.isNotPkcs8Wrapper(line)) {
base64Encoded.append(line);
if (lines.size() == 1) {
base64Encoded.append(lines.get(0)
.replace(PKCS8_PEM_HEADER, "")
.replace(PKCS8_PEM_FOOTER, "")
.replaceAll("\\s+", ""));
}
else {
for (String line : lines) {
if (RsaKeyConverters.isNotPkcs8Wrapper(line)) {
base64Encoded.append(line);
}
}
}
byte[] pkcs8 = Base64.getDecoder().decode(base64Encoded.toString());
@@ -165,9 +173,15 @@ public final class RsaKeyConverters {
@Override
public @NonNull RSAPublicKey convert(List<String> lines) {
StringBuilder base64Encoded = new StringBuilder();
for (String line : lines) {
if (isNotX509PemWrapper(line)) {
base64Encoded.append(line);
if (lines.size() == 1) {
base64Encoded.append(
lines.get(0).replace(X509_PEM_HEADER, "").replace(X509_PEM_FOOTER, "").replaceAll("\\s+", ""));
}
else {
for (String line : lines) {
if (isNotX509PemWrapper(line)) {
base64Encoded.append(line);
}
}
}
byte[] x509 = Base64.getDecoder().decode(base64Encoded.toString());
@@ -196,9 +210,17 @@ public final class RsaKeyConverters {
@Override
public @NonNull RSAPublicKey convert(List<String> lines) {
StringBuilder base64Encoded = new StringBuilder();
for (String line : lines) {
if (isNotX509CertificateWrapper(line)) {
base64Encoded.append(line);
if (lines.size() == 1) {
base64Encoded.append(lines.get(0)
.replace(X509_CERT_HEADER, "")
.replace(X509_CERT_FOOTER, "")
.replaceAll("\\s+", ""));
}
else {
for (String line : lines) {
if (isNotX509CertificateWrapper(line)) {
base64Encoded.append(line);
}
}
}
byte[] x509 = Base64.getDecoder().decode(base64Encoded.toString());
@@ -88,7 +88,6 @@ public final class FactorGrantedAuthority implements GrantedAuthority {
private final Instant issuedAt;
@SuppressWarnings("NullAway")
private FactorGrantedAuthority(String authority, Instant issuedAt) {
Assert.notNull(authority, "authority cannot be null");
Assert.notNull(issuedAt, "issuedAt cannot be null");
@@ -31,6 +31,7 @@ class OneTimeTokenAuthenticationTokenTests {
// gh-18095
@Test
@SuppressWarnings("removal")
void shouldBeAbleToDeserializeFromJsonWithDefaultTypingActivated() throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
@@ -59,6 +59,7 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
private static final String TOKEN = "token";
@Test
@SuppressWarnings("removal")
public void constructorWhenOneTimeTokenServiceNullThenIllegalArgumentException() {
ReactiveUserDetailsService userDetailsService = mock(ReactiveUserDetailsService.class);
// @formatter:off
@@ -68,6 +69,7 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
public void constructorWhenUserDetailsServiceNullThenIllegalArgumentException() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
// @formatter:off
@@ -77,6 +79,7 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
void authenticateWhenOneTimeTokenAuthenticationTokenIsPresentThenSuccess() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
given(oneTimeTokenService.consume(ArgumentMatchers.any(OneTimeTokenAuthenticationToken.class)))
@@ -103,6 +106,7 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
void authenticateWhenInvalidOneTimeTokenAuthenticationTokenIsPresentThenFail() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
given(oneTimeTokenService.consume(ArgumentMatchers.any(OneTimeTokenAuthenticationToken.class)))
@@ -120,6 +124,7 @@ public class OneTimeTokenReactiveAuthenticationManagerTests {
}
@Test
@SuppressWarnings("removal")
void authenticateWhenIncorrectTypeOfAuthenticationIsPresentThenFail() {
ReactiveOneTimeTokenService oneTimeTokenService = mock(ReactiveOneTimeTokenService.class);
given(oneTimeTokenService.consume(ArgumentMatchers.any(OneTimeTokenAuthenticationToken.class)))
@@ -17,7 +17,9 @@
package org.springframework.security.authorization;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
@@ -30,11 +32,13 @@ import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
/**
* Tests for {@link AuthoritiesAuthorizationManager}.
*
* @author Evgeniy Cheban
* @author Khyojae
*/
class AuthoritiesAuthorizationManagerTests {
@@ -83,4 +87,20 @@ class AuthoritiesAuthorizationManagerTests {
assertThat(manager.authorize(authentication, Collections.singleton("ROLE_USER")).isGranted()).isTrue();
}
@Test
// gh-18543
void authorizeWhenAuthorityIsNullThenDoesNotThrowNullPointerException() {
AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager();
Authentication authentication = new TestingAuthenticationToken("user", "password",
Collections.singletonList(() -> null));
Collection<String> authoritiesContainsThrowsNPE = Set.of("ROLE_USER");
// must be Collection that throws NPE when .contains(null) is invoked
// to replicate the issue in gh-18543
assertThatNullPointerException().isThrownBy(() -> authoritiesContainsThrowsNPE.contains(null));
assertThat(manager.authorize(() -> authentication, authoritiesContainsThrowsNPE).isGranted()).isFalse();
}
}
@@ -61,7 +61,7 @@ public class AuthorityAuthorizationManagerTests {
@Test
public void hasAnyRoleWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(null))
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole((String[]) null))
.withMessage("roles cannot be empty");
}
@@ -97,7 +97,8 @@ public class AuthorityAuthorizationManagerTests {
@Test
public void hasAnyAuthorityWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null))
assertThatIllegalArgumentException()
.isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority((String[]) null))
.withMessage("authorities cannot be empty");
}
@@ -120,6 +120,13 @@ public class RsaKeyConvertersTests {
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(2048);
}
@Test
public void pkcs8WhenConvertingSingleLinePkcs8PrivateKeyThenOk() {
RSAPrivateKey key = this.pkcs8.convert(toInputStream(PKCS8_PRIVATE_KEY.replace("\n", "")));
Assertions.assertThat(key).isInstanceOf(RSAPrivateCrtKey.class);
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(2048);
}
@Test
public void pkcs8WhenConvertingPkcs1PrivateKeyThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.pkcs8.convert(toInputStream(PKCS1_PRIVATE_KEY)));
@@ -131,12 +138,24 @@ public class RsaKeyConvertersTests {
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingSingleLineX509PublicKeyThenOk() {
RSAPublicKey key = this.x509.convert(toInputStream(X509_PUBLIC_KEY.replace("\n", "")));
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingX509CertificateThenOk() {
RSAPublicKey key = this.x509.convert(toInputStream(X509_CERTIFICATE));
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingX509SingleLineCertificateThenOk() {
RSAPublicKey key = this.x509.convert(toInputStream(X509_CERTIFICATE.replace("\n", "")));
Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024);
}
@Test
public void x509WhenConvertingDerEncodedX509PublicKeyThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.x509.convert(toInputStream(MALFORMED_X509_KEY)));
@@ -73,7 +73,8 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Map::class.java)
val result = (filtered as Map<String, String>)
@Suppress("UNCHECKED_CAST")
val result = filtered as Map<String, String>
assertThat(result).hasSize(1)
assertThat(result).containsKey("key2")
assertThat(result).containsValue("value2")
@@ -95,7 +96,8 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Map::class.java)
val result = (filtered as Map<String, String>)
@Suppress("UNCHECKED_CAST")
val result = filtered as Map<String, String>
assertThat(result).hasSize(0)
}
@@ -119,7 +121,8 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Collection::class.java)
val result = (filtered as Collection<String>)
@Suppress("UNCHECKED_CAST")
val result = filtered as Collection<String>
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
}
@@ -140,7 +143,8 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Collection::class.java)
val result = (filtered as Collection<String>)
@Suppress("UNCHECKED_CAST")
val result = filtered as Collection<String>
assertThat(result).hasSize(0)
}
@@ -164,7 +168,8 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Array<String>::class.java)
val result = (filtered as Array<String>)
@Suppress("UNCHECKED_CAST")
val result = filtered as Array<String>
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
}
@@ -185,7 +190,8 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Array<String>::class.java)
val result = (filtered as Array<String>)
@Suppress("UNCHECKED_CAST")
val result = filtered as Array<String>
assertThat(result).hasSize(0)
}
@@ -209,6 +215,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Stream::class.java)
@Suppress("UNCHECKED_CAST")
val result = (filtered as Stream<String>).toList()
assertThat(result).hasSize(1)
assertThat(result).contains("string2")
@@ -230,6 +237,7 @@ class DefaultMethodSecurityExpressionHandlerKotlinTests {
)
assertThat(filtered).isInstanceOf(Stream::class.java)
@Suppress("UNCHECKED_CAST")
val result = (filtered as Stream<String>).toList()
assertThat(result).hasSize(0)
}
@@ -18,9 +18,23 @@ package org.springframework.security.crypto.password;
import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
import org.springframework.util.StringUtils;
/**
* An abstract {@link PasswordEncoder} that implementers can use for expecting the
* password to be non-{@code null}. Each common password API method is accompanied with an
* abstract method with a {@code NonNull} prefix. By implementing this, the concrete class
* is specifying what to do with the password when it is non-{@code null}, allowing this
* class to handle the {@code null} case.
*
* @author Rob Winch
* @since 7.0
*/
public abstract class AbstractValidatingPasswordEncoder implements PasswordEncoder {
@Override
@Contract("!null -> !null; null -> null")
public final @Nullable String encode(@Nullable CharSequence rawPassword) {
if (rawPassword == null) {
return null;
@@ -32,8 +46,7 @@ public abstract class AbstractValidatingPasswordEncoder implements PasswordEncod
@Override
public final boolean matches(@Nullable CharSequence rawPassword, @Nullable String encodedPassword) {
if (rawPassword == null || rawPassword.length() == 0 || encodedPassword == null
|| encodedPassword.length() == 0) {
if (!StringUtils.hasLength(rawPassword) || !StringUtils.hasLength(encodedPassword)) {
return false;
}
return matchesNonNull(rawPassword.toString(), encodedPassword);
@@ -43,7 +56,7 @@ public abstract class AbstractValidatingPasswordEncoder implements PasswordEncod
@Override
public final boolean upgradeEncoding(@Nullable String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
if (!StringUtils.hasLength(encodedPassword)) {
return false;
}
return upgradeEncodingNonNull(encodedPassword);
@@ -38,7 +38,7 @@ public interface PasswordEncoder {
* @return A non-null encoded password, unless the rawPassword was null in which case
* the result must be null.
*/
@Contract("null -> null; !null -> !null")
@Contract("!null -> !null; null -> null")
@Nullable String encode(@Nullable CharSequence rawPassword);
/**
@@ -337,8 +337,9 @@ Protecting multipart requests (file uploads) from CSRF attacks causes a https://
To prevent a CSRF attack from occurring, the body of the HTTP request must be read to obtain the actual CSRF token.
However, reading the body means that the file is uploaded, which means an external site can upload a file.
There are two options to using CSRF protection with multipart/form-data:
There are three options to using CSRF protection with multipart/form-data:
* <<csrf-considerations-multipart-header,Include CSRF Token in an HTTP Request Header>>
* <<csrf-considerations-multipart-body,Place CSRF Token in the Body>>
* <<csrf-considerations-multipart-url,Place CSRF Token in the URL>>
@@ -350,9 +351,17 @@ Before you integrate Spring Security's CSRF protection with multipart file uploa
More information about using multipart forms with Spring, see the https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web.html#mvc-multipart[1.1.11. Multipart Resolver] section of the Spring reference and the https://docs.spring.io/spring/docs/5.2.x/javadoc-api/org/springframework/web/multipart/support/MultipartFilter.html[`MultipartFilter` Javadoc].
====
[[csrf-considerations-multipart-header]]
==== Include CSRF Token in an HTTP Request Header
When JavaScript is available, you can submit multipart requests by sending the CSRF token in an HTTP request header.
This avoids placing the token in the URL and avoids processing multipart request bodies before CSRF validation.
This is generally the preferred approach for browser applications with JavaScript clients.
See xref:servlet/exploits/csrf.adoc#csrf-integration-javascript-other[JavaScript applications] for Servlet applications and xref:reactive/exploits/csrf.adoc#webflux-csrf-include-ajax[AJAX and JSON Requests] for Reactive applications.
[[csrf-considerations-multipart-body]]
==== Place CSRF Token in the Body
The first option is to include the actual CSRF token in the body of the request.
Another option is to include the actual CSRF token in the body of the request.
By placing the CSRF token in the body, the body is read before authorization is performed.
This means that anyone can place temporary files on your server.
However, only authorized users can submit a file that is processed by your application.
@@ -360,7 +369,7 @@ In general, this is the recommended approach, because the temporary file upload
[[csrf-considerations-multipart-url]]
==== Include CSRF Token in URL
If letting unauthorized users upload temporary files is not acceptable, an alternative is to include the expected CSRF token as a query parameter in the action attribute of the form.
If letting unauthorized users upload temporary files is not acceptable and JavaScript is not available, an alternative is to include the expected CSRF token as a query parameter in the action attribute of the form.
The disadvantage to this approach is that query parameters can be leaked.
More generally, it is considered best practice to place sensitive data within the body or headers to ensure it is not leaked.
You can find additional information in https://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3[RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI's].
@@ -0,0 +1,4 @@
= Kerberos Migrations
For users leveraging Spring Security's Kerberos support, the Maven and Gradle Coordinates have been changed since the support was moved from an external module into Spring Security.
See the xref:servlet/authentication/kerberos/introduction.adoc[Keberos documentation] for the new Maven and Gradle coordinates.
@@ -391,7 +391,8 @@ For details, see the <<webflux-csrf-configure-custom-repository>> section.
[[webflux-csrf-considerations-multipart]]
=== Multipart (file upload)
We have xref:features/exploits/csrf.adoc#csrf-considerations-multipart[already discussed] how protecting multipart requests (file uploads) from CSRF attacks causes a https://en.wikipedia.org/wiki/Chicken_or_the_egg[chicken and the egg] problem.
This section discusses how to implement placing the CSRF token in the <<webflux-csrf-considerations-multipart-body,body>> and <<webflux-csrf-considerations-multipart-url,url>> within a WebFlux application.
When JavaScript is available, we _recommend_ including the CSRF token in an HTTP request header (see <<webflux-csrf-include-ajax,AJAX and JSON Requests>>) to side-step the issue.
If JavaScript is not available, this section discusses how to place the CSRF token in the <<webflux-csrf-considerations-multipart-body,body>> and <<webflux-csrf-considerations-multipart-url,url>> within a WebFlux application.
[NOTE]
====
+14 -295
View File
@@ -51,28 +51,9 @@ image::{figures}/delegatingfilterproxy.png[]
The following listing shows pseudo code of `DelegatingFilterProxy`:
.`DelegatingFilterProxy` Pseudo Code
[tabs]
======
Java::
+
[source,java,role="primary"]
----
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); // <1>
delegate.doFilter(request, response); // <2>
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val delegate: Filter = getFilterBean(someBeanName) // <1>
delegate.doFilter(request, response) // <2>
}
----
======
include-code::./SampleDelegatingFilterProxy[tag=dofilter,indent=0]
<1> Lazily get Filter that was registered as a Spring Bean.
For the example in <<servlet-delegatingfilterproxy-figure>> `delegate` is an instance of __Bean Filter~0~__.
<2> Delegate work to the Spring Bean.
@@ -155,58 +136,7 @@ However, there are times that it is beneficial to know the ordering, if you want
These security filters are most often declared using an javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[`HttpSecurity`] instance.
To exemplify the above paragraph, let's consider the following security configuration:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
);
return http.build();
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
import org.springframework.security.config.web.servlet.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
httpBasic { }
formLogin { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
return http.build()
}
}
----
======
include-code::./SecurityConfig[tag=snippet,indent=0]
The above configuration will result in the following `Filter` ordering:
@@ -233,7 +163,7 @@ If you want to see the list of filters invoked for a particular request, you can
=== Printing the Security Filters
Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request.
For example, you want to make sure that the <<adding-custom-filter,filter you have added>> is in the list of the security filters.
For example, you want to make sure that the <<adding-filters-to-chain,filter you have added>> is in the list of the security filters.
The list of filters is printed at DEBUG level on the application startup, so you can see something like the following on the console output for example:
@@ -248,7 +178,7 @@ But that is not all, you can also configure your application to print the invoca
That is helpful to see if the filter you have added is invoked for a particular request or to check where an exception is coming from.
To do that, you can configure your application to <<servlet-logging,log the security events>>.
[[adding-custom-filter]]
[[adding-filters-to-chain]]
=== Adding Filters to the Filter Chain
Most of the time, the default <<servlet-security-filters>> are enough to provide security to your application.
@@ -260,6 +190,7 @@ javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity
* `#addFilterAfter(Filter, Class<?>)` adds your filter after another filter
* `#addFilterAt(Filter, Class<?>)` replaces another filter with your filter
[[adding-custom-filter]]
==== Adding a Custom Filter
If you are creating a filter of your own, you will need to determine its location in the filter chain.
@@ -298,39 +229,7 @@ For example, let's say that you want to add a `Filter` that gets a tenant id hea
First, let's create the `Filter`:
[source,java]
----
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); <1>
boolean hasAccess = isUserAllowed(tenantId); <2>
if (hasAccess) {
filterChain.doFilter(request, response); <3>
return;
}
throw new AccessDeniedException("Access denied"); <4>
}
}
----
include-code::./TenantFilter[tag=snippet,indent=0]
The sample code above does the following:
@@ -349,34 +248,7 @@ The previous description already gives us a clue on where to add the filter, sin
Based on the rule of thumb, add it after xref:servlet/authentication/anonymous.adoc[ `AnonymousAuthenticationFilter`], the last authentication filter in the chain, like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); <1>
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) <1>
return http.build()
}
----
======
include-code::./SecurityConfig[tag=snippet,indent=0]
<1> Use `HttpSecurity#addFilterAfter` to add the `TenantFilter` after the `AnonymousAuthenticationFilter`.
@@ -405,127 +277,23 @@ public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilte
This makes so that `HttpSecurity` is the only one adding it.
[[customizing-filter]]
==== Customizing a Spring Security Filter
Generally, you can use a filter's DSL method to configure Spring Security's filters.
For example, the simplest way to add `BasicAuthenticationFilter` is by asking the DSL to do it:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(Customizer.withDefaults())
// ...
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic { }
// ...
}
return http.build()
}
----
======
include-code::./CustomizingFilterTests[tag=basic-default,indent=0]
However, in the event that you want to construct a Spring Security filter yourself, you specify it in the DSL using `addFilterAt` like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
----
======
include-code::./CustomizingFilterTests[tag=custom-filter,indent=0]
Note that if that filter has already been added, then Spring Security will throw an exception.
For example, calling xref:servlet/authentication/passwords/basic.adoc[ `HttpSecurity#httpBasic`] adds a `BasicAuthenticationFilter` for you.
So, the following arrangement fails since there are two calls that are both trying to add `BasicAuthenticationFilter`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
.httpBasic(Customizer.withDefaults())
// ... on no! BasicAuthenticationFilter is added twice!
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http {
httpBasic { }
}
// ... on no! BasicAuthenticationFilter is added twice!
http.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
----
======
include-code::./CustomizingFilterTests[tag=incorrect,indent=0]
In this case, remove the call to `httpBasic` since you are constructing `BasicAuthenticationFilter` yourself.
@@ -533,10 +301,7 @@ In this case, remove the call to `httpBasic` since you are constructing `BasicAu
====
In the event that you are unable to reconfigure `HttpSecurity` to not add a certain filter, you can typically disable the Spring Security filter by calling its DSL's `disable` method like so:
[source,java]
----
.httpBasic((basic) -> basic.disable())
----
include-code::./CustomizingFilterTests[tag=disable,indent=0]
====
[[servlet-exceptiontranslationfilter]]
@@ -618,53 +383,7 @@ Or you may want to shut off this feature since you always want to redirect the u
To do that, you can use the javadoc:org.springframework.security.web.savedrequest.NullRequestCache[NullRequestCache] implementation.
.Prevent the Request From Being Saved
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
----
XML::
+
[source,xml,role="secondary"]
----
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
----
======
include-code::./SecurityConfig[tag=snippet,indent=0]
[[requestcacheawarefilter]]
=== RequestCacheAwareFilter
@@ -3,3 +3,38 @@
Spring Security Kerberos {spring-security-version} is built and tested with JDK 17,
Spring Security {spring-security-version} and Spring Framework {spring-core-version}.
The dependency coordinates changed with Spring Security 7:
[tabs]
======
Maven::
+
.pom.xml
[source,xml,subs="verbatim,attributes"]
----
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-kerberos-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-kerberos-web</artifactId>
</dependency>
</dependencies>
----
Gradle::
+
.build.gradle
[source,groovy]
[subs="verbatim,attributes"]
----
dependencies {
implementation "org.springframework.security:spring-security-kerberos-core"
implementation "org.springframework.security:spring-security-kerberos-web"
}
----
======
@@ -949,6 +949,21 @@ fun getMessages(): List<Message> { }
----
======
[[method-security-has-scope]]
=== Using `hasScope` in Method Security
Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:
include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]
and then doing:
include-code::./MessageService[tag=protected-method,indent=0]
If you are using xref:servlet/authentication/mfa.adoc[Spring Security's MFA feature], then you can supply its `AuthorizationManagerFactory` instance to ensure that your authentication factors are automatically checked as well by including it in your `DefaultOAuth2AuthorizationManagerFactory` constructor as follows:
include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]
[[oauth2resourceserver-jwt-authorization-extraction]]
=== Extracting Authorities Manually
@@ -638,6 +638,21 @@ fun getMessages(): List<Message?> {}
----
======
[[method-security-has-scope]]
=== Using `hasScope` in Method Security
Because method security expressions can evaluation `AuthorizationManager` instances, you can also use the `hasScope` API by publishing a `DefaultOAuth2AuthorizationManagerFactory` `@Bean`:
include-code::./MethodSecurityHasScopeConfiguration[tag=declare-factory,indent=0]
and then doing:
include-code::./MessageService[tag=protected-method,indent=0]
If you are using xref:servlet/authentication/mfa.adoc[Spring Security's MFA feature], then you can supply its `AuthorizationManagerFactory` instance to ensure that your authentication factors are automatically checked as well by including it in your `DefaultOAuth2AuthorizationManagerFactory` constructor as follows:
include-code::./MethodSecurityHasScopeMfaConfiguration[tag=declare-factory,indent=0]
[[oauth2resourceserver-opaque-authorization-extraction]]
=== Extracting Authorities Manually
+1 -1
View File
@@ -1,4 +1,4 @@
[[new]]
= What's New in Spring Security 7.1
This is a placeholder for updates to Spring Security 7.1
* https://github.com/spring-projects/spring-security/pull/18634[gh-18634] - Added javadoc:org.springframework.security.web.util.matcher.InetAddressMatcher[]
+1
View File
@@ -6,6 +6,7 @@ plugins {
id 'java-toolchain'
id 'test-compile-target-jdk25'
id 'compile-warnings-error'
id 'javadoc-warnings-error'
}
apply plugin: 'io.spring.convention.docs'
@@ -19,7 +19,6 @@ package org.springframework.security.docs.features.authentication.password4jbcry
import com.password4j.BcryptFunction;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder;
@@ -33,7 +32,7 @@ public class BcryptUsageTests {
@Test
void defaultParams() {
// tag::default-params[]
PasswordEncoder encoder = new BCryptPasswordEncoder();
PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::default-params[]
@@ -0,0 +1,102 @@
/*
* Copyright 2004-present 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.docs.servlet.addingcustomfilter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
CustomFilterTests.UserDetailsConfig.class,
CustomFilterTests.ApiController.class,
SecurityConfig.class })
@WebAppConfiguration
public class CustomFilterTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@BeforeEach
void setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context)
.defaultRequest(get("/api").with(user("user")))
.apply(springSecurity())
.build();
}
@Test
void tenantFilterWhenHeaderMissingThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.mvc.perform(get("/api")).andReturn());
}
@Test
void tenantFilterWhenHeaderPresentThenContinuesFilterChain() throws Exception {
this.mvc.perform(get("/api").header("X-Tenant-Id", "some-tenant-id"))
.andExpect(status().isOk())
.andExpect(authenticated().withUsername("user"));
}
@Configuration
static class UserDetailsConfig {
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
@RestController
static class ApiController {
@GetMapping("/api")
String api() {
return "ok";
}
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2004-present 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.docs.servlet.addingcustomfilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.test.context.ContextConfiguration;
@Configuration
@ContextConfiguration(classes = { SecurityConfig.class })
@EnableWebSecurity
public class SecurityConfig {
// tag::snippet[]
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); // <1>
return http.build();
}
// end::snippet[]
}
@@ -0,0 +1,53 @@
/*
* Copyright 2004-present 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.docs.servlet.addingcustomfilter;
// tag::snippet[]
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); // <1>
boolean hasAccess = isUserAllowed(tenantId); // <2>
if (hasAccess) {
filterChain.doFilter(request, response); // <3>
return;
}
throw new AccessDeniedException("Access denied"); // <4>
}
private boolean isUserAllowed(String tenantId) {
return "some-tenant-id".equals(tenantId);
}
}
// end::snippet[]
@@ -0,0 +1,203 @@
/*
* Copyright 2004-present 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.docs.servlet.customizingfilter;
import java.io.IOException;
import java.util.List;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
/**
* Tests for customizing security filters.
*
*/
@ExtendWith(SpringTestContextExtension.class)
public class CustomizingFilterTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
MockMvc mvc;
@Autowired
FilterChainProxy filterChainProxy;
@Test
public void filterChainWhenBasicDefaultThenBasicAuthenticationFilterPresent() {
this.spring.register(SecurityConfigBasicDefault.class).autowire();
List<Filter> filters = this.filterChainProxy.getFilters("/");
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter.class);
}
@Test
public void filterChainWhenCustomFilterThenCustomFilterPresent() {
this.spring.register(SecurityConfigCustom.class).autowire();
List<Filter> filters = this.filterChainProxy.getFilters("/");
assertThat(filters).extracting("class").contains(SecurityConfigCustom.MyBasicAuthenticationFilter.class);
assertThat(filters).extracting("class").doesNotContain(BasicAuthenticationFilter.class);
}
@Test
public void requestWhenDisableThenNoWwwAuthenticateHeader() throws Exception {
this.spring.register(SecurityConfigDisable.class).autowire();
this.mvc.perform(get("/")).andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE));
}
@Test
public void filterChainWhenIncorrectThenBothFiltersPresent() {
this.spring.register(SecurityConfigIncorrect.class).autowire();
List<Filter> filters = this.filterChainProxy.getFilters("/");
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter.class);
assertThat(filters).extracting("class").contains(SecurityConfigIncorrect.MyBasicAuthenticationFilter.class);
}
@Configuration
@EnableWebSecurity
static class SecurityConfigBasicDefault {
// tag::basic-default[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(Customizer.withDefaults());
// ...
return http.build();
}
// end::basic-default[]
}
@Configuration
@EnableWebSecurity
static class SecurityConfigCustom {
// tag::custom-filter[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
MyBasicAuthenticationFilter basic = new MyBasicAuthenticationFilter();
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
// end::custom-filter[]
static class MyBasicAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
}
}
}
@Configuration
@EnableWebSecurity
static class SecurityConfigDisable {
// tag::disable[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic((basic) -> basic.disable());
// ...
return http.build();
}
// end::disable[]
}
@Configuration
@EnableWebSecurity
static class SecurityConfigIncorrect {
// tag::incorrect[]
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
MyBasicAuthenticationFilter basic = new MyBasicAuthenticationFilter();
// ... configure
http
.httpBasic(Customizer.withDefaults())
// ... on no! BasicAuthenticationFilter is added twice!
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
// end::incorrect[]
static class MyBasicAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
}
}
}
@Configuration
static class UserDetailsConfig {
@Bean
InMemoryUserDetailsManager userDetailsManager() {
return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build());
}
}
}
@@ -0,0 +1,16 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
class MessageService {
// tag::protected-method[]
@PreAuthorize("@oauth2.hasScope('message:read')")
String readMessage() {
return "message";
}
// end::protected-method[]
}
@@ -0,0 +1,18 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;
@Configuration
@EnableMethodSecurity
class MethodSecurityHasScopeConfiguration {
// tag::declare-factory[]
@Bean
OAuth2AuthorizationManagerFactory<?> oauth2() {
return new DefaultOAuth2AuthorizationManagerFactory<>();
}
// end::declare-factory[]
}
@@ -0,0 +1,59 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ExtendWith(SpringTestContextExtension.class)
@ExtendWith(SpringExtension.class)
@SecurityTestExecutionListeners
public class MethodSecurityHasScopeConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this).mockMvcAfterSpringSecurityOk();
@Autowired
private MessageService messages;
@Test
@WithMockUser(authorities = "SCOPE_message:read")
void readMessageWhenMessageReadThenAllowed() {
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
this.messages.readMessage();
}
@Test
@WithMockUser
void readMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}
@Test
@WithMockUser(authorities = { "SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509" })
void mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
this.messages.readMessage();
}
@Test
@WithMockUser(authorities = { "SCOPE_message:read" })
void mfaReadMessageWhenMessageReadThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}
@Test
@WithMockUser
void mfaReadMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration.class, MessageService.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.messages::readMessage);
}
}
@@ -0,0 +1,21 @@
package org.springframework.security.docs.servlet.oauth2.resourceserver.methodsecurityhasscope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory;
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory;
@Configuration
@EnableMethodSecurity
@EnableMultiFactorAuthentication(authorities = { "FACTOR_BEARER", "FACTOR_X509" })
class MethodSecurityHasScopeMfaConfiguration {
// tag::declare-factory[]
@Bean
OAuth2AuthorizationManagerFactory<?> oauth2(AuthorizationManagerFactory<?> authz) {
return new DefaultOAuth2AuthorizationManagerFactory<>(authz);
}
// end::declare-factory[]
}
@@ -0,0 +1,40 @@
/*
* Copyright 2004-present 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.docs.servlet.requestcachepreventsavedrequest;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
public class SecurityConfig {
// tag::snippet[]
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
// end::snippet[]
}
@@ -0,0 +1,53 @@
/*
* Copyright 2004-present 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.docs.servlet.servletdelegatingfilterproxy;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
/**
* A very simple implementation of a DelegatingFilterProxy.
*/
public class SampleDelegatingFilterProxy extends GenericFilterBean {
private StaticWebApplicationContext wac;
private final String someBeanName;
public SampleDelegatingFilterProxy(String someBeanName, StaticWebApplicationContext webApplicationContext) {
this.wac = webApplicationContext;
this.someBeanName = someBeanName;
}
// tag::dofilter[]
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Filter delegate = getFilterBean(someBeanName); // <1>
delegate.doFilter(request, response, chain); // <2>
}
// end::dofilter[]
private Filter getFilterBean(String someBeanName) {
return this.wac.getBean(someBeanName, Filter.class);
}
}
@@ -0,0 +1,129 @@
/*
* Copyright 2004-present 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.docs.servlet.servletdelegatingfilterproxy;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.web.servlet.MockServletContext;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.StaticWebApplicationContext;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class SampleDelegatingFilterProxyTests {
@Test
void testFilter() throws ServletException, IOException {
ServletContext sc = new MockServletContext();
StaticWebApplicationContext wac = new StaticWebApplicationContext();
wac.registerSingleton("targetFilter", MockFilter.class);
wac.setServletContext(sc);
wac.refresh();
sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);
MockFilter targetFilter = (MockFilter) wac.getBean("targetFilter");
MockFilterConfig proxyConfig = new MockFilterConfig(sc);
proxyConfig.addInitParameter("targetBeanName", "targetFilter");
SampleDelegatingFilterProxy filterProxy = new SampleDelegatingFilterProxy("targetFilter", wac);
filterProxy.init(proxyConfig);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
filterProxy.doFilter(request, response, null);
assertThat(targetFilter.filterConfig).isNull();
assertThat(request.getAttribute("called")).isEqualTo(Boolean.TRUE);
filterProxy.destroy();
assertThat(targetFilter.filterConfig).isNull();
}
private static class MockFilter implements Filter {
private FilterConfig filterConfig;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws java.io.IOException, ServletException {
request.setAttribute("called", Boolean.TRUE);
}
@Override
public void destroy() {
this.filterConfig = null;
}
}
private static class MockFilterConfig implements FilterConfig {
private final ServletContext servletContext;
private final String filterName;
private final Map<String, String> initParameters = new LinkedHashMap<>();
public MockFilterConfig(ServletContext servletContext) {
this.servletContext = servletContext;
this.filterName = "";
}
@Override
public String getFilterName() {
return this.filterName;
}
@Override
public ServletContext getServletContext() {
return this.servletContext;
}
public void addInitParameter(String name, String value) {
Assert.notNull(name, "Parameter name must not be null");
this.initParameters.put(name, value);
}
@Override
public String getInitParameter(String name) {
Assert.notNull(name, "Parameter name must not be null");
return this.initParameters.get(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return Collections.enumeration(this.initParameters.keySet());
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2004-present 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.docs.servlet.servletsecurityfilters;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { SampleSecurityConfigTests.UserDetailsConfig.class, SecurityConfig.class })
@WebAppConfiguration
public class SampleSecurityConfigTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@BeforeEach
public void setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context)
.defaultRequest(get("/api").with(user("user")))
.defaultRequest(post("/api").with(csrf()))
.apply(springSecurity())
.build();
}
@Test
void testGet() throws Exception {
this.mvc.perform(get("/api")
.with(httpBasic("user", "password")))
// Security check was successful
.andExpect(status().isNotFound())
.andExpect(authenticated().withUsername("user"));
}
@Test
void testUnauthenticated() throws Exception {
this.mvc.perform(get("/api"))
// Security check was successful
.andExpect(status().isUnauthorized());
}
@Test
void testCsrf() throws Exception {
this.mvc.perform(post("/api")
.with(csrf())
.with(httpBasic("user", "password"))
).andExpect(status().isNotFound());
}
@Configuration
static class UserDetailsConfig {
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.build();
return new InMemoryUserDetailsManager(user);
}
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2004-present 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.docs.servlet.servletsecurityfilters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
// tag::snippet[]
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
);
return http.build();
}
}
// end::snippet[]
@@ -3,7 +3,6 @@ package org.springframework.security.kt.docs.features.authentication.password4jb
import com.password4j.BcryptFunction
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder
@@ -14,7 +13,7 @@ class BcryptUsageTests {
@Test
fun defaultParams() {
// tag::default-params[]
val encoder: PasswordEncoder = BCryptPasswordEncoder()
val encoder: PasswordEncoder = BcryptPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::default-params[]
@@ -0,0 +1,106 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.addingcustomfilter
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@ExtendWith(SpringExtension::class)
@ContextConfiguration(
classes = [
CustomFilterTests.UserDetailsConfig::class,
CustomFilterTests.ApiController::class,
SecurityConfig::class
]
)
@WebAppConfiguration
class CustomFilterTests {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mvc: MockMvc
@BeforeEach
fun setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build();
}
@Test
fun tenantFilterWhenHeaderMissingThenAccessDenied() {
assertThatExceptionOfType(Exception::class.java)
.isThrownBy { this.mvc.perform(get("/api").with(user("user"))).andReturn() }
}
@Test
fun tenantFilterWhenHeaderPresentThenContinuesFilterChain() {
this.mvc.perform(get("/api")
.with(user("user"))
.header("X-Tenant-Id", "some-tenant-id"))
.andExpect(status().isOk)
.andExpect(authenticated().withUsername("user"))
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): UserDetailsService {
@Suppress("DEPRECATION")
val user: UserDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
}
@RestController
class ApiController {
@GetMapping("/api")
fun api(): String {
return "ok"
}
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.addingcustomfilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter
@Configuration
@EnableWebSecurity
open class SecurityConfig {
// tag::snippet[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) // <1>
return http.build()
}
// end::snippet[]
}
@@ -0,0 +1,51 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.addingcustomfilter
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import java.io.IOException
// tag::snippet[]
class TenantFilter : Filter {
@Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {
val request = servletRequest as HttpServletRequest
val response = servletResponse as HttpServletResponse
val tenantId = request.getHeader("X-Tenant-Id") // <1>
val hasAccess = isUserAllowed(tenantId) // <2>
if (hasAccess) {
filterChain.doFilter(request, response) // <3>
return
}
throw AccessDeniedException("Access denied") // <4>
}
private fun isUserAllowed(tenantId: String?): Boolean {
return "some-tenant-id" == tenantId
}
}
// end::snippet[]
@@ -0,0 +1,167 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.customizingfilter
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.test.web.servlet.MockMvc
/**
* Tests for customizing security filters.
*
*/
@ExtendWith(SpringTestContextExtension::class)
class CustomizingFilterTests {
@JvmField
val spring = SpringTestContext(this)
@Autowired
lateinit var mvc: MockMvc
@Autowired
lateinit var filterChainProxy: FilterChainProxy
@Test
fun `filter chain when basic default then BasicAuthenticationFilter present`() {
spring.register(SecurityConfigBasicDefault::class.java).autowire()
val filters = filterChainProxy.getFilters("/")
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter::class.java)
}
@Test
fun `filter chain when custom filter then custom filter present`() {
spring.register(SecurityConfigCustom::class.java).autowire()
val filters = filterChainProxy.getFilters("/")
assertThat(filters).extracting("class").contains(SecurityConfigCustom.MyBasicAuthenticationFilter::class.java)
assertThat(filters).extracting("class").doesNotContain(BasicAuthenticationFilter::class.java)
}
@Test
fun `filter chain when incorrect then both filters present`() {
spring.register(SecurityConfigIncorrect::class.java).autowire()
val filters = filterChainProxy.getFilters("/")
assertThat(filters).extracting("class").contains(BasicAuthenticationFilter::class.java)
assertThat(filters).extracting("class").contains(SecurityConfigIncorrect.MyBasicAuthenticationFilter::class.java)
}
@Configuration
@EnableWebSecurity
open class SecurityConfigBasicDefault {
// tag::basic-default[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic { }
// ...
}
return http.build()
}
// end::basic-default[]
}
@Configuration
@EnableWebSecurity
open class SecurityConfigCustom {
// tag::custom-filter[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = MyBasicAuthenticationFilter()
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
// end::custom-filter[]
class MyBasicAuthenticationFilter : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
chain.doFilter(request, response)
}
}
}
@Configuration @EnableWebSecurity
open class SecurityConfigDisable {
// tag::disable[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic {
disable()
}
// ...
}
return http.build()
}
// end::disable[]
}
@Configuration
@EnableWebSecurity
open class SecurityConfigIncorrect {
// tag::incorrect[]
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = MyBasicAuthenticationFilter()
// ... configure
http {
httpBasic { }
}
// ... on no! BasicAuthenticationFilter is added twice!
http.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
// end::incorrect[]
class MyBasicAuthenticationFilter : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
chain.doFilter(request, response)
}
}
}
}
@@ -0,0 +1,15 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Service
@Service
open class MessageService {
// tag::protected-method[]
@PreAuthorize("@oauth2.hasScope('message:read')")
open fun readMessage(): String {
return "message"
}
// end::protected-method[]
}
@@ -0,0 +1,18 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory
@Configuration
@EnableMethodSecurity
open class MethodSecurityHasScopeConfiguration {
// tag::declare-factory[]
@Bean
open fun oauth2(): OAuth2AuthorizationManagerFactory<Any> {
return DefaultOAuth2AuthorizationManagerFactory()
}
// end::declare-factory[]
}
@@ -0,0 +1,61 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners
import org.springframework.security.test.context.support.WithMockUser
import org.springframework.test.context.junit.jupiter.SpringExtension
@ExtendWith(SpringTestContextExtension::class)
@ExtendWith(SpringExtension::class)
@SecurityTestExecutionListeners
class MethodSecurityHasScopeConfigurationTests {
@JvmField
val spring: SpringTestContext = SpringTestContext(this).mockMvcAfterSpringSecurityOk()
@Autowired
var messages: MessageService? = null
@Test
@WithMockUser(authorities = ["SCOPE_message:read"])
fun readMessageWhenMessageReadThenAllowed() {
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
this.messages!!.readMessage()
}
@Test
@WithMockUser
fun readMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}
@Test
@WithMockUser(authorities = ["SCOPE_message:read", "FACTOR_BEARER", "FACTOR_X509"])
fun mfaReadMessageWhenMessageReadAndFactorsThenAllowed() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
this.messages!!.readMessage()
}
@Test
@WithMockUser(authorities = ["SCOPE_message:read"])
fun mfaReadMessageWhenMessageReadThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}
@Test
@WithMockUser
fun mfaReadMessageWhenNoScopeThenDenied() {
this.spring.register(MethodSecurityHasScopeMfaConfiguration::class.java, MessageService::class.java).autowire()
Assertions.assertThatExceptionOfType<AccessDeniedException?>(AccessDeniedException::class.java)
.isThrownBy({ this.messages!!.readMessage() })
}
}
@@ -0,0 +1,20 @@
package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.methodsecurityhasscope
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authorization.AuthorizationManagerFactory
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.oauth2.core.authorization.DefaultOAuth2AuthorizationManagerFactory
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagerFactory
@Configuration
@EnableMethodSecurity
@EnableMultiFactorAuthentication(authorities = ["FACTOR_BEARER", "FACTOR_X509"])
open class MethodSecurityHasScopeMfaConfiguration {
// tag::declare-factory[]
@Bean
open fun oauth2(authz: AuthorizationManagerFactory<Any>): OAuth2AuthorizationManagerFactory<Any> {
return DefaultOAuth2AuthorizationManagerFactory(authz)
} // end::declare-factory[]
}
@@ -0,0 +1,40 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.requestcachepreventsavedrequest
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.savedrequest.NullRequestCache
open class SecurityConfig {
// tag::snippet[]
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
// end::snippet[]
}
@@ -0,0 +1,49 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.servletdelegatingfilterproxy;
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
/**
* A very simple implementation of a DelegatingFilterProxy.
*/
class SampleDelegatingFilterProxy(
private val someBeanName: String,
private var wac: StaticWebApplicationContext
) : GenericFilterBean() {
// tag::dofilter[]
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain?) {
val delegate: Filter = getFilterBean(someBeanName) // <1>
delegate.doFilter(request, response, chain) // <2>
}
// end::dofilter[]
private fun getFilterBean(someBeanName: String): Filter {
return wac.getBean(someBeanName, Filter::class.java)
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.servletdelegatingfilterproxy
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.FilterConfig
import jakarta.servlet.ServletContext
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.junit.jupiter.api.Test
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.web.servlet.MockServletContext
import org.springframework.util.Assert
import org.springframework.web.context.WebApplicationContext
import org.springframework.web.context.support.StaticWebApplicationContext
import java.io.IOException
import org.assertj.core.api.Assertions.assertThat
import java.util.Enumeration
import java.util.Collections
import kotlin.collections.LinkedHashMap
class SampleDelegatingFilterProxyTests {
@Test
@Throws(ServletException::class, IOException::class)
fun testFilter() {
val sc: ServletContext = MockServletContext()
val wac = StaticWebApplicationContext()
wac.registerSingleton("targetFilter", MockFilter::class.java)
wac.setServletContext(sc)
wac.refresh()
sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac)
val targetFilter = wac.getBean("targetFilter") as MockFilter
val proxyConfig = MockFilterConfig(sc)
proxyConfig.addInitParameter("targetBeanName", "targetFilter")
val filterProxy = SampleDelegatingFilterProxy("targetFilter", wac)
filterProxy.init(proxyConfig)
val request = MockHttpServletRequest()
val response = MockHttpServletResponse()
filterProxy.doFilter(request, response, null)
assertThat(targetFilter.filterConfig).isNull()
assertThat(request.getAttribute("called")).isEqualTo(true)
filterProxy.destroy()
assertThat(targetFilter.filterConfig).isNull()
}
private class MockFilter : Filter {
var filterConfig: FilterConfig? = null
@Throws(ServletException::class)
override fun init(filterConfig: FilterConfig) {
this.filterConfig = filterConfig
}
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain?) {
request.setAttribute("called", true)
}
override fun destroy() {
filterConfig = null
}
}
private class MockFilterConfig(
private val servletContext: ServletContext
) : FilterConfig {
private val filterName: String = ""
private val initParameters = LinkedHashMap<String, String>()
override fun getFilterName(): String = filterName
override fun getServletContext(): ServletContext = servletContext
fun addInitParameter(name: String, value: String) {
Assert.notNull(name, "Parameter name must not be null")
initParameters[name] = value
}
override fun getInitParameter(name: String): String? {
Assert.notNull(name, "Parameter name must not be null")
return initParameters[name]
}
override fun getInitParameterNames(): Enumeration<String> =
Collections.enumeration(initParameters.keys)
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2004-present 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.kt.docs.servlet.servletsecurityfilters
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
// tag::snippet[]
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
httpBasic { }
formLogin { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
return http.build()
}
}
// end::snippet[]
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2004-present 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.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- tag::snippet[] -->
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
<!-- end::snippet[] -->
</b:beans>
+18 -18
View File
@@ -4,39 +4,39 @@ io-rsocket = "1.1.5"
io-spring-javaformat = "0.0.47"
io-spring-nohttp = "0.0.11"
jakarta-websocket = "2.2.0"
org-apache-maven-resolver = "1.9.25"
org-apache-maven-resolver = "1.9.27"
org-aspectj = "1.9.25.1"
org-bouncycastle = "1.83"
org-eclipse-jetty = "11.0.26"
org-jetbrains-kotlin = "2.3.10"
org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.21.0"
org-opensaml5 = "5.2.0"
org-springframework = "7.0.3"
org-opensaml5 = "5.2.1"
org-springframework = "7.0.5"
com-password4j = "1.8.4"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.28"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.21.0"
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.21.1"
com-google-inject-guice = "com.google.inject:guice:3.0"
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:10.4"
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:11.26.1"
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:10.6"
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:11.33"
com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "com-squareup-okhttp3" }
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:7.0.4"
com-jayway-jsonpath-json-path = "com.jayway.jsonpath:json-path:2.10.0"
commons-collections = "commons-collections:commons-collections:3.2.2"
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.2.1"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.16.2"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.16.3"
io-mockk = "io.mockk:mockk:1.14.9"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.2"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.3"
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" }
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.13"
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.14"
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:3.0.0"
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.2.0"
@@ -65,27 +65,27 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.2.4.Final"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.2.5.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "org-jetbrains-kotlin" }
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:6.0.2"
org-junit-junit-bom = "org.junit:junit-bom:6.0.3"
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
org-opensaml-opensaml5-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml5" }
org-opensaml-opensaml5-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml5" }
org-python-jython = { module = "org.python:jython", version = "2.5.3" }
org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit3-driver:4.40.0"
org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4.40.0"
org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit3-driver:4.41.0"
org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4.41.0"
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
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.17"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.2"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.1"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.3"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.2"
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"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.4"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.1.0"
com-google-code-gson-gson = "com.google.code.gson:gson:2.13.2"
com-thaiopensource-trag = "com.thaiopensource:trang:20091111"
@@ -102,7 +102,7 @@ org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanne
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.11'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.29.7.RELEASE'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.0.RELEASE'
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
[plugins]
@@ -0,0 +1,43 @@
/*
* Copyright 2004-present 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.kerberos.aot.hint;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
/**
* {@link RuntimeHintsRegistrar} for Kerberos authentication classes.
*
* @author Josh Long
*/
class KerberosRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
// Krb5LoginModule is referenced as a plain string in LoginConfig and loaded by
// JAAS via Class.forName at runtime, so it needs an explicit reflection hint.
hints.reflection()
.registerType(TypeReference.of("com.sun.security.auth.module.Krb5LoginModule"),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS));
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* AOT and native image hint support for Kerberos authentication.
*/
@NullMarked
package org.springframework.security.kerberos.aot.hint;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,2 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.security.kerberos.aot.hint.KerberosRuntimeHints
@@ -30,6 +30,7 @@ import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -112,6 +113,11 @@ public class JwtClientAssertionAuthenticationProviderTests {
.setContext(new TestAuthorizationServerContext(this.authorizationServerSettings, null));
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
@@ -25,6 +25,7 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -111,6 +112,11 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
@@ -22,6 +22,7 @@ import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -95,6 +96,11 @@ public class OAuth2AuthorizationConsentAuthenticationProviderTests {
.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
@@ -25,6 +25,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -96,6 +97,11 @@ public class OAuth2DeviceVerificationAuthenticationProviderTests {
mockAuthorizationServerContext();
}
@AfterEach
public void tearDown() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryIsNullThenThrowIllegalArgumentException() {
// @formatter:off
@@ -254,6 +254,8 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient;
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
private Duration clockSkew;
private Clock clock;
@@ -274,6 +276,21 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
return this;
}
/**
* Sets a {@link ReactiveOAuth2AuthorizationSuccessHandler} to use for handling
* successful refresh token response, defaults to
* {@link RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler}.
* @param authorizationSuccessHandler the
* {@link ReactiveOAuth2AuthorizationSuccessHandler} to use
* @return the {@link RefreshTokenGrantBuilder}
* @since 7.1
*/
public RefreshTokenGrantBuilder authorizationSuccessHandler(
ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
this.authorizationSuccessHandler = authorizationSuccessHandler;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if
@@ -310,6 +327,9 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.authorizationSuccessHandler != null) {
authorizedClientProvider.setAuthorizationSuccessHandler(this.authorizationSuccessHandler);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
@@ -0,0 +1,305 @@
/*
* Copyright 2004-present 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.oauth2.client;
import java.time.Duration;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* A {@link ReactiveOAuth2AuthorizationSuccessHandler} that refreshes an {@link OidcUser}
* in the {@link SecurityContext} if the refreshed {@link OidcIdToken} is valid according
* to <a href=
* "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse">OpenID
* Connect Core 1.0 - Section 12.2 Successful Refresh Response</a>
*
* @author Evgeniy Cheban
* @since 7.1
* @see RefreshTokenReactiveOAuth2AuthorizedClientProvider
*/
public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
implements ReactiveOAuth2AuthorizationSuccessHandler {
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private static final String REFRESH_TOKEN_RESPONSE_ERROR_URI = "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse";
// @formatter:off
private static final Mono<ServerWebExchange> currentServerWebExchangeMono = Mono.deferContextual(Mono::just)
.filter((c) -> c.hasKey(ServerWebExchange.class))
.map((c) -> c.get(ServerWebExchange.class));
// @formatter:on
private ServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
private ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
private ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = new OidcReactiveOAuth2UserService();
private GrantedAuthoritiesMapper authoritiesMapper = (authorities) -> authorities;
private Duration clockSkew = Duration.ofSeconds(60);
@Override
public Mono<Void> onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, Authentication principal,
Map<String, Object> attributes) {
// The response must contain the openid scope.
if (!authorizedClient.getAccessToken().getScopes().contains(OidcScopes.OPENID)) {
return Mono.empty();
}
// The response must contain an id_token.
String idToken = extractIdToken(attributes);
if (!StringUtils.hasText(idToken)) {
return Mono.empty();
}
if (!(principal instanceof OAuth2AuthenticationToken authenticationToken)
|| authenticationToken.getClass() != OAuth2AuthenticationToken.class) {
// If the application customizes the authentication result, then a custom
// handler should be provided.
return Mono.empty();
}
// The current principal must be an OidcUser.
if (!(authenticationToken.getPrincipal() instanceof OidcUser existingOidcUser)) {
return Mono.empty();
}
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
// The registrationId must match the one used to log in.
if (!authenticationToken.getAuthorizedClientRegistrationId().equals(clientRegistration.getRegistrationId())) {
return Mono.empty();
}
// Create, validate OidcIdToken and refresh OidcUser in the SecurityContext.
return Mono.justOrEmpty((ServerWebExchange) attributes.get(ServerWebExchange.class.getName()))
.switchIfEmpty(currentServerWebExchangeMono)
.flatMap((exchange) -> {
ReactiveJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
return jwtDecoder.decode(idToken).onErrorMap(JwtException.class, (ex) -> {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(),
null);
return new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
})
.map((jwt) -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims()))
.doOnNext((oidcIdToken) -> validateIdToken(existingOidcUser, oidcIdToken))
.flatMap((oidcIdToken) -> {
OidcUserRequest userRequest = new OidcUserRequest(clientRegistration,
authorizedClient.getAccessToken(), oidcIdToken);
return this.userService.loadUser(userRequest);
})
.flatMap((oidcUser) -> refreshSecurityContext(exchange, clientRegistration, authenticationToken,
oidcUser));
});
}
/**
* Sets a {@link ServerSecurityContextRepository} to use for refreshing a
* {@link SecurityContext}, defaults to
* {@link WebSessionServerSecurityContextRepository}.
* @param serverSecurityContextRepository the {@link ServerSecurityContextRepository}
* to use
*/
public void setServerSecurityContextRepository(ServerSecurityContextRepository serverSecurityContextRepository) {
Assert.notNull(serverSecurityContextRepository, "serverSecurityContextRepository cannot be null");
this.serverSecurityContextRepository = serverSecurityContextRepository;
}
/**
* Sets a {@link ReactiveJwtDecoderFactory} to use for decoding refreshed oidc
* id-token, defaults to {@link ReactiveOidcIdTokenDecoderFactory}.
* @param jwtDecoderFactory the {@link ReactiveJwtDecoderFactory} to use
*/
public void setJwtDecoderFactory(ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory) {
Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
this.jwtDecoderFactory = jwtDecoderFactory;
}
/**
* Sets a {@link ReactiveOAuth2UserService} to use for loading an {@link OidcUser}
* from refreshed oidc id-token, defaults to {@link OidcReactiveOAuth2UserService}.
* @param userService the {@link ReactiveOAuth2UserService} to use
*/
public void setUserService(ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService) {
Assert.notNull(userService, "userService cannot be null");
this.userService = userService;
}
/**
* Sets a {@link GrantedAuthoritiesMapper} to use for mapping
* {@link GrantedAuthority}s, defaults to no-op implementation.
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} to use
*/
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
this.authoritiesMapper = authoritiesMapper;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OidcIdToken#getIssuedAt()} to match the existing
* {@link OidcUser#getIdToken()}'s issuedAt time, defaults to 60 seconds.
* @param clockSkew the maximum acceptable clock skew to use
*/
public void setClockSkew(Duration clockSkew) {
Assert.notNull(clockSkew, "clockSkew cannot be null");
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
this.clockSkew = clockSkew;
}
private String extractIdToken(Map<String, Object> attributes) {
if (attributes.get(OidcParameterNames.ID_TOKEN) instanceof String idToken) {
return idToken;
}
return null;
}
private void validateIdToken(OidcUser existingOidcUser, OidcIdToken idToken) {
// OpenID Connect Core 1.0 - Section 12.2 Successful Refresh Response
// If an ID Token is returned as a result of a token refresh request, the
// following requirements apply:
// its iss Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateIssuer(existingOidcUser, idToken);
// its sub Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateSubject(existingOidcUser, idToken);
// its iat Claim MUST represent the time that the new ID Token is issued,
validateIssuedAt(existingOidcUser, idToken);
// its aud Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateAudience(existingOidcUser, idToken);
// if the ID Token contains an auth_time Claim, its value MUST represent the time
// of the original authentication - not the time that the new ID token is issued,
validateAuthenticatedAt(existingOidcUser, idToken);
// it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of
// the original authentication contained nonce; however, if it is present, its
// value MUST be the same as in the ID Token issued at the time of the original
// authentication,
validateNonce(existingOidcUser, idToken);
}
private void validateIssuer(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getIssuer().toString().equals(existingOidcUser.getIdToken().getIssuer().toString())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issuer",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateSubject(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getSubject().equals(existingOidcUser.getIdToken().getSubject())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid subject",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateIssuedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getIssuedAt().isAfter(existingOidcUser.getIdToken().getIssuedAt().minus(this.clockSkew))) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issued at time",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!isValidAudience(existingOidcUser, idToken)) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid audience",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private boolean isValidAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
List<String> idTokenAudiences = idToken.getAudience();
Set<String> oidcUserAudiences = new HashSet<>(existingOidcUser.getIdToken().getAudience());
if (idTokenAudiences.size() != oidcUserAudiences.size()) {
return false;
}
for (String audience : idTokenAudiences) {
if (!oidcUserAudiences.contains(audience)) {
return false;
}
}
return true;
}
private void validateAuthenticatedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
if (idToken.getAuthenticatedAt() == null) {
return;
}
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateNonce(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!StringUtils.hasText(idToken.getNonce())) {
return;
}
if (!idToken.getNonce().equals(existingOidcUser.getIdToken().getNonce())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE, "Invalid nonce",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private Mono<Void> refreshSecurityContext(ServerWebExchange exchange, ClientRegistration clientRegistration,
OAuth2AuthenticationToken authenticationToken, OidcUser oidcUser) {
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oidcUser.getAuthorities());
OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(oidcUser, mappedAuthorities,
clientRegistration.getRegistrationId());
authenticationResult.setDetails(authenticationToken.getDetails());
SecurityContext securityContext = new SecurityContextImpl(authenticationResult);
return this.serverSecurityContextRepository.save(exchange, securityContext);
}
}
@@ -21,7 +21,9 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import reactor.core.publisher.Mono;
@@ -40,6 +42,7 @@ import org.springframework.util.Assert;
* {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
*
* @author Joe Grandja
* @author Evgeniy Cheban
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientProvider
* @see WebClientReactiveRefreshTokenTokenResponseClient
@@ -49,6 +52,8 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = new WebClientReactiveRefreshTokenTokenResponseClient();
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
@@ -96,12 +101,17 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
.flatMap(this.accessTokenResponseClient::getTokenResponse)
.onErrorMap(OAuth2AuthorizationException.class,
(e) -> new ClientAuthorizationException(e.getError(), clientRegistration.getRegistrationId(), e))
.map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()));
}
private boolean hasTokenExpired(OAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
.flatMap((tokenResponse) -> {
OAuth2AuthorizedClient refreshedClient = new OAuth2AuthorizedClient(clientRegistration,
context.getPrincipal().getName(), tokenResponse.getAccessToken(),
tokenResponse.getRefreshToken());
Map<String, Object> attributes = new HashMap<>(context.getAttributes());
attributes.putAll(tokenResponse.getAdditionalParameters());
return this.authorizationSuccessHandler
.onAuthorizationSuccess(refreshedClient, context.getPrincipal(),
Collections.unmodifiableMap(attributes))
.thenReturn(refreshedClient);
});
}
/**
@@ -116,6 +126,19 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets a {@link ReactiveOAuth2AuthorizationSuccessHandler} to use for handling
* successful refresh token response, defaults to
* {@link RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler}.
* @param authorizationSuccessHandler the
* {@link ReactiveOAuth2AuthorizationSuccessHandler} to use
* @since 7.1
*/
public void setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
Assert.notNull(authorizationSuccessHandler, "authorizationSuccessHandler cannot be null");
this.authorizationSuccessHandler = authorizationSuccessHandler;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
@@ -143,4 +166,8 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
this.clock = clock;
}
private boolean hasTokenExpired(OAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}
}
@@ -16,8 +16,13 @@
package org.springframework.security.oauth2.client.aot.hint;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.util.ClassUtils;
/**
* {@link RuntimeHintsRegistrar} for OAuth2 Client
@@ -27,9 +32,20 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
*/
class OAuth2ClientRuntimeHints implements RuntimeHintsRegistrar {
private static final boolean r2dbcPresent;
static {
ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
r2dbcPresent = ClassUtils.isPresent("io.r2dbc.spi.Row", classLoader)
&& ClassUtils.isPresent("org.springframework.r2dbc.core.DatabaseClient", classLoader);
}
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
registerOAuth2ClientSchemaFilesHints(hints);
if (r2dbcPresent) {
registerR2dbcHints(hints);
}
}
private void registerOAuth2ClientSchemaFilesHints(RuntimeHints hints) {
@@ -38,4 +54,23 @@ class OAuth2ClientRuntimeHints implements RuntimeHintsRegistrar {
.registerPattern("org/springframework/security/oauth2/client/oauth2-client-schema-postgres.sql");
}
private void registerR2dbcHints(RuntimeHints hints) {
// Register R2DBC OAuth2 client service types
hints.reflection()
.registerType(
TypeReference
.of("org.springframework.security.oauth2.client.R2dbcReactiveOAuth2AuthorizedClientService"),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS));
// Register OAuth2 client types that may be serialized in R2DBC scenarios
hints.reflection()
.registerTypes(java.util.List.of(
TypeReference.of("org.springframework.security.oauth2.client.OAuth2AuthorizedClient"),
TypeReference
.of("org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken")),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS));
}
}
@@ -105,7 +105,7 @@ public final class ClientRegistrations {
* @return the {@link ClientRegistration} built from the configuration
*/
public static ClientRegistration.Builder fromOidcConfiguration(Map<String, Object> configuration) {
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
OIDCProviderMetadata metadata = parseInput(configuration, OIDCProviderMetadata::parse);
ClientRegistration.Builder builder = withProviderConfiguration(metadata, metadata.getIssuer().getValue());
builder.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
if (metadata.getUserInfoEndpointURI() != null) {
@@ -292,6 +292,15 @@ public final class ClientRegistrations {
throw new IllegalArgumentException(errorMessage);
}
private static <T> T parseInput(Map<String, Object> body, ThrowingFunction<JSONObject, T, ParseException> parser) {
try {
return parse(body, parser);
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(ex);
}
}
private static <T> T parse(Map<String, Object> body, ThrowingFunction<JSONObject, T, ParseException> parser) {
try {
return parser.apply(new JSONObject(body));
@@ -318,14 +318,14 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React
return Mono.justOrEmpty(serverWebExchange)
.switchIfEmpty(currentServerWebExchangeMono)
.flatMap((exchange) -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
Map<String, Object> contextAttributes = new HashMap<>();
contextAttributes.put(ServerWebExchange.class.getName(), serverWebExchange);
String scope = exchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope)) {
contextAttributes = new HashMap<>();
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
StringUtils.delimitedListToStringArray(scope, " "));
}
return Mono.just(contextAttributes);
return Mono.just(Collections.unmodifiableMap(contextAttributes));
})
.defaultIfEmpty(Collections.emptyMap());
// @formatter:on
@@ -51,6 +51,8 @@ import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.ClientRequest;
@@ -96,6 +98,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rob Winch
* @author Joe Grandja
* @author Phil Clay
* @author Evgeniy Cheban
* @since 5.1
*/
public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
@@ -139,6 +142,8 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
private ClientResponseHandler clientResponseHandler;
private ServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
/**
* Constructs a {@code ServerOAuth2AuthorizedClientExchangeFilterFunction} using the
* provided parameters.
@@ -330,8 +335,11 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
}
private Mono<ClientResponse> exchangeAndHandleResponse(ClientRequest request, ExchangeFunction next) {
return next.exchange(request)
.transform((responseMono) -> this.clientResponseHandler.handleResponse(request, responseMono));
// Re-request an Authentication from serverSecurityContextRepository since it
// might have been changed during provider invocation.
return effectiveAuthentication(request).flatMap((authentication) -> next.exchange(request)
.transform((responseMono) -> this.clientResponseHandler.handleResponse(request, responseMono))
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)));
}
private Mono<OAuth2AuthorizedClient> authorizedClient(ClientRequest request) {
@@ -362,6 +370,17 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
// @formatter:on
}
private Mono<Authentication> effectiveAuthentication(ClientRequest request) {
// @formatter:off
return effectiveServerWebExchange(request)
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(this.serverSecurityContextRepository::load)
.mapNotNull(SecurityContext::getAuthentication)
.switchIfEmpty(this.currentAuthenticationMono);
// @formatter:on
}
/**
* Returns a {@link Mono} the emits the {@code clientRegistrationId} that is active
* for the given request.
@@ -445,6 +464,19 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
this.clientResponseHandler = new AuthorizationFailureForwarder(authorizationFailureHandler);
}
/**
* Sets a {@link ServerSecurityContextRepository} to use for re-obtaining a
* {@link SecurityContext} if it has been refreshed during provider invocation,
* defaults to {@link WebSessionServerSecurityContextRepository}.
* @param serverSecurityContextRepository the {@link ServerSecurityContextRepository}
* to use
* @since 7.1
*/
public void setServerSecurityContextRepository(ServerSecurityContextRepository serverSecurityContextRepository) {
Assert.notNull(serverSecurityContextRepository, "serverSecurityContextRepository cannot be null");
this.serverSecurityContextRepository = serverSecurityContextRepository;
}
@FunctionalInterface
private interface ClientResponseHandler {
@@ -0,0 +1,398 @@
/*
* Copyright 2004-present 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.oauth2.client;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler}.
*
* @author Evgeniy Cheban
*/
class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests {
@Test
void setServerSecurityContextRepositoryWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler()
.setServerSecurityContextRepository(null))
.withMessage("serverSecurityContextRepository cannot be null");
}
@Test
void setJwtDecoderFactoryWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setJwtDecoderFactory(null))
.withMessage("jwtDecoderFactory cannot be null");
}
@Test
void setAuthoritiesMapperWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setAuthoritiesMapper(null))
.withMessage("authoritiesMapper cannot be null");
}
@Test
void setUserServiceWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setUserService(null))
.withMessage("userService cannot be null");
}
@Test
void setClockSkewWhenNullThenException() {
assertThatException()
.isThrownBy(() -> new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler().setClockSkew(null))
.withMessage("clockSkew cannot be null");
}
@Test
void onAuthorizationSuccessWhenIdTokenValidThenSecurityContextRefreshed() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyComplete();
StepVerifier.create(serverSecurityContextRepository.load(exchange).map(SecurityContext::getAuthentication))
.expectNext(authenticationToken)
.verifyComplete();
}
@Test
void onAuthorizationSuccessWhenIdTokenIssuerNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", "https://issuer.com");
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid issuer");
}
@Test
void onAuthorizationSuccessWhenIdTokenSubNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", "invalid_sub");
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid subject");
}
@Test
void onAuthorizationSuccessWhenIdTokenIatNotAfterThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt().minus(Duration.ofDays(1)));
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid issued at time");
}
@Test
void onAuthorizationSuccessWhenIdTokenAudEmptyThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", Collections.emptyList());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid audience");
}
@Test
void onAuthorizationSuccessWhenIdTokenAudNotContainThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", List.of("invalid_client-id"));
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid audience");
}
@Test
void onAuthorizationSuccessWhenIdTokenAuthTimeNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("auth_time", principal.getIssuedAt());
claims.put("nonce", principal.getNonce());
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_id_token] Invalid authenticated at time");
}
@Test
void onAuthorizationSuccessWhenIdTokenNonceNotSameThenException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
DefaultOidcUser principal = TestOidcUsers.create();
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
principal.getAuthorities(), clientRegistration.getRegistrationId());
OAuth2AccessToken accessToken = createAccessToken();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
accessToken, null);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> claims = new HashMap<>();
claims.put("iss", principal.getIssuer());
claims.put("sub", principal.getSubject());
claims.put("aud", principal.getAudience());
claims.put("nonce", "invalid_nonce");
Jwt jwt = mock(Jwt.class);
given(jwt.getTokenValue()).willReturn("id-token-1234");
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
given(jwt.getClaims()).willReturn(claims);
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
given(userService.loadUser(any())).willReturn(Mono.just(principal));
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
handler.setUserService(userService);
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
.verifyErrorMessage("[invalid_nonce] Invalid nonce");
}
private static OAuth2AccessToken createAccessToken() {
Instant issuedAt = Instant.now().minus(Duration.ofDays(1));
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60));
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token-1234", issuedAt, expiresAt,
Set.of(OidcScopes.OPENID));
}
}

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