1
0
mirror of synced 2026-07-13 22:55:10 +00:00

Compare commits

..

98 Commits

Author SHA1 Message Date
github-actions[bot] 0a9d4dc8fc Release 6.5.10 2026-04-20 17:54:21 +00:00
Josh Cummings 3d4e20597a Merge remote-tracking branch 'oss/6.5.x' into 6.5.x 2026-04-20 11:49:17 -06:00
dependabot[bot] 81bd52ae48 Bump org.hibernate.orm:hibernate-core from 6.6.48.Final to 6.6.49.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.48.Final to 6.6.49.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.49/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.48...6.6.49)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 11:27:51 -06:00
dependabot[bot] 25b6af2738 Bump org.springframework:spring-framework-bom from 6.2.17 to 6.2.18
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.17 to 6.2.18.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.17...v6.2.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 11:27:29 -06:00
dependabot[bot] 95987bffc1 Bump org.apache.maven:maven-resolver-provider from 3.9.14 to 3.9.15
Bumps org.apache.maven:maven-resolver-provider from 3.9.14 to 3.9.15.

---
updated-dependencies:
- dependency-name: org.apache.maven:maven-resolver-provider
  dependency-version: 3.9.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 11:27:07 -06:00
Josh Cummings 6e5f8f2a1d Merge remote-tracking branch 'origin/6.5.x' into 6.5.x 2026-04-20 09:51:26 -06:00
Seol-JY 4187af38b2 Verify token deletion in JdbcOneTimeTokenService 2026-04-18 12:30:30 -04:00
Josh Cummings 5b638a54a4 Use SHA Hashes
This commit updates workflows that were using tags to instead
use SHA hashes to reference actions and workflows

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-17 14:15:50 -06:00
dependabot[bot] 51eef2b980 Bump io.projectreactor:reactor-bom from 2024.0.16 to 2024.0.17
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.16 to 2024.0.17.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2024.0.16...2024.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:57:33 -06:00
dependabot[bot] 302cfb116e Bump @springio/antora-extensions from 1.14.10 to 1.14.11 in /docs
Bumps [@springio/antora-extensions](https://github.com/spring-io/antora-extensions) from 1.14.10 to 1.14.11.
- [Changelog](https://github.com/spring-io/antora-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/antora-extensions/compare/v1.14.10...v1.14.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:08:19 -06:00
dependabot[bot] 695ea1717f Bump org.hibernate.orm:hibernate-core from 6.6.47.Final to 6.6.48.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.47.Final to 6.6.48.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.48/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.47...6.6.48)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:07:58 -06:00
dependabot[bot] 1206c2b141 Bump actions/upload-artifact from 7.0.0 to 7.0.1
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:07:36 -06:00
dependabot[bot] 3539f06146 Bump spring-io/spring-release-actions from 0.0.3 to 0.0.4
Bumps [spring-io/spring-release-actions](https://github.com/spring-io/spring-release-actions) from 0.0.3 to 0.0.4.
- [Release notes](https://github.com/spring-io/spring-release-actions/releases)
- [Commits](https://github.com/spring-io/spring-release-actions/compare/0.0.3...0.0.4)

---
updated-dependencies:
- dependency-name: spring-io/spring-release-actions
  dependency-version: 0.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:07:08 -06:00
Josh Cummings a317a3d866 Add Support for Always Running Additional Authentication Checks
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-15 21:07:39 -06:00
Josh Cummings 68b820ed09 Check Issuer with Issuer Provided
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-15 18:23:22 -06:00
dependabot[bot] 44d32815b1 Bump @springio/antora-extensions from 1.14.9 to 1.14.10 in /docs
Bumps [@springio/antora-extensions](https://github.com/spring-io/antora-extensions) from 1.14.9 to 1.14.10.
- [Changelog](https://github.com/spring-io/antora-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/antora-extensions/compare/v1.14.9...v1.14.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:29:49 -06:00
dependabot[bot] 87c3335e01 Bump org.hibernate.orm:hibernate-core from 6.6.45.Final to 6.6.47.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.45.Final to 6.6.47.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.47/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.45...6.6.47)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:07:57 -06:00
Joe Grandja eefbb4da64 Fix DefaultOidcUser.equals()
Closes gh-18622
2026-04-02 10:41:32 -04:00
Rob Winch a2793f31b4 Merge Add XML Based shouldWriteHeadersEagerly tests
Add XML Based shouldWriteHeadersEagerly tests
2026-04-01 12:53:29 -04:00
Robert Winch 679a47a51d Add XML Based shouldWriteHeadersEagerly tests 2026-04-01 11:37:39 -05:00
Josh Cummings 08fca57d12 Add Missing Serialization Support
Closed gh-19012

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-31 13:58:35 -06:00
Josh Cummings acabacb971 Update Test to find SuppressWarnings
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-31 13:47:52 -06:00
johnycho 1a130fca3c Improve serialVersionUID check in tests
Signed-off-by: johnycho <shunnn215@gmail.com>
2026-03-31 13:47:50 -06:00
Rob Winch 5a4ada04ac Merge pull request #19004 from rwinch/CredentialRecordOwnerAuthorizationManager
Add CredentialRecordOwnerAuthorizationManager
2026-03-29 23:46:03 -04:00
Robert Winch a856baa6a8 Add CredentialRecordOwnerAuthorizationManager
Add CredentialRecordOwnerAuthorizationManager that verifies the
credential being deleted is owned by the currently authenticated user.
Also add an AuthorizationManager<Bytes> to WebAuthnRegistrationFilter
for the delete credential operation, defaulting to deny all, and wire it
up in WebAuthnConfigurer.

Per the WebAuthn specification [1], credential ids contain at least 16
bytes with at least 100 bits of entropy, making them practically
unguessable. The specification also advises that credential ids should
be kept private, as exposing them can leak personally identifying
information [2]. The CredentialRecordOwnerAuthorizationManager serves as
defense in depth: even if a credential id were somehow exposed, an
unauthorized user could not delete another user's credential.

[1] https://www.w3.org/TR/webauthn-3/#credential-id
[2] https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak
2026-03-29 21:54:27 -05:00
Josh Cummings ac63cf4fa5 Polish CustomAuthorizationManager Docs
Issue gh-13967

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 16:45:25 -06:00
as1605 f6bb55effb Fix documentation for Custom Authorization Manager
Closes gh-13967

Signed-off-by: as1605 <1605.aditya.singh@gmail.com>
2026-03-27 16:45:25 -06:00
Tran Ngoc Nhan 85b756cb74 Update FilterChainProxy#getFilters(String) javadoc
Closes gh-18157

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-27 16:09:50 -06:00
dependabot[bot] 7441ce7f16 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.14 to 1.0.15.
- [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/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:25:46 -06:00
dependabot[bot] 9dbcd8cf00 Bump spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [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/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:25:35 -06:00
Josh Cummings 835d6c1fbd Add Issuer Validation to withIssuerLocation Snippets
Closes gh-19000

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 13:22:24 -06:00
Josh Cummings 95b2cdf7f4 Clarify JavaDoc
Removed note about DelegatingJwtGrantedAuthoritiesConverter from
ExpressionJwtGrantedAuthoritiesConverter and further explained in
DelegatingJwtGrantedAuthoritiesConverter where it comes in handy.

Issue gh-18300

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-27 11:48:56 -06:00
Rob Winch 3ecf84855e Merge pull request #18989 from rwinch/gh-18970-null-oncommitted
Merge Handle null value in OnCommittedResponseWrapper header methods
2026-03-26 17:29:33 -04:00
Robert Winch 0039bc0cf0 Handle null value in OnCommittedResponseWrapper header methods
Closes gh-18970
2026-03-26 14:50:44 -05:00
Josh Cummings 057e5181ea Adjust Formatting
Issue gh-18805

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-25 15:19:29 -06:00
Tran Ngoc Nhan 178ca56aaf Fallback defaultTargetUrl if refererHeader is empty
Closes gh-18805

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-25 15:19:29 -06:00
dependabot[bot] 61ccf14953 Bump org.hibernate.orm:hibernate-core from 6.6.44.Final to 6.6.45.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.44.Final to 6.6.45.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.45/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.44...6.6.45)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 15:11:27 -06:00
Joe Grandja 6e683f2286 Fix ID Token auth_time validation
Closes gh-18839
2026-03-25 11:33:55 -04:00
Josh Cummings b6e24db68c Return Mono.empty on Empty POST
Closes gh-18973

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-23 18:12:21 -06:00
Daniel Garnier-Moiroux aeb5fc1fb0 Fix HttpSessionRequestCache#getMatchingRequest query string parsing
- URL parsing changed in framework 6.2, and fails when path contains a % sign.
- The HttpSessionRequestCache only needs to inspect the query string, not the full URL.

Fixes gh-16656

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-03-23 17:52:17 -06:00
Tran Ngoc Nhan 62f33d3fcf Add equals and hashCode to HttpMethodRequestMatcher
Closes gh-18911

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-20 21:22:20 -06:00
Rob Winch 9fed1ac8c3 New line per sentence
Signed-off-by: Rob Winch <362503+rwinch@users.noreply.github.com>
2026-03-20 15:28:21 -06:00
Josh Cummings 9dbe3bdcc0 Polish Session Management Persistence Docs
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-20 15:28:21 -06:00
sankranti d547ae0181 Fix defaults description in Session Management doc
Corrected that starting from Spring Security 6
security context is not automatically saved by default.

Signed-off-by: sankranti <sankranty@gmail.com>
2026-03-20 15:28:21 -06:00
dependabot[bot] b8b1278e1f Bump @springio/antora-extensions from 1.14.7 to 1.14.9 in /docs
Bumps [@springio/antora-extensions](https://github.com/spring-io/antora-extensions) from 1.14.7 to 1.14.9.
- [Changelog](https://github.com/spring-io/antora-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/antora-extensions/compare/v1.14.7...v1.14.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 15:22:06 -06:00
dependabot[bot] 381047e386 Bump spring-io/spring-security-release-tools from 1.0.14 to 1.0.15
Bumps [spring-io/spring-security-release-tools](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [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/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 15:21:53 -06:00
dependabot[bot] 376b40a735 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.14 to 1.0.15.
- [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.14...v1.0.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:58:20 -06:00
dependabot[bot] 89fa1cbdd2 Bump spring-io/spring-security-release-tools/.github/workflows/build.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/build.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [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/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:57:09 -06:00
dependabot[bot] 0d75e6d10c Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.17 to 1.0.0-alpha.18.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.17...v1.0.0-alpha.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:56:46 -06:00
dependabot[bot] 01758c4c59 Bump spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [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/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:56:10 -06:00
dependabot[bot] f37833a59c 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.14 to 1.0.15.
- [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/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:55:52 -06:00
dependabot[bot] 52e6c4c4be Bump spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [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/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:55:38 -06:00
github-actions[bot] 96ceb535f4 Next development version 2026-03-16 18:13:58 +00:00
github-actions[bot] 0c54a55ae8 Release 6.5.9 2026-03-16 17:40:54 +00:00
Josh Cummings 01ff3b086a Add Workflow for Deferring Issues
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-16 10:49:07 -06:00
Rob Winch 33e6f4bd3f Merge Fix Jackson Deserializer for AuthenticationExtensionsClientOutputs 2026-03-16 11:57:07 -04:00
Robert Winch cdd4b36d37 Update Antora UI Spring to v0.4.26 2026-03-16 08:26:19 -05:00
Robert Winch 7672f76fde Bump io.projectreactor:reactor-bom from 2024.0.15 to 2024.0.16 2026-03-16 08:26:12 -05:00
Robert Winch 3db4999da4 Bump org.apache.maven:maven-resolver-provider from 3.9.13 to 3.9.14 2026-03-16 08:26:04 -05:00
dependabot[bot] a708d2f61b Bump org.springframework:spring-framework-bom from 6.2.16 to 6.2.17
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.16 to 6.2.17.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:07:46 +00:00
Ziqin Wang e726c05e76 Fix Jackson 2 deserializer for AuthenticationExtensionsClientOutputs
The deserializer is updated to properly ignore unknown extensions.

Closes gh-18643

Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
2026-03-15 15:04:14 +08:00
Ziqin Wang a7039fb3e6 Test Jackson 2 deserializer with unknown primitive WebAuthn ext
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
2026-03-15 15:03:28 +08:00
Ziqin Wang 88ea668f47 Test Jackson 2 deserializer with unknown obj/arr WebAuthn ext
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
2026-03-15 15:03:17 +08:00
github-actions[bot] 03a5de1955 Update Antora Spring UI to v0.4.26 2026-03-13 17:45:05 +00:00
dependabot[bot] 06cbea383e Bump org.apache.maven:maven-resolver-provider from 3.9.13 to 3.9.14
Bumps org.apache.maven:maven-resolver-provider from 3.9.13 to 3.9.14.

---
updated-dependencies:
- dependency-name: org.apache.maven:maven-resolver-provider
  dependency-version: 3.9.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-13 03:07:50 +00:00
Andrey Litvitski e250236279 Read relayState from authenticationRequest
Closes gh-18243

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
2026-03-12 10:30:11 -06:00
Josh Cummings ef76ba040d Require non-null authenticationRequest
Closes gh-18880

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-11 16:45:23 -06:00
dependabot[bot] d69af716c8 Bump io.projectreactor:reactor-bom from 2024.0.15 to 2024.0.16
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.15 to 2024.0.16.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2024.0.15...2024.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-11 03:06:57 +00:00
Rob Winch 7e37aa2b75 Merge Fix CookieRequestCache parameters 2026-03-09 15:25:05 -04:00
Vishnutheep B 07bfe371b4 Fix CookieRequestCache parameters
Previously the parameters were not restored.

This commit ensures the parameters are restored.

Closes gh-18204

Signed-off-by: Vishnutheep B <vishnutheep@gmail.com>
2026-03-09 14:10:30 -05:00
Robert Winch e12edf43f2 Bump @antora/collector-extension from 1.0.2 to 1.0.3 in /docs 2026-03-09 09:58:04 -05:00
dependabot[bot] a499e56b9b Bump org.apache.maven:maven-resolver-provider from 3.9.12 to 3.9.13
Bumps org.apache.maven:maven-resolver-provider from 3.9.12 to 3.9.13.

---
updated-dependencies:
- dependency-name: org.apache.maven:maven-resolver-provider
  dependency-version: 3.9.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:09:41 +00:00
dependabot[bot] 8c3f6ea0d4 Bump @antora/collector-extension from 1.0.2 to 1.0.3 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 00:37:07 +00:00
Josh Cummings e17d85e460 Add IDE Setup Documentation
Issue gh-17833

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-03 18:51:32 -07:00
dependabot[bot] f12036db05 Bump actions/upload-artifact from 6.0.0 to 7.0.0
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 18:16:39 -07:00
dependabot[bot] 7b5c502a97 Bump org.hibernate.orm:hibernate-core from 6.6.43.Final to 6.6.44.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.43.Final to 6.6.44.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.44/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.43...6.6.44)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 17:47:07 -07:00
HaiYan 706b059ea8 Update logout.adoc
Directives should be Directive

Signed-off-by: HaiYan <haiyan_qi@hotmail.com>
2026-03-03 16:43:18 -07:00
Rob Winch ea3b112bea Fix Flaky Crypto Tests 2026-03-03 15:58:17 -06:00
Robert Winch 1261c229a3 Fix Flaky Crypto Tests
Previously the RsaSecretEncryptorTests were flaky because the assumed that a BadPaddigException would be thrown
when using things like different salt. However, given that the tests had random inputs (e.g. keys) there is the
possibility that, despite the fact that it can never be properly decrypted, the final bytes look like a valid
encrypted value.

This updates the tests to ensure that decrypt either throws an Exception or is not equal to the original
plaintext.
2026-03-03 14:52:28 -06:00
Josh Cummings 4501ae7d1c Update Reactive Resource Server startup exceptations
Issue gh-16708

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-26 16:56:22 -07:00
Josh Cummings 48112d3d74 Polish Resource Server startup expectations
Issue gh-16708

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-26 16:56:22 -07:00
[CLOUD4] 한현 b8735abb63 Clarify Resource Server startup expectations
Clarify that Spring Boot defers OIDC discovery by default.

Closes gh-16708

Signed-off-by: [CLOUD4] 한현 <gusgus1467@naver.com>
2026-02-26 16:56:22 -07:00
Guillaume Husta 68a02ff176 Update Link to CRSF Docs in FAQ
Signed-off-by: Guillaume Husta <guillaume.husta@gmail.com>
2026-02-26 14:47:21 -07:00
dependabot[bot] f37a706d62 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-26 14:38:30 -07:00
Rob Winch 522c48b3b5 Merge Add Missing OnCommitedResponseWrapper Header Overrides
Add Missing OnCommitedResponseWrapper Header Overrides
2026-02-24 20:16:24 -06:00
Robert Winch 1dae9aa459 Add Missing OnCommitedResponseWrapper Header Overrides
Spring Security's `OnCommitedResponseWrapper` does not override the `setHeader`, `setIntHeader`, `addIntHeader`
methods. This means that if the `Content-Length` response header is specified using any of those methods then
the response body length is not tracked and can be committed before the response headers are written.

Spring Security should override the missing methods and track `Content-Length` as is already done for `addHeader`.

This issue is the underlying problem for spring-projects/spring-framework#36381

Closes gh-18797
2026-02-24 19:46:29 -06:00
Josh Cummings bec25edeb0 Merge pull request #18566 from Hann244/docs/gh-16530-jsp-method-attribute
Clarify need for method attribute in JSP authorize tag
2026-02-24 17:08:14 -07:00
Josh Cummings 4d43edfb20 Polish Documentation
- Combined explanation of method attribute with usage recommendations
- Used one sentence per line format

Issue gh-16530

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-02-24 14:24:11 -07:00
onhann 9f9699f8a5 Clarify need for method attribute in JSP authorize tag
Closes gh-16530

This aligns the JSP documentation with the changes made in gh-16529.
Added a NOTE to clarify that the method attribute is required when the underlying RequestMatcher is method-specific.

Signed-off-by: onhann <gusgus1467@naver.com>
2026-02-24 14:24:11 -07:00
Rob Winch d29c984881 Merge pull request #18544 from Khyojae/gh-18543
Fix GrantedAuthority.authority null in AuthoritiesAuthorizationManager
2026-02-23 11:16:42 -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
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
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
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
github-actions[bot] 6c2b2a7611 Next development version 2026-02-13 18:24:26 +00:00
75 changed files with 1549 additions and 133 deletions
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
actions: read
contents: read
security-events: write
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@e415dadd0910c901e7a7fabd67bbb355b2324500 # 1
@@ -17,7 +17,7 @@ permissions:
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
@@ -29,7 +29,7 @@ jobs:
secrets: inherit
test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
strategy:
matrix:
include:
@@ -48,7 +48,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
with:
java-version: 17
distribution: temurin
@@ -67,21 +67,21 @@ jobs:
deploy-artifacts:
name: Deploy Artifacts
needs: [ build, test, check-samples ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-schema:
name: Deploy Schema
needs: [ build, test, check-samples ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
perform-release:
name: Perform Release
needs: [ deploy-artifacts, deploy-schema ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
@@ -97,6 +97,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+76
View File
@@ -0,0 +1,76 @@
name: Defer Issues
on:
workflow_dispatch:
permissions:
contents: read
jobs:
defer-issues:
name: Defer Issues
runs-on: ubuntu-latest
if: github.repository_owner == 'spring-projects'
permissions:
issues: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Compute Version
id: compute-version
uses: spring-io/spring-release-actions/compute-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
- name: Get Today's Release Version
id: todays-release
uses: spring-io/spring-release-actions/get-todays-release-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
with:
snapshot-version: ${{ steps.compute-version.outputs.version }}
milestone-repository: ${{ github.repository }}
milestone-token: ${{ secrets.GITHUB_TOKEN }}
- name: Compute Next Version
id: next-version
uses: spring-io/spring-release-actions/compute-next-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
with:
version: ${{ steps.todays-release.outputs.release-version }}
- name: Schedule Next Milestone
uses: spring-io/spring-release-actions/schedule-milestone@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
with:
version: ${{ steps.next-version.outputs.version }}
version-date: ${{ steps.next-version.outputs.version-date }}
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Move Open Issues to Next Milestone
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT_MILESTONE: ${{ steps.todays-release.outputs.release-version }}
NEXT_MILESTONE: ${{ steps.next-version.outputs.version }}
run: |
current_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
--jq ".[] | select(.title == \"$CURRENT_MILESTONE\") | .number")
if [ -z "$current_milestone_number" ]; then
echo "No milestone found for $CURRENT_MILESTONE"
exit 0
fi
next_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
--jq ".[] | select(.title == \"$NEXT_MILESTONE\") | .number")
if [ -z "$next_milestone_number" ]; then
echo "No milestone found for $NEXT_MILESTONE"
exit 1
fi
echo "Moving open issues from milestone '$CURRENT_MILESTONE' (#$current_milestone_number) to '$NEXT_MILESTONE' (#$next_milestone_number)"
page=1
while true; do
issues=$(gh api "repos/${{ github.repository }}/issues?milestone=$current_milestone_number&state=open&per_page=100&page=$page" \
--jq '.[].number')
if [ -z "$issues" ]; then
break
fi
for issue in $issues; do
echo "Moving issue/PR #$issue to milestone $NEXT_MILESTONE"
gh api repos/${{ github.repository }}/issues/$issue \
--method PATCH \
--field milestone=$next_milestone_number \
--silent
done
page=$((page + 1))
done
echo "Done."
@@ -30,6 +30,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
- name: Upload Docs
id: upload
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docs
path: docs/build/site
@@ -46,6 +46,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -18,7 +18,7 @@ jobs:
matrix:
branch: [ '5.8.x', '6.2.x', '6.3.x', 'main' ]
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
name: Update
with:
docs-branch: ${{ matrix.branch }}
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
name: Update on docs-build
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
name: Update
with:
docs-branch: 'docs-build'
@@ -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@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
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@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+21
View File
@@ -68,6 +68,27 @@ The https://github.com/spring-projects/spring-security/tree/docs-build[playbook
Discover more commands with `./gradlew tasks`.
=== IDE setup (IntelliJ)
No special steps are needed to open Spring Security in IntelliJ.
=== IDE setup (Eclipse and VS Code)
To work in Eclipse or VS Code, first generate Eclipse metadata so you can import the project into Eclipse or VS Code:
[indent=0]
----
./gradlew cleanEclipse eclipse
----
If you have not built the project yet, run `./gradlew publishToMavenLocal` first so dependencies are resolved.
*VS Code:* Open the repository root as a folder. The repository includes `.vscode/settings.json` which disables automatic Gradle import so that the generated Eclipse metadata (`.classpath`, `.project`) is used. Do not use the Gradle for Java extension to import the project.
*Eclipse:* File → Import → General → Existing Projects into Workspace, then select the repository root.
The build uses a custom Eclipse plugin to work around Gradle dependency cycles that confuse IDE metadata generation. You may see Eclipse warnings about `xml-apis` from some test dependencies; those are excluded in the build and can be ignored.
== Getting Support
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
https://spring.io/support[Commercial support] is available too.
@@ -36,6 +36,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
import org.springframework.security.web.webauthn.management.CredentialRecordOwnerAuthorizationManager;
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
@@ -166,6 +167,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
rpOperations);
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
rpOperations);
if (creationOptionsRepository != null) {
@@ -211,6 +211,7 @@ import org.springframework.security.web.webauthn.api.AuthenticationExtensionsCli
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.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
@@ -225,6 +226,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestO
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses;
import org.springframework.security.web.webauthn.api.TestBytes;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
@@ -654,6 +656,8 @@ final class SerializationSamples {
generatorByClassName.put(CredentialPropertiesOutput.class, (o) -> credentialOutput);
generatorByClassName.put(ImmutableAuthenticationExtensionsClientOutputs.class, (o) -> outputs);
generatorByClassName.put(AuthenticatorAssertionResponse.class, (r) -> response);
generatorByClassName.put(AuthenticatorAttestationResponse.class,
(r) -> TestAuthenticatorAttestationResponses.createAuthenticatorAttestationResponse().build());
generatorByClassName.put(RelyingPartyAuthenticationRequest.class, (r) -> authRequest);
generatorByClassName.put(PublicKeyCredential.class, (r) -> credential);
generatorByClassName.put(WebAuthnAuthenticationRequestToken.class, (r) -> requestToken);
@@ -33,10 +33,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.commons.lang3.ObjectUtils;
@@ -207,10 +207,7 @@ class SpringSecurityCoreVersionSerializableTests {
boolean hasSerialVersion = Stream.of(clazz.getDeclaredFields())
.map(Field::getName)
.anyMatch((n) -> n.equals("serialVersionUID"));
SuppressWarnings suppressWarnings = clazz.getAnnotation(SuppressWarnings.class);
boolean hasSerialIgnore = suppressWarnings == null
|| Arrays.asList(suppressWarnings.value()).contains("Serial");
if (!hasSerialVersion && !hasSerialIgnore) {
if (!hasSerialVersion && !hasSuppressSerialInSource(clazz)) {
classes.add(clazz);
continue;
}
@@ -249,6 +246,58 @@ class SpringSecurityCoreVersionSerializableTests {
return classes.stream();
}
private static boolean hasSuppressSerialInSource(Class<?> clazz) {
try {
Class<?> fileClass = clazz;
while (fileClass.getEnclosingClass() != null) {
fileClass = fileClass.getEnclosingClass();
}
var codeSource = fileClass.getProtectionDomain().getCodeSource();
if (codeSource == null) {
return false;
}
Path sourceFile = findSourceFile(Path.of(codeSource.getLocation().toURI()), fileClass);
if (sourceFile == null) {
return false;
}
return hasSuppressSerialAnnotation(Files.readAllLines(sourceFile), clazz.getSimpleName());
}
catch (Exception ex) {
return false;
}
}
private static Path findSourceFile(Path start, Class<?> clazz) {
String relativePath = clazz.getName().replace('.', '/') + ".java";
Path dir = start;
for (int i = 0; i < 10 && dir != null; i++) {
for (String sourceRoot : List.of("src/main/java", "src/test/java")) {
Path candidate = dir.resolve(sourceRoot).resolve(relativePath);
if (Files.exists(candidate)) {
return candidate;
}
}
dir = dir.getParent();
}
return null;
}
private static boolean hasSuppressSerialAnnotation(List<String> lines, String simpleClassName) {
Pattern classDeclaration = Pattern
.compile("\\b(?:class|interface|enum|record)\\s+" + Pattern.quote(simpleClassName) + "\\b");
for (int i = 0; i < lines.size(); i++) {
if (classDeclaration.matcher(lines.get(i)).find()) {
for (int j = Math.max(0, i - 5); j < i; j++) {
String line = lines.get(j);
if (line.contains("@SuppressWarnings") && line.contains("\"serial\"")) {
return true;
}
}
}
}
return false;
}
private static String getCurrentVersion() {
String version = System.getProperty("springSecurityVersion");
String[] parts = version.split("\\.");
@@ -42,8 +42,15 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
import org.springframework.test.web.servlet.MockMvc;
@@ -56,6 +63,7 @@ import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@@ -247,6 +255,24 @@ public class WebAuthnConfigurerTests {
.andExpect(content().string(expectedBody));
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToUserThenNoContent() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"))))
.andExpect(status().isNoContent());
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("other-user", "password", "ROLE_USER"))))
.andExpect(status().isForbidden());
}
@Configuration
@EnableWebSecurity
static class ConfigCredentialCreationOptionsRepository {
@@ -417,4 +443,47 @@ public class WebAuthnConfigurerTests {
}
@Configuration
@EnableWebSecurity
static class DeleteCredentialConfiguration {
static final String CREDENTIAL_ID_BASE64URL = "NauGCN7bZ5jEBwThcde51g";
static final Bytes USER_ENTITY_ID = Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM");
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager();
}
@Bean
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
return mock(WebAuthnRelyingPartyOperations.class);
}
@Bean
UserCredentialRepository userCredentialRepository() {
MapUserCredentialRepository repository = new MapUserCredentialRepository();
repository.save(TestCredentialRecords.userCredential().build());
return repository;
}
@Bean
PublicKeyCredentialUserEntityRepository userEntityRepository() {
MapPublicKeyCredentialUserEntityRepository repository = new MapPublicKeyCredentialUserEntityRepository();
repository.save(ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(USER_ENTITY_ID)
.displayName("User")
.build());
return repository;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
}
}
}
@@ -28,14 +28,17 @@ import jakarta.servlet.http.HttpSession;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.session.SessionLimit;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -150,6 +153,16 @@ public class HttpHeadersConfigTests {
// @formatter:on
}
@Test
public void requestWhenHeadersEagerlyConfiguredThenHeadersAreWritten() throws Exception {
this.spring.configLocations(this.xml("HeadersEagerlyConfigured")).autowire();
// @formatter:off
this.mvc.perform(get("/").secure(true))
.andExpect(status().isOk())
.andExpect(includesDefaults());
// @formatter:on
}
@Test
public void requestWhenFrameOptionsConfiguredThenIncludesHeader() throws Exception {
Map<String, String> headers = new HashMap(defaultHeaders);
@@ -955,6 +968,18 @@ public class HttpHeadersConfigTests {
}
public static class EagerHeadersBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof HeaderWriterFilter headerWriterFilter) {
headerWriterFilter.setShouldWriteHeadersEagerly(true);
}
return bean;
}
}
public static class CustomSessionLimit implements SessionLimit {
@Override
@@ -0,0 +1,37 @@
<?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"
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 auto-config="true">
<headers/>
<intercept-url pattern="/**" access="permitAll"/>
</http>
<b:bean class="org.springframework.security.config.http.HttpHeadersConfigTests.EagerHeadersBeanPostProcessor"/>
<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
<b:import resource="userservice.xml"/>
</b:beans>
@@ -92,6 +92,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
private boolean alwaysPerformAdditionalChecksOnUser = true;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
/**
@@ -146,8 +148,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
@@ -157,8 +158,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
@@ -171,6 +171,25 @@ public abstract class AbstractUserDetailsAuthenticationProvider
return createSuccessAuthentication(principalToReturn, authentication, user);
}
private void performPreCheck(UserDetails user, UsernamePasswordAuthenticationToken authentication) {
try {
this.preAuthenticationChecks.check(user);
}
catch (AuthenticationException ex) {
if (!this.alwaysPerformAdditionalChecksOnUser) {
throw ex;
}
try {
additionalAuthenticationChecks(user, authentication);
}
catch (AuthenticationException ignored) {
// preserve the original failed check
}
throw ex;
}
additionalAuthenticationChecks(user, authentication);
}
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}
@@ -313,6 +332,22 @@ public abstract class AbstractUserDetailsAuthenticationProvider
this.postAuthenticationChecks = postAuthenticationChecks;
}
/**
* Set whether to always perform the additional checks on the user, even if the
* pre-authentication checks fail. This is useful to ensure that regardless of the
* state of the user account, authentication takes the same amount of time to
* complete.
*
* <p>
* For applications that rely on the additional checks running only once should set
* this value to {@code false}
* @param alwaysPerformAdditionalChecksOnUser
* @since 5.7.23
*/
public void setAlwaysPerformAdditionalChecksOnUser(boolean alwaysPerformAdditionalChecksOnUser) {
this.alwaysPerformAdditionalChecksOnUser = alwaysPerformAdditionalChecksOnUser;
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
@@ -152,7 +152,9 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
return null;
}
OneTimeToken token = tokens.get(0);
deleteOneTimeToken(token);
if (deleteOneTimeToken(token) == 0) {
return null;
}
if (isExpired(token)) {
return null;
}
@@ -170,11 +172,11 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper);
}
private void deleteOneTimeToken(OneTimeToken oneTimeToken) {
private int deleteOneTimeToken(OneTimeToken oneTimeToken) {
List<SqlParameterValue> parameters = List
.of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue()));
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
return this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
}
private ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
@@ -67,7 +67,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;
}
}
@@ -28,6 +28,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
* instead
*/
@Deprecated
@SuppressWarnings("serial")
public class ExpressionAttributeAuthorizationDecision extends AuthorizationDecision {
private final ExpressionAttribute expressionAttribute;
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.springframework.cache.Cache;
import org.springframework.dao.DataRetrievalFailureException;
@@ -42,6 +43,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -62,6 +64,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -452,12 +455,10 @@ public class DaoAuthenticationProviderTests {
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
}
/**
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
* SEC-2056 is fixed.
*/
public void IGNOREtestSec2056() {
// SEC-2056
@Test
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
public void testSec2056() {
UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod",
"koala");
UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken
@@ -491,6 +492,41 @@ public class DaoAuthenticationProviderTests {
.isTrue();
}
// related to SEC-2056
@Test
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
public void testDisabledUserTiming() {
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
PasswordEncoder encoder = new BCryptPasswordEncoder();
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(encoder);
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
users.password = encoder.encode((CharSequence) user.getCredentials());
provider.setUserDetailsService(users);
int sampleSize = 100;
List<Long> enabledTimes = new ArrayList<>(sampleSize);
for (int i = 0; i < sampleSize; i++) {
long start = System.currentTimeMillis();
provider.authenticate(user);
enabledTimes.add(System.currentTimeMillis() - start);
}
UserDetailsChecker preChecks = mock(UserDetailsChecker.class);
willThrow(new DisabledException("User is disabled")).given(preChecks).check(any(UserDetails.class));
provider.setPreAuthenticationChecks(preChecks);
List<Long> disabledTimes = new ArrayList<>(sampleSize);
for (int i = 0; i < sampleSize; i++) {
long start = System.currentTimeMillis();
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> provider.authenticate(user));
disabledTimes.add(System.currentTimeMillis() - start);
}
double enabledAvg = avg(enabledTimes);
double disabledAvg = avg(disabledTimes);
assertThat(Math.abs(disabledAvg - enabledAvg) <= 3)
.withFailMessage("Disabled user average " + disabledAvg + " should be within 3ms of enabled user average "
+ enabledAvg)
.isTrue();
}
private double avg(List<Long> counts) {
return counts.stream().mapToLong(Long::longValue).average().orElse(0);
}
@@ -21,19 +21,24 @@ import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@@ -145,6 +150,27 @@ class JdbcOneTimeTokenServiceTests {
assertThat(consumedOneTimeToken).isNull();
}
@Test
void consumeWhenTokenIsDeletedConcurrentlyThenReturnNull() throws Exception {
// Simulates a concurrent consume: SELECT finds the token but DELETE affects
// 0 rows because another caller already consumed it.
JdbcOperations jdbcOperations = mock(JdbcOperations.class);
Instant notExpired = Instant.now().plus(5, ChronoUnit.MINUTES);
OneTimeToken token = new DefaultOneTimeToken(TOKEN_VALUE, USERNAME, notExpired);
given(jdbcOperations.query(any(String.class), any(PreparedStatementSetter.class),
ArgumentMatchers.<RowMapper<OneTimeToken>>any()))
.willReturn(List.of(token));
given(jdbcOperations.update(any(String.class), any(PreparedStatementSetter.class))).willReturn(0);
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcOperations);
try {
OneTimeToken consumed = service.consume(new OneTimeTokenAuthenticationToken(TOKEN_VALUE));
assertThat(consumed).isNull();
}
finally {
service.destroy();
}
}
@Test
void consumeWhenTokenIsExpiredThenReturnNull() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
@@ -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 {
@@ -84,4 +88,20 @@ class AuthoritiesAuthorizationManagerTests {
assertThat(manager.check(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();
}
}
+6
View File
@@ -1,3 +1,7 @@
plugins {
id 'java-test-fixtures'
}
apply plugin: 'io.spring.convention.spring-module'
dependencies {
@@ -6,6 +10,8 @@ dependencies {
optional 'org.springframework:spring-core'
optional 'org.bouncycastle:bcpkix-jdk18on'
testFixturesImplementation "org.assertj:assertj-core"
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.junit.jupiter:junit-jupiter-params"
@@ -0,0 +1,47 @@
/*
* 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.crypto.assertions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link CryptoAssertions} and {@link CryptoStringAssert}.
*/
class CryptoAssertionsTests {
@Test
void doesNotDecryptToPassesWhenSupplierThrows() {
CryptoAssertions.assertThat((() -> {
throw new RuntimeException("decrypt failed");
})).doesNotDecryptTo("any");
}
@Test
void doesNotDecryptToPassesWhenResultDiffersFromExpected() {
CryptoAssertions.assertThat(() -> "other").doesNotDecryptTo("plaintext");
}
@Test
void doesNotDecryptToFailsWhenResultEqualsExpected() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> CryptoAssertions.assertThat(() -> "plaintext").doesNotDecryptTo("plaintext"))
.withMessageContaining("Expected supplier not to return <plaintext> but it did");
}
}
@@ -22,8 +22,9 @@ import java.security.interfaces.RSAPublicKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.assertions.CryptoAssertions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* @author Dave Syer
@@ -86,13 +87,15 @@ public class RsaSecretEncryptorTests {
@Test
public void roundTripWithMixedAlgorithm() {
RsaSecretEncryptor oaep = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
assertThatIllegalStateException().isThrownBy(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")));
CryptoAssertions.assertThat(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")))
.doesNotDecryptTo("encryptor");
}
@Test
public void roundTripWithMixedSalt() {
RsaSecretEncryptor other = new RsaSecretEncryptor(this.encryptor.getPublicKey(), RsaAlgorithm.DEFAULT, "salt");
assertThatIllegalStateException().isThrownBy(() -> this.encryptor.decrypt(other.encrypt("encryptor")));
CryptoAssertions.assertThat(() -> this.encryptor.decrypt(other.encrypt("encryptor")))
.doesNotDecryptTo("encryptor");
}
@Test
@@ -106,7 +109,8 @@ public class RsaSecretEncryptorTests {
public void publicKeyCannotDecrypt() {
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
assertThat(encryptor.canDecrypt()).as("Encryptor schould not be able to decrypt").isFalse();
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encryptor.encrypt("encryptor")));
CryptoAssertions.assertThat(() -> encryptor.decrypt(encryptor.encrypt("encryptor")))
.doesNotDecryptTo("encryptor");
}
@Test
@@ -0,0 +1,44 @@
/*
* 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.crypto.assertions;
import java.util.function.Supplier;
/**
* AssertJ entry point for crypto-related assertions. Use {@link #assertThat(Supplier)} to
* assert on a {@link Supplier}&lt;{@link String}&gt; (e.g. a decryption lambda).
* <p>
* Example: <pre>
* assertThat(() -&gt; encryptor.decrypt(ciphertext)).doesNotDecryptTo("plaintext");
* </pre>
*/
public final class CryptoAssertions {
private CryptoAssertions() {
}
/**
* Create assertions for the given supplier (e.g. a decryption expression).
* @param actual the supplier to assert on
* @return assertion object with methods like
* {@link CryptoStringAssert#doesNotDecryptTo(String)}
*/
public static CryptoStringAssert assertThat(Supplier<String> actual) {
return new CryptoStringAssert(actual);
}
}
@@ -0,0 +1,56 @@
/*
* 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.crypto.assertions;
import java.util.Objects;
import java.util.function.Supplier;
import org.assertj.core.api.AbstractObjectAssert;
/**
* AssertJ assertion for {@link Supplier}&lt;{@link String}&gt;, supporting
* decryption-related checks such as {@link #doesNotDecryptTo(String)}.
*/
public final class CryptoStringAssert extends AbstractObjectAssert<CryptoStringAssert, Supplier<String>> {
CryptoStringAssert(Supplier<String> actual) {
super(actual, CryptoStringAssert.class);
}
/**
* Asserts that either the supplier throws an exception when invoked, or the value it
* returns is not equal to the given string. Use this to assert that a decryption
* attempt does not yield a specific plaintext (e.g. wrong key or tampered
* ciphertext).
* @param expected the value that the supplier must not return
* @return this assertion for chaining
*/
public CryptoStringAssert doesNotDecryptTo(String expected) {
isNotNull();
try {
String result = this.actual.get();
if (Objects.equals(result, expected)) {
failWithMessage("Expected supplier not to return <%s> but it did", expected);
}
}
catch (Exception ex) {
// Exception thrown: supplier does not "decrypt to" the expected value
}
return this;
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ urls:
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.26/ui-bundle.zip
snapshot: true
runtime:
log:
@@ -39,17 +39,18 @@ This endpoint is referred to as a https://openid.net/specs/openid-connect-discov
When this property and these dependencies are used, Resource Server automatically configures itself to validate JWT-encoded Bearer Tokens.
It achieves this through a deterministic startup process:
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property.
. Configure the validation strategy to query `jwks_url` for valid public keys.
. Configure the validation strategy to validate each JWT's `iss` claim against `https://idp.example.com`.
A consequence of this process is that the authorization server must be receiving requests in order for Resource Server to successfully start up.
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
[NOTE]
====
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup fails.
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder[`SupplierReactiveJwtDecoder`].
Consider wrapping any <<webflux-oauth2resourceserver-decoder-bean,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
====
=== Runtime Expectations
@@ -85,7 +86,7 @@ From here, consider jumping to:
[[webflux-oauth2resourceserver-jwt-jwkseturi]]
=== Specifying the Authorization Server JWK Set Uri Directly
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, you can supply `jwk-set-uri` as well:
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, you can supply `jwk-set-uri` as well:
[source,yaml]
----
@@ -375,7 +376,9 @@ Java::
----
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
return jwtDecoder;
}
----
@@ -385,7 +388,9 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
return jwtDecoder
}
----
======
@@ -451,8 +456,10 @@ Java::
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -462,8 +469,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -478,8 +487,10 @@ Java::
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -489,8 +500,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -505,11 +518,13 @@ Java::
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -519,12 +534,14 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}
.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -319,7 +319,7 @@ If you have trouble working out where a session is being created, you can add so
[[appendix-faq-forbidden-csrf]]
=== I get a 403 Forbidden when performing a POST. What is wrong?
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to https://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to xref:features/exploits/csrf.adoc#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
[[appendix-faq-no-security-on-forward]]
=== I am forwarding a request to another URL by using the RequestDispatcher, but my security constraints are not being applied.
@@ -259,7 +259,7 @@ Java::
+
[source,java,role="primary"]
----
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directives.ALL));
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL));
http
.logout((logout) -> logout.addLogoutHandler(clearSiteData))
----
@@ -268,7 +268,7 @@ Kotlin::
+
[source,kotlin,role="secondary"]
----
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directives.ALL))
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directive.ALL))
http {
logout {
addLogoutHandler(clearSiteData)
@@ -3,7 +3,10 @@
Once you have got an application that is xref:servlet/authentication/index.adoc[authenticating requests], it is important to consider how that resulting authentication will be persisted and restored on future requests.
This is done automatically by default, so no additional code is necessary, though it is important to know what `requireExplicitSave` means in `HttpSecurity`.
This is done automatically by default.
If you have a custom filter or controller that is setting the security context, you will need to use a `SecurityContextRepository` to persist it across requests.
If you are upgrading from an older version, you may be interested in the `requireExplicitSave` setting that preserves Spring Security 5's default, though note that this is primarily for migration purposes.
If you like, <<how-it-works-requireexplicitsave,you can read more about what requireExplicitSave is doing>> or <<requireexplicitsave,why it's important>>. Otherwise, in most cases you are done with this section.
@@ -1377,14 +1377,17 @@ Java::
[source,java,role="primary"]
----
@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
public class MyPreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}
}
@Component
public class MyPostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
// ... authorization logic
}
}
@@ -1395,12 +1398,15 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
class MyPreAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocation> {
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationResult {
// ... authorization logic
}
}
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
@Component
class MyPostAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocationResult> {
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
// ... authorization logic
}
}
@@ -1422,13 +1428,13 @@ Java::
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(MyAuthorizationManager manager) {
Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(MyAuthorizationManager manager) {
Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
}
}
@@ -1441,15 +1447,15 @@ Kotlin::
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun preAuthorize(manager: MyAuthorizationManager) : Advisor {
fun preAuthorize(manager: MyPreAuthorizeAuthorizationManager): Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize(manager: MyAuthorizationManager) : Advisor {
fun postAuthorize(manager: MyPostAuthorizeAuthorizationManager): Advisor {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
}
}
@@ -1466,13 +1472,13 @@ Xml::
<bean id="preAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="preAuthorize">
<constructor-arg ref="myAuthorizationManager"/>
<constructor-arg ref="myPreAuthorizeAuthorizationManager"/>
</bean>
<bean id="postAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
factory-method="postAuthorize">
<constructor-arg ref="myAuthorizationManager"/>
<constructor-arg ref="myPostAuthorizeAuthorizationManager"/>
</bean>
----
======
@@ -1482,6 +1488,8 @@ Xml::
You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
====
You can also implement `MethodAuthorizationDeniedHandler` in the same manager class to override the default exception-handling behavior.
[[customizing-expression-handling]]
=== Customizing Expression Handling
@@ -63,7 +63,9 @@ To use this tag, you must also have an instance of `WebInvocationPrivilegeEvalua
If you are using the namespace, one is automatically registered.
This is an instance of `DefaultWebInvocationPrivilegeEvaluator`, which creates a dummy web request for the supplied URL and invokes the security interceptor to see whether the request would succeed or fail.
This lets you delegate to the access-control setup you defined by using `intercept-url` declarations within the `<http>` namespace configuration and saves having to duplicate the information (such as the required roles) within your JSPs.
You can also combine this approach with a `method` attribute (supplying the HTTP method, such as `POST`) for a more specific match.
If you have xref:servlet/authorization/authorize-http-requests.adoc#match-by-httpmethod[method-based authorization rules], you should combine this approach with the `method` attribute (supplying the HTTP method, such as `POST`) to activate the intended method-based rule.
For example, if you have a rule `.requestMatchers(POST, "/admin").hasRole("ADMIN")`, then you should do `<sec:authorize method="POST" url="/admin">` to match.
You can store the Boolean result of evaluating the tag (whether it grants or denies access) in a page context scope variable by setting the `var` attribute to the variable name, avoiding the need for duplicating and re-evaluating the condition at other points in the page.
@@ -40,17 +40,20 @@ And that's it!
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
It achieves this through a deterministic startup process:
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
1. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property
2. Query the `jwks_url` endpoint for supported algorithms
3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found
4. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
[NOTE]
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
====
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierJwtDecoder[`SupplierJwtDecoder`].
Consider wrapping any <<oauth2resourceserver-jwt-decoder,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
====
=== Runtime Expectations
@@ -66,7 +69,7 @@ So long as this scheme is indicated, Resource Server will attempt to process the
Given a well-formed JWT, Resource Server will:
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup or on first request, depending on configuration, and matched against the JWT
2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and
3. Map each scope to an authority with the prefix `SCOPE_`.
@@ -111,7 +114,7 @@ Ultimately, the returned `JwtAuthenticationToken` will be set on the xref:servle
[[oauth2resourceserver-jwt-jwkseturi]]
== Specifying the Authorization Server JWK Set Uri Directly
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
[source,yaml]
----
@@ -516,7 +519,9 @@ Java::
----
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
return jwtDecoder;
}
----
@@ -526,7 +531,9 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
return jwtDecoder
}
----
======
@@ -592,8 +599,10 @@ Java::
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -603,8 +612,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -619,8 +630,10 @@ Java::
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -630,8 +643,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -646,11 +661,13 @@ Java::
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -660,11 +677,13 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
+3 -3
View File
@@ -2,9 +2,9 @@
"dependencies": {
"antora": "3.2.0-alpha.11",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.2",
"@antora/collector-extension": "1.0.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
"@springio/antora-extensions": "1.14.11",
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
}
}
+1 -1
View File
@@ -14,7 +14,7 @@
# limitations under the License.
#
springBootVersion=3.3.3
version=6.5.8
version=6.5.10
samplesBranch=6.5.x
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+8 -8
View File
@@ -5,7 +5,7 @@ io-spring-javaformat = "0.0.47"
io-spring-nohttp = "0.0.11"
jakarta-websocket = "2.2.0"
org-apache-directory-server = "1.5.5"
org-apache-maven-resolver = "1.9.25"
org-apache-maven-resolver = "1.9.27"
org-aspectj = "1.9.25.1"
org-bouncycastle = "1.80"
org-eclipse-jetty = "11.0.26"
@@ -14,11 +14,11 @@ org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.17.0"
org-opensaml = "4.3.2"
org-opensaml5 = "5.1.2"
org-springframework = "6.2.16"
org-springframework = "6.2.18"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.29"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.5"
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.6"
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:9.37.4"
@@ -31,13 +31,13 @@ commons-collections = "commons-collections:commons-collections:3.2.2"
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.4"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.14"
io-mockk = "io.mockk:mockk:1.14.7"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.15"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.17"
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.14"
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.15"
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:2.1.1"
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
@@ -57,7 +57,7 @@ org-apache-directory-server-apacheds-protocol-shared = { module = "org.apache.di
org-apache-directory-server-apacheds-server-jndi = { module = "org.apache.directory.server:apacheds-server-jndi", version.ref = "org-apache-directory-server" }
org-apache-directory-shared-shared-ldap = "org.apache.directory.shared:shared-ldap:0.9.15"
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents:httpclient:4.5.14"
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.12"
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.15"
org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "org-apache-maven-resolver" }
org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" }
org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" }
@@ -71,7 +71,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.42.Final"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.49.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
@@ -300,7 +300,11 @@ public final class OidcAuthorizedClientRefreshedEventListener
return;
}
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
// The auth_time claim MUST represent the time of the original authentication OR
// the most recent time when the end-user reauthenticated when "prompt=login" is
// passed in the authentication request
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())
&& !idToken.getAuthenticatedAt().isAfter(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());
@@ -269,6 +269,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
}
@SuppressWarnings("serial")
private static final class OAuth2AuthorizationRequestException extends AuthenticationException {
OAuth2AuthorizationRequestException(Throwable cause) {
@@ -419,8 +419,8 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
}
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.SECONDS);
public void onApplicationEventWhenIdTokenAuthenticatedAtBeforeCurrentAuthenticatedAtThenThrowsOAuth2AuthenticationException() {
Instant authTime = this.oidcUser.getAuthenticatedAt().minus(5, ChronoUnit.MINUTES);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
@@ -440,8 +440,26 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
// gh-18839
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesThenOidcUserRefreshedEventPublished() {
public void onApplicationEventWhenIdTokenAuthenticatedAtAfterCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.MINUTES);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
this.accessTokenResponse, this.authorizedClient);
this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
verify(this.applicationEventPublisher).publishEvent(any(OidcUserRefreshedEvent.class));
verifyNoMoreInteractions(this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
Instant authTime = this.authentication.getPrincipal().getAttribute(IdTokenClaimNames.AUTH_TIME);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.core.oidc.user;
import java.io.Serial;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
@@ -114,4 +115,38 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
return this.userInfo;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
DefaultOidcUser that = (DefaultOidcUser) obj;
if (!this.getName().equals(that.getName())) {
return false;
}
if (!this.getAuthorities().equals(that.getAuthorities())) {
return false;
}
if (this.getIdToken().getIssuer() == null || that.getIdToken().getIssuer() == null) {
return false;
}
return Objects.equals(this.getIdToken().getIssuer().toExternalForm(),
that.getIdToken().getIssuer().toExternalForm())
&& Objects.equals(this.getIdToken().getSubject(), that.getIdToken().getSubject());
}
@Override
public int hashCode() {
int result = this.getName().hashCode();
result = 31 * result + this.getAuthorities().hashCode();
result = 31 * result + ((this.getIdToken().getIssuer() != null)
? this.getIdToken().getIssuer().toExternalForm().hashCode() : 0);
result = 31 * result
+ ((this.getIdToken().getSubject() != null) ? this.getIdToken().getSubject().hashCode() : 0);
return result;
}
}
@@ -17,6 +17,7 @@
package org.springframework.security.oauth2.core.oidc.user;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -147,4 +148,31 @@ public class DefaultOidcUserTests {
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
}
// gh-18622
@Test
public void equalsWhenOidcUserPrincipalSameThenTrue() {
String issuer = "https://example.com";
String subject = "subject-1";
// @formatter:off
OidcIdToken idToken1 = OidcIdToken.withTokenValue("id-token-value-1")
.issuer(issuer)
.subject(subject)
.issuedAt(Instant.now())
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
.build();
OidcIdToken idToken2 = OidcIdToken.withTokenValue("id-token-value-2")
.issuer(issuer)
.subject(subject)
.issuedAt(Instant.now())
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
.build();
// @formatter:on
DefaultOidcUser user1 = new DefaultOidcUser(AUTHORITIES, idToken1, USER_INFO);
DefaultOidcUser user2 = new DefaultOidcUser(AUTHORITIES, idToken2, USER_INFO);
assertThat(user1).isEqualTo(user2);
}
}
@@ -185,6 +185,7 @@ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory<DPoPP
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
@SuppressWarnings("serial")
private static final class JtiCache extends LinkedHashMap<String, Long> {
private static final int MAX_SIZE = 1000;
@@ -230,7 +230,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
.getConfigurationForIssuerLocation(issuer, rest);
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
return configuration.get("jwks_uri").toString();
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
.validator(JwtValidators.createDefaultWithIssuer(issuer));
}
/**
@@ -289,6 +290,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
this.jwkSetUri = (rest) -> jwkSetUri;
@@ -423,6 +426,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
return this;
}
JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
Assert.notNull(validator, "validator cannot be null");
this.validator = validator;
return this;
}
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
if (this.signatureAlgorithms.isEmpty()) {
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
@@ -461,7 +470,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
* @return the configured {@link NimbusJwtDecoder}
*/
public NimbusJwtDecoder build() {
return new NimbusJwtDecoder(processor());
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor());
decoder.setJwtValidator(this.validator);
return decoder;
}
private static final class SpringJWKSource<C extends SecurityContext> implements JWKSetSource<C> {
@@ -241,7 +241,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
}
return Mono.just(configuration.get("jwks_uri").toString());
}),
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
.validator(JwtValidators.createDefaultWithIssuer(issuer));
}
/**
@@ -332,6 +333,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer;
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
@@ -456,6 +459,11 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
return this;
}
JwkSetUriReactiveJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
this.validator = validator;
return this;
}
JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer(
BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer) {
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
@@ -468,7 +476,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
* @return the configured {@link NimbusReactiveJwtDecoder}
*/
public NimbusReactiveJwtDecoder build() {
return new NimbusReactiveJwtDecoder(processor());
NimbusReactiveJwtDecoder decoder = new NimbusReactiveJwtDecoder(processor());
decoder.setJwtValidator(this.validator);
return decoder;
}
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
@@ -328,11 +328,26 @@ public class NimbusJwtDecoderTests {
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
.restOperations(restOperations)
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@Test
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
String issuer = "https://example.org/wrong-issuer";
RestOperations restOperations = mock(RestOperations.class);
given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class)))
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
assertThatExceptionOfType(JwtValidationException.class).isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT));
}
@Test
public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
// @formatter:off
@@ -617,11 +617,31 @@ public class NimbusReactiveJwtDecoderTests {
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
given(spec.retrieve()).willReturn(responseSpec);
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
.webClient(webClient)
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@Test
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
String issuer = "https://example.org/wrong-issuer";
WebClient real = WebClient.builder().build();
WebClient.RequestHeadersUriSpec spec = spy(real.get());
WebClient webClient = spy(WebClient.class);
given(webClient.get()).willReturn(spec);
WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
given(spec.retrieve()).willReturn(responseSpec);
ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
.webClient(webClient)
.build();
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
assertThatExceptionOfType(JwtValidationException.class)
.isThrownBy(() -> jwtDecoder.decode(this.messageReadToken).block());
}
@Test
@@ -30,6 +30,23 @@ import org.springframework.util.Assert;
* A {@link Jwt} to {@link GrantedAuthority} {@link Converter} that is a composite of
* converters.
*
* <p>
* This is handy when needing to read authorities from multiple locations in a JWT; each
* underlying converter is called in series and the results are aggregated into a single
* collection of authorities.
*
* <p>
* For example, you might have a claim called "scope" and another called "roles". With
* {@link DelegatingJwtGrantedAuthoritiesConverter}, you can do:
*
* <code>
* JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
* JwtGrantedAuthoritiesConverter roles = new JwtGrantedAUthoritiesConverter();
* roles.setAuthoritiesClaimName("roles");
* roles.setAuthorityPrefix("ROLE_");
* return new DelegatingJwtGrantedAuthoritiesConverter(scopes, roles);
* </code>
*
* @author Laszlo Stahorszki
* @author Josh Cummings
* @since 5.5
@@ -36,9 +36,6 @@ import org.springframework.util.Assert;
* Uses an expression for extracting the token claim value to use for mapping
* {@link GrantedAuthority authorities}.
*
* Note this can be used in combination with a
* {@link DelegatingJwtGrantedAuthoritiesConverter}.
*
* @author Thomas Darimont
* @since 6.4
*/
@@ -16,6 +16,7 @@
package org.springframework.security.saml2.provider.service.authentication;
import java.io.Serial;
import java.util.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
@@ -33,6 +34,9 @@ import org.springframework.util.Assert;
*/
public class Saml2AuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 5225098478444036532L;
private final RelyingPartyRegistration relyingPartyRegistration;
private final String saml2Response;
@@ -42,6 +42,7 @@ import org.springframework.security.saml2.core.Saml2X509Credential;
* </pre>
*/
@Deprecated
@SuppressWarnings("serial")
public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistration {
OpenSamlRelyingPartyRegistration(RelyingPartyRegistration registration) {
@@ -35,6 +35,7 @@ import org.springframework.util.Assert;
* that it was for the user trying to log in. Please see the reference for details.
*
* @author Josh Cummings
* @author Andrey Litvitski
* @since 6.5
*/
public final class CacheSaml2AuthenticationRequestRepository
@@ -52,7 +53,8 @@ public final class CacheSaml2AuthenticationRequestRepository
@Override
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
HttpServletRequest request, HttpServletResponse response) {
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
Assert.notNull(authenticationRequest, "authenticationRequest must not be null");
String relayState = authenticationRequest.getRelayState();
Assert.notNull(relayState, "relayState must not be null");
this.cache.put(relayState, authenticationRequest);
}
@@ -42,9 +42,10 @@ class CacheSaml2AuthenticationRequestRepositoryTests {
@Test
void loadAuthenticationRequestWhenCachedThenReturns() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter(Saml2ParameterNames.RELAY_STATE, "test");
Saml2PostAuthenticationRequest authenticationRequest = TestSaml2PostAuthenticationRequests.create();
String relayState = authenticationRequest.getRelayState();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter(Saml2ParameterNames.RELAY_STATE, relayState);
this.repository.saveAuthenticationRequest(authenticationRequest, request, null);
assertThat(this.repository.loadAuthenticationRequest(request)).isEqualTo(authenticationRequest);
this.repository.removeAuthenticationRequest(request, null);
@@ -77,15 +78,16 @@ class CacheSaml2AuthenticationRequestRepositoryTests {
CacheSaml2AuthenticationRequestRepository repository = new CacheSaml2AuthenticationRequestRepository();
Cache cache = spy(new ConcurrentMapCache("requests"));
repository.setCache(cache);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter(Saml2ParameterNames.RELAY_STATE, "test");
Saml2PostAuthenticationRequest authenticationRequest = TestSaml2PostAuthenticationRequests.create();
String relayState = authenticationRequest.getRelayState();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter(Saml2ParameterNames.RELAY_STATE, relayState);
repository.saveAuthenticationRequest(authenticationRequest, request, null);
verify(cache).put(eq("test"), any());
verify(cache).put(eq(relayState), any());
repository.loadAuthenticationRequest(request);
verify(cache).get("test", AbstractSaml2AuthenticationRequest.class);
verify(cache).get(relayState, AbstractSaml2AuthenticationRequest.class);
repository.removeAuthenticationRequest(request, null);
verify(cache).evict("test");
verify(cache).evict(relayState);
}
}
@@ -254,6 +254,11 @@ public class FilterChainProxy extends GenericFilterBean {
/**
* Convenience method, mainly for testing.
* <p>
* Attempt to find the matching filter chain based on the given {@code url}. Note that
* the URI is often not enough information and this method should be used with
* caution. Instead, consider using Spring Security's testing support that mocks a
* full HTTP request.
* @param url the URL
* @return matching filter list
*/
@@ -32,6 +32,7 @@ import org.springframework.security.web.FilterInvocation;
* {@link AuthorizationManager}.
*/
@Deprecated
@SuppressWarnings("serial")
class WebExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<FilterInvocation> {
private final Expression authorizeExpression;
@@ -112,9 +112,14 @@ public abstract class AbstractAuthenticationTargetUrlRequestHandler {
trace("Using url %s from request parameter %s", targetUrlParameterValue, this.targetUrlParameter);
return targetUrlParameterValue;
}
String refererHeader = request.getHeader("Referer");
if (!StringUtils.hasText(refererHeader)) {
return this.defaultTargetUrl;
}
if (this.useReferer) {
trace("Using url %s from Referer header", request.getHeader("Referer"));
return request.getHeader("Referer");
trace("Using url %s from Referer header", refererHeader);
return refererHeader;
}
return this.defaultTargetUrl;
}
@@ -91,6 +91,7 @@ public class CookieRequestCache implements RequestCache {
.setServerPort(port)
.setMethod(request.getMethod())
.setLocales(Collections.list(request.getLocales()))
.setParameters(request.getParameterMap())
.build();
}
@@ -103,11 +103,11 @@ public class HttpSessionRequestCache implements RequestCache {
@Override
public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) {
if (this.matchingRequestParameterName != null) {
if (!StringUtils.hasText(request.getQueryString())
|| !UriComponentsBuilder.fromUriString(UrlUtils.buildRequestUrl(request))
.build()
.getQueryParams()
.containsKey(this.matchingRequestParameterName)) {
if (!StringUtils.hasText(request.getQueryString()) || !UriComponentsBuilder.newInstance()
.query(request.getQueryString())
.build()
.getQueryParams()
.containsKey(this.matchingRequestParameterName)) {
this.logger.trace(
"matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided");
return null;
@@ -49,7 +49,8 @@ public final class ServerOneTimeTokenAuthenticationConverter implements ServerAu
Assert.notNull(exchange, "exchange cannot be null");
if (isFormEncodedRequest(exchange.getRequest())) {
return exchange.getFormData()
.map((data) -> OneTimeTokenAuthenticationToken.unauthenticated(data.getFirst(TOKEN)));
.mapNotNull((data) -> data.getFirst(TOKEN))
.map((data) -> OneTimeTokenAuthenticationToken.unauthenticated(data));
}
String token = resolveTokenFromRequest(exchange.getRequest());
if (!StringUtils.hasText(token)) {
@@ -305,6 +305,19 @@ public final class PathPatternRequestMatcher implements RequestMatcher {
return this.method.name().equals(request.getMethod());
}
@Override
public boolean equals(Object o) {
if (!(o instanceof HttpMethodRequestMatcher that)) {
return false;
}
return Objects.equals(this.method, that.method);
}
@Override
public int hashCode() {
return Objects.hash(this.method);
}
@Override
public String toString() {
return "HttpMethod [" + this.method + "]";
@@ -58,10 +58,38 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
@Override
public void addHeader(String name, String value) {
checkContentLengthHeader(name, value);
super.addHeader(name, value);
}
@Override
public void addIntHeader(String name, int value) {
checkContentLengthHeader(name, value);
super.addIntHeader(name, value);
}
@Override
public void setHeader(String name, String value) {
checkContentLengthHeader(name, value);
super.setHeader(name, value);
}
@Override
public void setIntHeader(String name, int value) {
checkContentLengthHeader(name, value);
super.setIntHeader(name, value);
}
private void checkContentLengthHeader(String name, int value) {
if ("Content-Length".equalsIgnoreCase(name)) {
setContentLength(value);
}
}
private void checkContentLengthHeader(String name, String value) {
if (value != null && "Content-Length".equalsIgnoreCase(name)) {
setContentLength(Long.parseLong(value));
}
super.addHeader(name, value);
}
@Override
@@ -477,7 +505,7 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
@Override
public PrintWriter append(CharSequence csq) {
checkContentLength(csq.length());
checkContentLength((csq != null) ? csq.length() : 4);
return this.delegate.append(csq);
}
@@ -16,6 +16,7 @@
package org.springframework.security.web.webauthn.api;
import java.io.Serial;
import java.util.Arrays;
import java.util.List;
@@ -34,6 +35,9 @@ import java.util.List;
*/
public final class AuthenticatorAttestationResponse extends AuthenticatorResponse {
@Serial
private static final long serialVersionUID = -1628559840895428945L;
private final Bytes attestationObject;
private final List<AuthenticatorTransport> transports;
@@ -56,11 +56,8 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
throws IOException, JacksonException {
List<AuthenticationExtensionsClientOutput<?>> outputs = new ArrayList<>();
for (String key = parser.nextFieldName(); key != null; key = parser.nextFieldName()) {
JsonToken startObject = parser.nextValue();
if (startObject != JsonToken.START_OBJECT) {
break;
}
if (CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
JsonToken next = parser.nextToken();
if (next == JsonToken.START_OBJECT && CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
CredentialPropertiesOutput output = parser.readValueAs(CredentialPropertiesOutput.class);
outputs.add(output);
}
@@ -68,7 +65,9 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
if (logger.isDebugEnabled()) {
logger.debug("Skipping unknown extension with id " + key);
}
parser.nextValue();
if (next.isStructStart()) {
parser.skipChildren();
}
}
}
@@ -0,0 +1,91 @@
/*
* 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.web.webauthn.management;
import java.util.function.Supplier;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.util.Assert;
/**
* An {@link AuthorizationManager} that grants access when the {@link CredentialRecord}
* identified by the provided credential id is owned by the currently authenticated user.
*
* <p>
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
* specification</a>, a credential id must contain at least 16 bytes with at least 100
* bits of entropy, making it practically unguessable. The specification also advises that
* credential ids should be kept private, as exposing them can leak personally identifying
* information (see
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§ 14.6.3
* Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is therefore
* intended as defense in depth: even if a credential id were somehow exposed, an
* unauthorized user could not delete another user's credential.
*
* @author Rob Winch
* @since 6.5.10
*/
public final class CredentialRecordOwnerAuthorizationManager implements AuthorizationManager<Bytes> {
private final AuthenticatedAuthorizationManager<Bytes> authenticatedAuthorizationManager = AuthenticatedAuthorizationManager
.authenticated();
private final UserCredentialRepository userCredentials;
private final PublicKeyCredentialUserEntityRepository userEntities;
/**
* Creates a new instance.
* @param userCredentials the {@link UserCredentialRepository} to use
* @param userEntities the {@link PublicKeyCredentialUserEntityRepository} to use
*/
public CredentialRecordOwnerAuthorizationManager(UserCredentialRepository userCredentials,
PublicKeyCredentialUserEntityRepository userEntities) {
Assert.notNull(userCredentials, "userCredentials cannot be null");
Assert.notNull(userEntities, "userEntities cannot be null");
this.userCredentials = userCredentials;
this.userEntities = userEntities;
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Bytes credentialId) {
AuthorizationDecision decision = this.authenticatedAuthorizationManager.check(authentication, credentialId);
if (!decision.isGranted()) {
return decision;
}
Authentication auth = authentication.get();
CredentialRecord credential = this.userCredentials.findByCredentialId(credentialId);
if (credential == null) {
return new AuthorizationDecision(false);
}
if (credential.getUserEntityUserId() == null) {
return new AuthorizationDecision(false);
}
PublicKeyCredentialUserEntity userEntity = this.userEntities.findByUsername(auth.getName());
if (userEntity == null) {
return new AuthorizationDecision(false);
}
return new AuthorizationDecision(credential.getUserEntityUserId().equals(userEntity.getId()));
}
}
@@ -17,6 +17,7 @@
package org.springframework.security.web.webauthn.registration;
import java.io.IOException;
import java.util.function.Supplier;
import com.fasterxml.jackson.databind.json.JsonMapper;
import jakarta.servlet.FilterChain;
@@ -34,6 +35,12 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.SingleResultAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.webauthn.api.Bytes;
@@ -87,6 +94,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
private final UserCredentialRepository userCredentials;
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
JsonMapper.builder().addModule(new WebauthnJackson2Module()).build());
@@ -98,6 +108,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
private RequestMatcher removeCredentialMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.DELETE, "/webauthn/register/{id}");
private AuthorizationManager<Bytes> deleteCredentialAuthorizationManager = SingleResultAuthorizationManager
.denyAll();
public WebAuthnRegistrationFilter(UserCredentialRepository userCredentials,
WebAuthnRelyingPartyOperations rpOptions) {
Assert.notNull(userCredentials, "userCredentials must not be null");
@@ -132,6 +145,42 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
this.removeCredentialMatcher = removeCredentialMatcher;
}
/**
* Sets the {@link AuthorizationManager} used to authorize the delete credential
* operation. The object being authorized is the credential id as {@link Bytes}. By
* default, all delete requests are denied.
*
* <p>
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
* specification</a>, a credential id must contain at least 16 bytes with at least 100
* bits of entropy, making it practically unguessable. The specification also advises
* that credential ids should be kept private, as exposing them can leak personally
* identifying information (see
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§
* 14.6.3 Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is
* therefore intended as defense in depth: even if a credential id were somehow
* exposed, an unauthorized user could not delete another user's credential.
* @param deleteCredentialAuthorizationManager the {@link AuthorizationManager} to use
* @since 6.5.10
*/
public void setDeleteCredentialAuthorizationManager(
AuthorizationManager<Bytes> deleteCredentialAuthorizationManager) {
Assert.notNull(deleteCredentialAuthorizationManager, "deleteCredentialAuthorizationManager cannot be null");
this.deleteCredentialAuthorizationManager = deleteCredentialAuthorizationManager;
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default is
* {@link SecurityContextHolder#getContextHolderStrategy()}.
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
* use
* @since 6.5.10
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
@@ -203,7 +252,15 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
private void removeCredential(HttpServletRequest request, HttpServletResponse response, String id)
throws IOException {
this.userCredentials.delete(Bytes.fromBase64(id));
Bytes credentialId = Bytes.fromBase64(id);
Supplier<Authentication> authentication = () -> this.securityContextHolderStrategy.getContext()
.getAuthentication();
AuthorizationResult result = this.deleteCredentialAuthorizationManager.authorize(authentication, credentialId);
if (result != null && !result.isGranted()) {
response.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
this.userCredentials.delete(credentialId);
response.setStatus(HttpStatus.NO_CONTENT.value());
}
@@ -114,4 +114,12 @@ public class AbstractAuthenticationTargetUrlRequestHandlerTests {
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setRedirectStrategy(null));
}
// gh-18805
@Test
void returnDefaultUrlIfUseRefererIsTrueAndRefererHeaderIsEmpty() {
this.handler.setUseReferer(true);
this.request.addHeader("Referer", "");
assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(DEFAULT_TARGET_URL);
}
}
@@ -108,6 +108,21 @@ public class CookieRequestCacheTests {
assertThat(savedRequest.getRedirectUrl()).isEqualTo(redirectUrl);
}
@Test
public void getRequestWhenRequestContainsSavedRequestCookieThenSavedRequestContainsRequestParameters() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie("https://abc.com/destination")));
request.setParameter("single", "first");
request.addParameter("multi", "second");
request.addParameter("multi", "third");
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
assertThat(savedRequest).isNotNull();
assertThat(savedRequest.getParameterValues("single")).containsExactly("first");
assertThat(savedRequest.getParameterValues("multi")).containsExactly("second", "third");
assertThat(savedRequest.getParameterMap()).containsKeys("single", "multi");
}
@Test
public void matchingRequestWhenRequestDoesNotContainSavedRequestCookieThenReturnsNull() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
@@ -168,6 +168,21 @@ public class HttpSessionRequestCacheTests {
verify(request, never()).getParameterMap();
}
// gh-16656
@Test
public void getMatchingRequestWhenMatchingRequestPathContainsPercentSignThenLookedUp() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setServletPath("/30 % off");
HttpSessionRequestCache cache = new HttpSessionRequestCache();
cache.saveRequest(request, new MockHttpServletResponse());
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
requestToMatch.setServletPath("/30 % off");
requestToMatch.setQueryString("continue");
requestToMatch.setSession(request.getSession());
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
assertThat(matchingRequest).isNotNull();
}
private static final class CustomSavedRequest implements SavedRequest {
private final SavedRequest delegate;
@@ -72,6 +72,18 @@ public class ServerOneTimeTokenAuthenticationConverterTests {
assertThat(authentication).isNull();
}
// gh-18973
@Test
void convertWhenNoTokenFormParameterThenNull() {
MockServerHttpRequest request = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body("username=Max");
Authentication authentication = this.converter.convert(MockServerWebExchange.from(request)).block();
assertThat(authentication).isNull();
}
@Test
void convertWhenTokenEncodedFormParameterThenReturnOneTimeTokenAuthenticationToken() {
// @formatter:off
@@ -145,6 +145,17 @@ public class PathPatternRequestMatcherTests {
assertThat(matcher.matches(mock)).isTrue();
}
// gh-18911
@Test
void testEqualsWithSameAndDifferentHttpMethod() {
PathPatternRequestMatcher.Builder builder = PathPatternRequestMatcher.withDefaults();
PathPatternRequestMatcher matcher1 = builder.matcher(HttpMethod.GET, "/foo");
PathPatternRequestMatcher matcher2 = builder.matcher(HttpMethod.GET, "/foo");
PathPatternRequestMatcher matcher3 = builder.matcher(HttpMethod.POST, "/foo");
assertThat(matcher1).isEqualTo(matcher2);
assertThat(matcher1).isNotEqualTo(matcher3);
}
MockHttpServletRequest request(String uri) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
ServletRequestPathUtils.parseAndCache(request);
@@ -28,7 +28,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@@ -1006,6 +1009,63 @@ public class OnCommittedResponseWrapperTests {
assertThat(this.committed).isTrue();
}
@Test
public void addHeaderNullNameDoesNotThrow() {
assertThatNoException().isThrownBy(() -> this.response.addHeader(null, "value"));
}
@Test
public void addHeaderNullValueDoesNotThrow() {
assertThatNoException().isThrownBy(() -> this.response.addHeader(HttpHeaders.CONTENT_LENGTH, null));
}
@Test
public void addIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
givenGetWriterThenReturn();
int expected = 1234;
this.response.addIntHeader("Content-Length", String.valueOf(expected).length());
this.response.getWriter().write(expected);
assertThat(this.committed).isTrue();
}
@Test
public void addIntHeaderNullNameDoesNotThrow() {
assertThatNoException().isThrownBy(() -> this.response.addIntHeader(null, 1));
}
@Test
public void setHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
givenGetWriterThenReturn();
int expected = 1234;
this.response.setHeader("Content-Length", String.valueOf(String.valueOf(expected).length()));
this.response.getWriter().write(expected);
assertThat(this.committed).isTrue();
}
@Test
public void setHeaderNullNameDoesNotThrow() {
assertThatNoException().isThrownBy(() -> this.response.setHeader(null, "value"));
}
@Test
public void setHeaderNullValueDoesNotThrow() {
assertThatNoException().isThrownBy(() -> this.response.setHeader(HttpHeaders.CONTENT_LENGTH, null));
}
@Test
public void setIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
givenGetWriterThenReturn();
int expected = 1234;
this.response.setIntHeader("Content-Length", String.valueOf(expected).length());
this.response.getWriter().write(expected);
assertThat(this.committed).isTrue();
}
@Test
public void setIntHeaderNullNameDoesNotThrow() {
assertThatNoException().isThrownBy(() -> this.response.setIntHeader(null, 1));
}
@Test
public void bufferSizePrintWriterWriteCommits() throws Exception {
givenGetWriterThenReturn();
@@ -1027,4 +1087,86 @@ public class OnCommittedResponseWrapperTests {
assertThat(this.committed).isFalse();
}
@Test
public void printWriterPrintNullStringDoesNotThrow() throws Exception {
givenGetWriterThenReturn();
String s = null;
assertThatNoException().isThrownBy(() -> this.response.getWriter().print(s));
verify(this.writer).print(s);
}
@Test
public void printWriterPrintlnNullStringDoesNotThrow() throws Exception {
givenGetWriterThenReturn();
String s = null;
assertThatNoException().isThrownBy(() -> this.response.getWriter().println(s));
verify(this.writer).println(s);
}
@Test
public void printWriterPrintNullObjectDoesNotThrow() throws Exception {
givenGetWriterThenReturn();
Object obj = null;
assertThatNoException().isThrownBy(() -> this.response.getWriter().print(obj));
verify(this.writer).print(obj);
}
@Test
public void printWriterPrintlnNullObjectDoesNotThrow() throws Exception {
givenGetWriterThenReturn();
Object obj = null;
assertThatNoException().isThrownBy(() -> this.response.getWriter().println(obj));
verify(this.writer).println(obj);
}
@Test
public void printWriterWriteNullStringDoesNotThrow() throws Exception {
givenGetWriterThenReturn();
String s = null;
assertThatNoException().isThrownBy(() -> this.response.getWriter().write(s));
verify(this.writer).write(s);
}
@Test
public void printWriterAppendNullCharSequenceDoesNotThrow() throws Exception {
givenGetWriterThenReturn();
CharSequence csq = null;
assertThatNoException().isThrownBy(() -> this.response.getWriter().append(csq));
verify(this.writer).append(csq);
}
@Test
public void printWriterAppendNullCharSequenceIntIntDoesNotThrow() throws Exception {
givenGetWriterThenReturn();
CharSequence csq = null;
assertThatNoException().isThrownBy(() -> this.response.getWriter().append(csq, 0, 3));
verify(this.writer).append(csq, 0, 3);
}
@Test
public void outputStreamPrintNullStringDoesNotThrow() throws Exception {
givenGetOutputStreamThenReturn();
String s = null;
assertThatNoException().isThrownBy(() -> this.response.getOutputStream().print(s));
verify(this.out).print(s);
}
@Test
public void outputStreamPrintlnNullStringDoesNotThrow() throws Exception {
givenGetOutputStreamThenReturn();
String s = null;
assertThatNoException().isThrownBy(() -> this.response.getOutputStream().println(s));
verify(this.out).println(s);
}
@Test
public void sendErrorWithNullMsgDoesNotThrow() throws Exception {
assertThatNoException().isThrownBy(() -> this.response.sendError(400, null));
}
@Test
public void sendRedirectWithNullLocationDoesNotThrow() throws Exception {
assertThatNoException().isThrownBy(() -> this.response.sendRedirect(null));
}
}
@@ -122,6 +122,47 @@ class JacksonTests {
assertThat(outputs).usingRecursiveComparison().isEqualTo(credProps);
}
@Test
void readAuthenticationExtensionsClientOutputsWhenAppId() throws Exception {
String json = """
{
"appid": false,
"credProps": {
"rk": false
}
}
""";
CredentialPropertiesOutput credProps = new CredentialPropertiesOutput(false);
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
AuthenticationExtensionsClientOutputs.class);
assertThat(outputs.getOutputs()).usingRecursiveFieldByFieldElementComparator().contains(credProps);
}
@Test
void readAuthenticationExtensionsClientOutputsWhenUnknownExtension() throws Exception {
String json = """
{
"unknownObject1": {
"key": "value"
},
"unknownArray": [
{ "key": "value1" },
{ "key": "value2" }
],
"credProps": {
"rk": false
},
"unknownObject2": {}
}
""";
CredentialPropertiesOutput credProps = new CredentialPropertiesOutput(false);
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
AuthenticationExtensionsClientOutputs.class);
assertThat(outputs.getOutputs()).usingRecursiveFieldByFieldElementComparator().contains(credProps);
}
@Test
void readAuthenticationExtensionsClientOutputsWhenFieldAfter() throws Exception {
String json = """
@@ -0,0 +1,154 @@
/*
* 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.web.webauthn.management;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.TestBytes;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link CredentialRecordOwnerAuthorizationManager}.
*
* @author Rob Winch
* @since 6.5.10
*/
@ExtendWith(MockitoExtension.class)
class CredentialRecordOwnerAuthorizationManagerTests {
@Mock
private UserCredentialRepository userCredentials;
@Mock
private PublicKeyCredentialUserEntityRepository userEntities;
@Test
void constructorWhenNullUserCredentialsThenIllegalArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(null, this.userEntities));
}
@Test
void constructorWhenNullUserEntitiesTonIllegalArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(this.userCredentials, null));
}
@Test
void checkWhenAuthenticationNullThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
AuthorizationDecision decision = manager.check(() -> null, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenNotAuthenticatedThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
authentication.setAuthenticated(false);
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenCredentialNotFoundThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenCredentialUserEntityUserIdNullThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().userEntityUserId(null).build());
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenUserEntityNotFoundThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().build());
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenCredentialBelongsToUserThenGranted() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
Bytes userId = TestCredentialRecords.userCredential().build().getUserEntityUserId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().build());
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(userId)
.displayName("User")
.build();
given(this.userEntities.findByUsername("user")).willReturn(userEntity);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isTrue();
}
@Test
void checkWhenCredentialBelongsToDifferentUserThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().build());
PublicKeyCredentialUserEntity otherUserEntity = ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(TestBytes.get())
.displayName("User")
.build();
given(this.userEntities.findByUsername("user")).willReturn(otherUserEntity);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
}
@@ -31,7 +31,11 @@ import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.SingleResultAuthorizationManager;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
@@ -213,12 +217,41 @@ class WebAuthnRegistrationFilterTests {
@Test
void doFilterWhenDeleteSuccessThenNoContent() throws Exception {
this.filter.setDeleteCredentialAuthorizationManager(SingleResultAuthorizationManager.permitAll());
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
.buildRequest(new MockServletContext());
this.filter.doFilter(request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
}
@Test
void setDeleteCredentialAuthorizationManagerWhenNullThenIllegalArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.filter.setDeleteCredentialAuthorizationManager(null));
}
@Test
void doFilterWhenDeleteAndCustomAuthorizationManagerThenUses() throws Exception {
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(true));
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
.buildRequest(new MockServletContext());
this.filter.doFilter(request, this.response, this.chain);
verify(authorizationManager).authorize(any(), any());
}
@Test
void doFilterWhenDeleteAndAuthorizationDeniedThenForbidden() throws Exception {
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(false));
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
.buildRequest(new MockServletContext());
this.filter.doFilter(request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.FORBIDDEN.value());
}
private static MockHttpServletRequest registerCredentialRequest(String body) {
return MockMvcRequestBuilders.post(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL)
.content(body)