1
0
mirror of synced 2026-07-19 01:25:18 +00:00

Compare commits

...

122 Commits

Author SHA1 Message Date
Josh Cummings 53bc6a7796 Fix Formatting
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-20 13:52:41 -06:00
Josh Cummings aed95aa2b1 Fix Formatting
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-20 13:45:45 -06:00
github-actions[bot] 7b57a4a3bb Release 7.0.5 2026-04-20 17:54:57 +00:00
Josh Cummings 11047960ad Merge branch '6.5.x' into 7.0.x 2026-04-20 11:49:27 -06:00
Josh Cummings 3d4e20597a Merge remote-tracking branch 'oss/6.5.x' into 6.5.x 2026-04-20 11:49:17 -06:00
Josh Cummings 4c40eeb89e Merge remote-tracking branch 'oss/6.5.x' into 7.0.x 2026-04-20 11:31:32 -06:00
Josh Cummings ff1e925c08 Merge remote-tracking branch 'oss/7.0.x' into 7.0.x 2026-04-20 11:31:15 -06:00
dependabot[bot] 7b309cf271 Bump org.springframework.data:spring-data-bom from 2025.1.4 to 2025.1.5
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2025.1.4 to 2025.1.5.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2025.1.4...2025.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 11:29:49 -06:00
dependabot[bot] 51a1f88ddf Bump com.webauthn4j:webauthn4j-core
Bumps [com.webauthn4j:webauthn4j-core](https://github.com/webauthn4j/webauthn4j) from 0.31.2.RELEASE to 0.31.3.RELEASE.
- [Release notes](https://github.com/webauthn4j/webauthn4j/releases)
- [Commits](https://github.com/webauthn4j/webauthn4j/compare/0.31.2.RELEASE...0.31.3.RELEASE)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 11:29:39 -06:00
dependabot[bot] 762d8f19e5 Bump org.springframework:spring-framework-bom from 7.0.6 to 7.0.7
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 7.0.6 to 7.0.7.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v7.0.6...v7.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 11:28:58 -06:00
dependabot[bot] c9d623966f 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:28:20 -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 e96203d578 Merge remote-tracking branch 'origin/7.0.x' into 7.0.x 2026-04-20 09:53:50 -06:00
Josh Cummings 1637727750 Merge branch '6.5.x' into 7.0.x 2026-04-20 09:53:27 -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
Joe Grandja 3ef1c34632 Merge branch '6.5.x' into 7.0.x 2026-04-18 12:47:05 -04:00
Seol-JY 4187af38b2 Verify token deletion in JdbcOneTimeTokenService 2026-04-18 12:30:30 -04:00
addcontent 19b3cae62e Add authentication validator for dynamic client registration
Signed-off-by: Kelvin Mbogo <addcontent08@gmail.com>
2026-04-17 17:22:40 -04:00
Josh Cummings a2358bbc83 Use SHA Hashes
This commit updates workflows to use SHA hashes to
reference other actions and workflows

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-17 14:27:09 -06:00
Josh Cummings fbe09c26ae Merge branch '6.5.x' into 7.0.x 2026-04-17 14:16:09 -06: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
Josh Cummings d0d2ac545b Merge remote-tracking branch 'origin/6.5.x' into 7.0.x 2026-04-17 11:58:35 -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
Josh Cummings 63cd037188 Fix Unit Test
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-17 11:54:31 -06:00
dependabot[bot] d3e349c290 Bump com.webauthn4j:webauthn4j-core
Bumps [com.webauthn4j:webauthn4j-core](https://github.com/webauthn4j/webauthn4j) from 0.31.1.RELEASE to 0.31.2.RELEASE.
- [Release notes](https://github.com/webauthn4j/webauthn4j/releases)
- [Commits](https://github.com/webauthn4j/webauthn4j/compare/0.31.1.RELEASE...0.31.2.RELEASE)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:54:31 -06:00
dependabot[bot] bc16a6f723 Bump io.projectreactor:reactor-bom from 2025.0.4 to 2025.0.5
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2025.0.4 to 2025.0.5.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2025.0.4...2025.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:54:13 -06:00
dependabot[bot] e480da69e9 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:12:01 -06:00
dependabot[bot] 1d466f86a0 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:11:39 -06:00
dependabot[bot] 9dfa5cf713 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:11:17 -06:00
dependabot[bot] bd8bdb8a98 Bump org.springframework.ldap:spring-ldap-core from 4.0.2 to 4.0.3
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/4.0.2...4.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 11:10:57 -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 4a6e0a13cd Update DaoAuthenticationProvider Usage
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-15 21:14:23 -06:00
Josh Cummings fc630ae6eb Merge branch '6.5.x' into 7.0.x 2026-04-15 21:07:50 -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 88118afb8f Use RDN Parsing
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-15 18:41:31 -06:00
Josh Cummings 9ad7981e36 Merge branch '6.5.x' into 7.0.x 2026-04-15 18:24:59 -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
Josh Cummings 53bcf0d16b Fix Servlet Path Application
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-15 17:12:08 -06:00
Josh Cummings 438c783c7d securityMatchers uses PathPatternRequestMatcher.Builder Bean
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-15 16:54:51 -06:00
Joe Grandja 41524880c6 Fix auth_time claim should represent authentication time
Closes gh-18282
2026-04-07 15:44:57 -04:00
Josh Cummings 2361dc131e Merge branch '6.5.x' into 7.0.x 2026-04-07 10:31:01 -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
dependabot[bot] 76e9d91f24 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:06:09 -06:00
Joe Grandja 77fe9e892a Merge branch '6.5.x' into 7.0.x
Closes gh-19022
2026-04-02 10:52:15 -04:00
Joe Grandja eefbb4da64 Fix DefaultOidcUser.equals()
Closes gh-18622
2026-04-02 10:41:32 -04:00
Rob Winch 8f65f88dc0 Merge Add XML Based shouldWriteHeadersEagerly tests
Add XML Based shouldWriteHeadersEagerly tests
2026-04-01 12:58:09 -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 64d8e6cc9b Merge Add XML Based shouldWriteHeadersEagerly tests 2026-04-01 11:41:58 -05:00
Robert Winch 679a47a51d Add XML Based shouldWriteHeadersEagerly tests 2026-04-01 11:37:39 -05:00
Josh Cummings d4678c8e04 Add Missing Serialization Support
Closes gh-19013

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-31 15:55:09 -06:00
Josh Cummings 43b132bec6 Merge branch '6.5.x' into 7.0.x 2026-03-31 15:27:58 -06: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
Josh Cummings 067f79dde5 Merge branch 'fix-17729' into 7.0.x 2026-03-30 17:19:31 -06:00
Josh Cummings 45758a5cec Merge branch '6.5.x' into 7.0.x 2026-03-30 17:14:28 -06:00
Josh Cummings 52d98ab7af Add Needed SuppressWarnings Annotations
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-30 17:14:17 -06:00
Josh Cummings 0b680be97b Update Test to find SuppressWarnings
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-30 17:14:03 -06:00
johnycho 7c28b15471 Improve serialVersionUID check in tests
Signed-off-by: johnycho <shunnn215@gmail.com>
2026-03-30 14:26:12 -06:00
Rob Winch abf3c866fb Merge pull request #19005 from rwinch/7.0.x-CredentialRecordOwnerAuthorizationManager
Merge Add CredentialRecordOwnerAuthorizationManager
2026-03-29 23:46:35 -04:00
Rob Winch 5a4ada04ac Merge pull request #19004 from rwinch/CredentialRecordOwnerAuthorizationManager
Add CredentialRecordOwnerAuthorizationManager
2026-03-29 23:46:03 -04:00
Robert Winch c08329c0c5 Merge CredentialRecordOwnerAuthorizationManager 2026-03-29 22:24:21 -05: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 611786e4b5 Merge branch '6.5.x' into 7.0.x 2026-03-27 16:49:26 -06: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
Josh Cummings 6020ab8e65 Polish CustomAuthorizationManager Docs
Issue gh-13967

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

Signed-off-by: as1605 <1605.aditya.singh@gmail.com>
2026-03-27 16:36:00 -06:00
Josh Cummings 721b22d87a Merge remote-tracking branch 'origin/6.5.x' into 7.0.x 2026-03-27 16:10:18 -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
Josh Cummings 0ce76d2c5d Merge branch '6.5.x' into 7.0.x 2026-03-27 13:27:03 -06:00
dependabot[bot] 66cf02c6b0 Bump spring-io/spring-gradle-build-action from 2.0.5 to 2.0.6
Bumps [spring-io/spring-gradle-build-action](https://github.com/spring-io/spring-gradle-build-action) from 2.0.5 to 2.0.6.
- [Release notes](https://github.com/spring-io/spring-gradle-build-action/releases)
- [Commits](https://github.com/spring-io/spring-gradle-build-action/compare/efc55f07f4dfa22f2afd97f9ea1be4212eeed737...c8668747d7c264864c8c7f7026d0d277d14a78dc)

---
updated-dependencies:
- dependency-name: spring-io/spring-gradle-build-action
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:26:10 -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 e6db4418b0 Merge branch '6.5.x' into 7.0.x 2026-03-27 13:22:44 -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 9fb3e14989 Merge branch '6.5.x' into 7.0.x 2026-03-27 12:14:41 -06:00
Josh Cummings 2c90edd7b7 Merge branch '6.5.x' into 7.0.x 2026-03-27 12:12:27 -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 f0e71a8bc4 Merge pull request #18990 from rwinch/7.0.x-gh-18970-null-oncommitted
Merge Handle null value in OnCommittedResponseWrapper header methods
2026-03-26 17:33:33 -04: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 2848b95fe0 Merge Handle null value in OnCommittedResponseWrapper header methods 2026-03-26 15:44:49 -05:00
Robert Winch 0039bc0cf0 Handle null value in OnCommittedResponseWrapper header methods
Closes gh-18970
2026-03-26 14:50:44 -05:00
Josh Cummings 671a53e850 Merge branch '6.5.x' into 7.0.x 2026-03-25 15:19:59 -06: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
Josh Cummings 164fbaf007 Merge branch '6.5.x' into 7.0.x 2026-03-25 15:11:52 -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 65cf2586c5 Merge branch '6.5.x' into 7.0.x
Closes gh-18978
2026-03-25 12:40:43 -04:00
Joe Grandja 6e683f2286 Fix ID Token auth_time validation
Closes gh-18839
2026-03-25 11:33:55 -04:00
Josh Cummings bae4cdd765 Adjust for Nullability
Issue gh-18973

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-24 13:39:27 -06:00
Josh Cummings a7c3e842d6 Merge branch '6.5.x' into 7.0.x 2026-03-23 18:12:36 -06: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
Josh Cummings 7dea8b8ca2 Merge branch '6.5.x' into 7.0.x 2026-03-23 17:53:14 -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
Josh Cummings 4542f58be7 Merge branch '6.5.x' into 7.0.x 2026-03-20 21:27:04 -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
Josh Cummings 956561e143 Merge branch '6.5.x' into 7.0.x 2026-03-20 15:28:36 -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
Josh Cummings fbbbd46bee Update asciidoctor-extensions to 1.0.0-alpha.18
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-03-20 21:21:22 +00:00
Josh Cummings 8abffbd0df Merge branch '6.5.x' into 7.0.x 2026-03-20 15:00:02 -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
dependabot[bot] 874dce4407 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 14:54:26 -06:00
dependabot[bot] f21e8af830 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-20 14:54:11 -06:00
github-actions[bot] 8aae3490da Next development version 2026-03-16 19:05:05 +00: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
119 changed files with 3261 additions and 225 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ permissions:
jobs:
snapshot-test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@v1
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
strategy:
matrix:
include:
@@ -33,6 +33,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+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
+4 -4
View File
@@ -18,21 +18,21 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Compute Version
id: compute-version
uses: spring-io/spring-release-actions/compute-version@0.0.3
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@0.0.3
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@0.0.3
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@0.0.3
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 }}
+1 -1
View File
@@ -16,7 +16,7 @@ permissions:
jobs:
perform-release:
name: Perform Release
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
with:
should-perform-release: true
project-version: ${{ inputs.version }}
@@ -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 }}
+4 -4
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
java-version: '17'
distribution: 'temurin'
@@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
java-version: '17'
distribution: 'temurin'
@@ -34,7 +34,7 @@ jobs:
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
- name: Upload Docs
id: upload
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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: [ '6.4.x', '6.5.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 }}
@@ -35,6 +35,7 @@ import org.springframework.security.web.FilterInvocation;
*/
@Deprecated
@NullUnmarked
@SuppressWarnings("serial")
class WebExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<FilterInvocation> {
private final Expression authorizeExpression;
@@ -48,6 +48,7 @@ import org.springframework.security.jackson.SecurityJacksonModules;
* @since 7.0
* @see SecurityJacksonModules
*/
@SuppressWarnings("serial")
public class CasJacksonModule extends SecurityJacksonModule {
public CasJacksonModule() {
@@ -238,7 +238,9 @@ class HttpSecurityConfiguration {
Map<Class<?>, Object> sharedObjects = new HashMap<>();
sharedObjects.put(ApplicationContext.class, this.context);
sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy);
sharedObjects.put(PathPatternRequestMatcher.Builder.class, constructRequestMatcherBuilder(this.context));
sharedObjects.put(PathPatternRequestMatcher.Builder.class,
this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
.getIfUnique(() -> constructRequestMatcherBuilder(this.context)));
return sharedObjects;
}
@@ -39,6 +39,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;
@@ -180,6 +181,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
webAuthnAuthnFilter = postProcess(webAuthnAuthnFilter);
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
rpOperations);
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
rpOperations);
if (creationOptionsRepository != null) {
@@ -70,7 +70,7 @@ public final class PathPatternRequestMatcherFactoryBean
@Override
public void afterPropertiesSet() throws Exception {
if (this.basePath != null) {
this.builder.basePath(this.basePath);
this.builder = this.builder.basePath(this.basePath);
}
}
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
@@ -85,6 +86,9 @@ import org.springframework.security.authentication.password.CompromisedPasswordE
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.FactorAuthorizationDecision;
import org.springframework.security.authorization.RequiredFactor;
import org.springframework.security.authorization.RequiredFactorError;
import org.springframework.security.authorization.event.AuthorizationEvent;
import org.springframework.security.authorization.event.AuthorizationGrantedEvent;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
@@ -161,6 +165,7 @@ import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.JwtValidationException;
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
@@ -168,15 +173,22 @@ import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntro
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -190,6 +202,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
@@ -249,6 +262,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;
@@ -263,6 +277,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;
@@ -427,6 +442,8 @@ final class SerializationSamples {
generatorByClassName.put(RegisteredClient.class, (r) -> registeredClient);
generatorByClassName.put(OAuth2Authorization.class, (r) -> authorization);
generatorByClassName.put(OAuth2Authorization.Token.class, (r) -> authorization.getAccessToken());
generatorByClassName.put(OAuth2AuthorizationCode.class,
(r) -> new OAuth2AuthorizationCode("code", Instant.now(), Instant.now().plusSeconds(300)));
generatorByClassName.put(OAuth2AuthorizationConsent.class,
(r) -> OAuth2AuthorizationConsent.withId("registeredClientId", "principalName")
.scope("scope1")
@@ -452,6 +469,58 @@ final class SerializationSamples {
authenticationToken.setDetails(details);
return authenticationToken;
});
generatorByClassName.put(
org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken.class,
(r) -> {
org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken token = new org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken(
"code", principal, "https://localhost/callback", Map.of("custom_param", "custom_value"));
token.setDetails(details);
return token;
});
generatorByClassName.put(OAuth2AuthorizationCodeRequestAuthenticationException.class, (r) -> {
OAuth2AuthorizationCodeRequestAuthenticationToken authToken = new OAuth2AuthorizationCodeRequestAuthenticationToken(
"https://localhost/authorize", "clientId", principal, "https://localhost/callback", "state",
authorizationRequest.getScopes(), authorizationRequest.getAdditionalParameters());
return new OAuth2AuthorizationCodeRequestAuthenticationException(
new OAuth2Error("invalid_request", "Missing required parameter", "https://example.com/error"),
authToken);
});
generatorByClassName.put(OAuth2ClientCredentialsAuthenticationToken.class, (r) -> {
OAuth2ClientCredentialsAuthenticationToken token = new OAuth2ClientCredentialsAuthenticationToken(principal,
Set.of("scope1", "scope2"), Map.of("custom_param", "custom_value"));
token.setDetails(details);
return token;
});
generatorByClassName.put(OAuth2DeviceCodeAuthenticationToken.class, (r) -> {
OAuth2DeviceCodeAuthenticationToken token = new OAuth2DeviceCodeAuthenticationToken("device-code",
principal, Map.of("custom_param", "custom_value"));
token.setDetails(details);
return token;
});
generatorByClassName.put(OAuth2RefreshTokenAuthenticationToken.class, (r) -> {
OAuth2RefreshTokenAuthenticationToken token = new OAuth2RefreshTokenAuthenticationToken("refresh-token",
principal, Set.of("scope1", "scope2"), Map.of("custom_param", "custom_value"));
token.setDetails(details);
return token;
});
generatorByClassName.put(OAuth2TokenExchangeAuthenticationToken.class, (r) -> {
OAuth2TokenExchangeAuthenticationToken token = new OAuth2TokenExchangeAuthenticationToken(
"urn:ietf:params:oauth:token-type:access_token", "subject-token",
"urn:ietf:params:oauth:token-type:jwt", principal, "actor-token",
"urn:ietf:params:oauth:token-type:jwt", Set.of("https://resource.example.com"), Set.of("audience"),
Set.of("scope1"), Map.of("custom_param", "custom_value"));
token.setDetails(details);
return token;
});
OAuth2TokenExchangeActor actor = new OAuth2TokenExchangeActor(Map.of(OAuth2TokenClaimNames.ISS,
"https://issuer.example.com", OAuth2TokenClaimNames.SUB, "actor-subject"));
generatorByClassName.put(OAuth2TokenExchangeActor.class, (r) -> actor);
generatorByClassName.put(OAuth2TokenExchangeCompositeAuthenticationToken.class, (r) -> {
AbstractAuthenticationToken token = new OAuth2TokenExchangeCompositeAuthenticationToken(authentication,
List.of(actor));
token.setDetails(details);
return token;
});
generatorByClassName.put(OAuth2AuthorizationConsentAuthenticationToken.class, (r) -> {
OAuth2AuthorizationConsentAuthenticationToken authenticationToken = new OAuth2AuthorizationConsentAuthenticationToken(
"authorizationUri", "clientId", principal, "state", authorizationRequest.getScopes(),
@@ -668,6 +737,12 @@ final class SerializationSamples {
generatorByClassName.put(AuthorizationDecision.class, (r) -> new AuthorizationDecision(true));
generatorByClassName.put(AuthorityAuthorizationDecision.class,
(r) -> new AuthorityAuthorizationDecision(true, AuthorityUtils.createAuthorityList("ROLE_USER")));
RequiredFactor factor = RequiredFactor.withAuthority("authority").validDuration(Duration.ofSeconds(5)).build();
generatorByClassName.put(RequiredFactor.class, (r) -> factor);
RequiredFactorError error = RequiredFactorError.createMissing(factor);
generatorByClassName.put(RequiredFactorError.class, (r) -> error);
generatorByClassName.put(FactorAuthorizationDecision.class,
(r) -> new FactorAuthorizationDecision(List.of(error)));
generatorByClassName.put(CycleInRoleHierarchyException.class, (r) -> new CycleInRoleHierarchyException());
generatorByClassName.put(AuthorizationEvent.class,
(r) -> new AuthorizationEvent(new SerializableSupplier<>(authentication), "source",
@@ -858,6 +933,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("\\.");
@@ -452,6 +452,18 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
}
@Test
public void requestMatchersWhenBuilderBeanWithBasePathAndRawStringThenHonorsBasePath() throws Exception {
this.spring.register(RequestMatchersRawStringServletPathConfig.class, BasicController.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder matchedByBasePath = get("/spring/path")
.servletPath("/spring")
.with(user("user").roles("USER"));
// @formatter:on
this.mvc.perform(matchedByBasePath).andExpect(status().isForbidden());
this.mvc.perform(get("/path").with(user("user").roles("USER"))).andExpect(status().isOk());
}
@Test
public void getWhenAnyRequestAuthenticatedConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception {
this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire();
@@ -1359,6 +1371,32 @@ public class AuthorizeHttpRequestsConfigurerTests {
}
@Configuration
@EnableWebMvc
@EnableWebSecurity
static class RequestMatchersRawStringServletPathConfig {
@Bean
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
bean.setBasePath("/spring");
return bean;
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
return http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/path").hasRole("ADMIN")
.anyRequest().permitAll()
)
.build();
// @formatter:on
}
}
@Configuration
@EnableWebSecurity
static class AuthenticatedConfig {
@@ -125,6 +125,34 @@ public class HttpSecuritySecurityMatchersTests {
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void securityMatcherWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception {
loadConfig(SecurityMatcherBuilderBeanConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void securityMatchersWhenBuilderBeanWithBasePathAndRawStringsThenHonorsBasePath() throws Exception {
loadConfig(SecurityMatchersBuilderBeanConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void securityMatchersWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception {
loadConfig(MultiMvcMatcherInLambdaConfig.class);
@@ -430,6 +458,83 @@ public class HttpSecuritySecurityMatchersTests {
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
@Import(UsersConfig.class)
static class SecurityMatcherBuilderBeanConfig {
@Bean
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
bean.setBasePath("/spring");
return bean;
}
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatcher("/path")
.httpBasic(withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().denyAll());
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
@Import(UsersConfig.class)
static class SecurityMatchersBuilderBeanConfig {
@Bean
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
bean.setBasePath("/spring");
return bean;
}
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers((matchers) -> matchers
.requestMatchers("/path")
)
.httpBasic(withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().denyAll()
);
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@Configuration
static class UsersConfig {
@@ -43,9 +43,16 @@ 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.authentication.WebAuthnAuthenticationFilter;
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;
@@ -58,6 +65,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;
@@ -257,6 +265,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 {
@@ -475,4 +501,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();
}
}
}
@@ -79,6 +79,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationValidator;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -411,6 +412,102 @@ public class OAuth2ClientRegistrationTests {
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
}
@Test
public void requestWhenProtocolRelativeRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["//client.example.com/path"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenJavascriptSchemeRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["javascript:alert(document.cookie)"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenDataSchemeRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["data:text/html,<h1>content</h1>"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenHttpJwkSetUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["https://client.example.com"],
"grant_types": ["authorization_code"],
"jwks_uri": "http://169.254.169.254/keys",
"token_endpoint_auth_method": "private_key_jwt"
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenArbitraryScopeThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["https://client.example.com"],
"grant_types": ["client_credentials"],
"scope": "read write"
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
private int requestWhenInvalidClientMetadataThenBadRequest(String json) throws Exception {
String clientRegistrationScope = "client.create";
// @formatter:off
RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-" + System.nanoTime())
.clientId("client-registrar-" + System.nanoTime())
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope(clientRegistrationScope)
.build();
// @formatter:on
this.registeredClientRepository.save(clientRegistrar);
MvcResult tokenResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
.with(httpBasic(clientRegistrar.getClientId(), "secret")))
.andExpect(status().isOk())
.andReturn();
OAuth2AccessToken accessToken = readAccessTokenResponse(tokenResult.getResponse()).getAccessToken();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(accessToken.getTokenValue());
MvcResult registerResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andReturn();
return registerResult.getResponse().getStatus();
}
private OAuth2ClientRegistration registerClient(OAuth2ClientRegistration clientRegistration) throws Exception {
// ***** (1) Obtain the "initial" access token used for registering the client
@@ -496,6 +593,17 @@ public class OAuth2ClientRegistrationTests {
return clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class, httpResponse);
}
private static Consumer<List<AuthenticationProvider>> scopePermissiveValidatorCustomizer() {
return (authenticationProviders) -> authenticationProviders.forEach((authenticationProvider) -> {
if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
provider.setAuthenticationValidator(
OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
}
});
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
@@ -512,7 +620,7 @@ public class OAuth2ClientRegistrationTests {
.clientRegistrationRequestConverter(authenticationConverter)
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(authenticationProvidersConsumer))
.clientRegistrationResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler)
)
@@ -539,7 +647,7 @@ public class OAuth2ClientRegistrationTests {
authorizationServer
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
)
)
.authorizeHttpRequests((authorize) ->
@@ -577,7 +685,7 @@ public class OAuth2ClientRegistrationTests {
authorizationServer
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
)
)
.authorizeHttpRequests((authorize) ->
@@ -614,6 +722,7 @@ public class OAuth2ClientRegistrationTests {
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.openRegistrationAllowed(true)
.authenticationProviders(scopePermissiveValidatorCustomizer())
)
)
.authorizeHttpRequests((authorize) ->
@@ -627,6 +736,30 @@ public class OAuth2ClientRegistrationTests {
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class DefaultValidatorConfiguration extends AuthorizationServerConfiguration {
// Override with Customizer.withDefaults() so the default (strict)
// OAuth2ClientRegistrationAuthenticationValidator is in effect.
// @formatter:off
@Bean
@Override
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
@@ -637,7 +770,10 @@ public class OAuth2ClientRegistrationTests {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint(Customizer.withDefaults())
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(scopePermissiveValidatorCustomizer())
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
@@ -97,6 +97,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.OidcClientR
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationValidator;
import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter;
import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter;
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
@@ -545,6 +546,129 @@ public class OidcClientRegistrationTests {
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
}
@Test
public void requestWhenProtocolRelativeRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["//client.example.com/path"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenJavascriptSchemeRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["javascript:alert(document.cookie)"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenDataSchemeRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["data:text/html,<h1>content</h1>"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenJavascriptSchemePostLogoutRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["https://client.example.com"],
"post_logout_redirect_uris": ["javascript:alert(document.cookie)"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenDataSchemePostLogoutRedirectUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["https://client.example.com"],
"post_logout_redirect_uris": ["data:text/html,<h1>content</h1>"],
"grant_types": ["authorization_code"]
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenHttpJwkSetUriThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["https://client.example.com"],
"grant_types": ["authorization_code"],
"jwks_uri": "http://169.254.169.254/keys",
"token_endpoint_auth_method": "private_key_jwt"
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
@Test
public void requestWhenArbitraryScopeThenBadRequest() throws Exception {
this.spring.register(DefaultValidatorConfiguration.class).autowire();
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
{
"client_name": "client-name",
"redirect_uris": ["https://client.example.com"],
"grant_types": ["authorization_code"],
"scope": "read write"
}
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
}
private int requestWhenInvalidClientMetadataThenBadRequest(String json) throws Exception {
String clientRegistrationScope = "client.create";
String clientId = "client-registrar-" + System.nanoTime();
// @formatter:off
RegisteredClient clientRegistrar = RegisteredClient.withId(clientId)
.clientId(clientId)
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope(clientRegistrationScope)
.build();
// @formatter:on
this.registeredClientRepository.save(clientRegistrar);
MvcResult tokenResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
.with(httpBasic(clientId, "secret")))
.andExpect(status().isOk())
.andReturn();
OAuth2AccessToken accessToken = readAccessTokenResponse(tokenResult.getResponse()).getAccessToken();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(accessToken.getTokenValue());
MvcResult registerResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andReturn();
return registerResult.getResponse().getStatus();
}
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
// ***** (1) Obtain the "initial" access token used for registering the client
@@ -642,6 +766,18 @@ public class OidcClientRegistrationTests {
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
}
private static Consumer<List<AuthenticationProvider>> scopePermissiveValidatorCustomizer() {
return (authenticationProviders) -> authenticationProviders.forEach((authenticationProvider) -> {
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
provider.setAuthenticationValidator(
OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR.andThen(
OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
}
});
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
@@ -660,7 +796,7 @@ public class OidcClientRegistrationTests {
.clientRegistrationRequestConverter(authenticationConverter)
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(authenticationProvidersConsumer))
.clientRegistrationResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler)
)
@@ -690,7 +826,7 @@ public class OidcClientRegistrationTests {
oidc
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
)
)
)
@@ -731,7 +867,7 @@ public class OidcClientRegistrationTests {
oidc
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
)
)
)
@@ -755,6 +891,33 @@ public class OidcClientRegistrationTests {
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class DefaultValidatorConfiguration extends AuthorizationServerConfiguration {
// Override with Customizer.withDefaults() so the default (strict)
// OidcClientRegistrationAuthenticationValidator is in effect.
// @formatter:off
@Bean
@Override
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.oidc((oidc) ->
oidc
.clientRegistrationEndpoint(Customizer.withDefaults())
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
@@ -767,7 +930,10 @@ public class OidcClientRegistrationTests {
authorizationServer
.oidc((oidc) ->
oidc
.clientRegistrationEndpoint(Customizer.withDefaults())
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(scopePermissiveValidatorCustomizer())
)
)
)
.authorizeHttpRequests((authorize) ->
@@ -60,6 +60,7 @@ import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@@ -210,7 +211,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user").roles("A", "B")))
.with(user("user").roles("A", "B")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
@@ -270,7 +272,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user").roles("A", "B")))
.with(user("user").roles("A", "B")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
@@ -335,7 +338,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI)).queryParams(authorizationRequestParameters)
.with(user("user")))
.with(user("user")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
@@ -388,7 +392,8 @@ public class OidcTests {
registeredClient1);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user1")))
.with(user("user1")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
@@ -424,7 +429,8 @@ public class OidcTests {
authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient2);
mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user2")))
.with(user("user2")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
@@ -497,7 +503,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user")))
.with(user("user")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
@@ -537,7 +544,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user")))
.with(user("user")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
@@ -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
@@ -314,6 +314,78 @@ public class InterceptUrlConfigTests {
.autowire());
}
@Test
public void requestWhenUsingDefaultMatcherAndServletPathThenAuthorizesRequestsAccordingly() throws Exception {
this.spring.configLocations(this.xml("DefaultMatcherServletPath")).autowire();
// @formatter:off
this.mvc.perform(get("/spring/path").with(userCredentials()))
.andExpect(status().isForbidden());
this.mvc.perform(get("/path").with(userCredentials()))
.andExpect(status().isOk());
// @formatter:on
}
@Test
public void requestWhenUsingDefaultMatcherAndServletPathAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
throws Exception {
this.spring.configLocations(this.xml("DefaultMatcherServletPathAuthorizationManager")).autowire();
// @formatter:off
this.mvc.perform(get("/spring/path").with(userCredentials()))
.andExpect(status().isForbidden());
this.mvc.perform(get("/path").with(userCredentials()))
.andExpect(status().isOk());
// @formatter:on
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
}
@Test
public void requestWhenUsingRegexMatcherThenAuthorizesRequestsAccordingly() throws Exception {
this.spring.configLocations(this.xml("RegexMatcher")).autowire();
// @formatter:off
this.mvc.perform(get("/path").with(userCredentials()))
.andExpect(status().isForbidden());
this.mvc.perform(get("/other").with(userCredentials()))
.andExpect(status().isNotFound());
// @formatter:on
}
@Test
public void requestWhenUsingRegexMatcherAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
throws Exception {
this.spring.configLocations(this.xml("RegexMatcherAuthorizationManager")).autowire();
// @formatter:off
this.mvc.perform(get("/path").with(userCredentials()))
.andExpect(status().isForbidden());
this.mvc.perform(get("/other").with(userCredentials()))
.andExpect(status().isNotFound());
// @formatter:on
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
}
@Test
public void requestWhenUsingCiRegexMatcherThenAuthorizesRequestsAccordingly() throws Exception {
this.spring.configLocations(this.xml("CiRegexMatcher")).autowire();
// @formatter:off
this.mvc.perform(get("/path").with(userCredentials()))
.andExpect(status().isForbidden());
this.mvc.perform(get("/PATH").with(userCredentials()))
.andExpect(status().isForbidden());
// @formatter:on
}
@Test
public void requestWhenUsingCiRegexMatcherAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
throws Exception {
this.spring.configLocations(this.xml("CiRegexMatcherAuthorizationManager")).autowire();
// @formatter:off
this.mvc.perform(get("/path").with(userCredentials()))
.andExpect(status().isForbidden());
this.mvc.perform(get("/PATH").with(userCredentials()))
.andExpect(status().isForbidden());
// @formatter:on
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
}
@Test
public void requestWhenUsingFilterAllDispatcherTypesAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
throws Exception {
@@ -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>
@@ -0,0 +1,36 @@
<?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 request-matcher="ciRegex" use-authorization-manager="false">
<intercept-url pattern="\A/PATH\Z" access="denyAll"/>
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
<http-basic/>
</http>
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
<b:import resource="userservice.xml"/>
</b:beans>
@@ -0,0 +1,36 @@
<?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 request-matcher="ciRegex">
<intercept-url pattern="\A/PATH\Z" access="denyAll"/>
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
<http-basic/>
</http>
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
<b:import resource="userservice.xml"/>
</b:beans>
@@ -26,8 +26,11 @@
<http use-authorization-manager="false">
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
<intercept-url pattern="/**" access="permitAll"/>
<http-basic/>
</http>
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
<b:import resource="userservice.xml"/>
</b:beans>
@@ -26,8 +26,11 @@
<http>
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
<intercept-url pattern="/**" access="permitAll"/>
<http-basic/>
</http>
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
<b:import resource="userservice.xml"/>
</b:beans>
@@ -0,0 +1,36 @@
<?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 request-matcher="regex" use-authorization-manager="false">
<intercept-url pattern="\A/path\Z" access="denyAll"/>
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
<http-basic/>
</http>
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
<b:import resource="userservice.xml"/>
</b:beans>
@@ -0,0 +1,36 @@
<?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 request-matcher="regex">
<intercept-url pattern="\A/path\Z" access="denyAll"/>
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
<http-basic/>
</http>
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
<b:import resource="userservice.xml"/>
</b:beans>
@@ -97,6 +97,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
private boolean alwaysPerformAdditionalChecksOnUser = true;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private static final String AUTHORITY = FactorGrantedAuthority.PASSWORD_AUTHORITY;
@@ -154,8 +156,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) {
@@ -165,8 +166,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) {
@@ -179,6 +179,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();
}
@@ -324,6 +343,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;
}
@@ -153,7 +153,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;
}
@@ -171,11 +173,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 @Nullable ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
@@ -16,6 +16,7 @@
package org.springframework.security.authorization;
import java.io.Serial;
import java.util.Collections;
import java.util.List;
@@ -29,6 +30,9 @@ import org.springframework.util.Assert;
*/
public class FactorAuthorizationDecision implements AuthorizationResult {
@Serial
private static final long serialVersionUID = -245342816437885039L;
private final List<RequiredFactorError> factorErrors;
/**
@@ -16,6 +16,8 @@
package org.springframework.security.authorization;
import java.io.Serial;
import java.io.Serializable;
import java.time.Duration;
import java.util.Objects;
@@ -40,7 +42,10 @@ import org.springframework.util.Assert;
* @author Rob Winch
* @since 7.0
*/
public final class RequiredFactor {
public final class RequiredFactor implements Serializable {
@Serial
private static final long serialVersionUID = 295501208651764485L;
private final String authority;
@@ -16,6 +16,8 @@
package org.springframework.security.authorization;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import org.springframework.security.core.authority.FactorGrantedAuthority;
@@ -27,7 +29,10 @@ import org.springframework.util.Assert;
* @author Rob Winch
* @since 7.0
*/
public class RequiredFactorError {
public class RequiredFactorError implements Serializable {
@Serial
private static final long serialVersionUID = 1946221547278528901L;
private final RequiredFactor requiredFactor;
@@ -16,6 +16,7 @@
package org.springframework.security.authentication;
@SuppressWarnings("serial")
public class NonBuildableAuthenticationToken extends TestingAuthenticationToken {
public NonBuildableAuthenticationToken(String user, String password, String... authorities) {
@@ -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;
@@ -44,6 +45,7 @@ import org.springframework.security.core.authority.FactorGrantedAuthority;
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;
@@ -64,6 +66,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;
@@ -422,12 +425,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
@@ -460,6 +461,40 @@ 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();
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
users.password = encoder.encode((CharSequence) user.getCredentials());
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(users);
provider.setPasswordEncoder(encoder);
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);
@@ -376,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;
}
----
@@ -386,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
}
----
======
@@ -452,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;
}
----
@@ -463,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
}
----
======
@@ -479,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;
}
----
@@ -490,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
}
----
======
@@ -506,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;
}
----
@@ -520,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
}
----
======
@@ -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.
@@ -1382,12 +1382,15 @@ Java::
[source,java,role="primary"]
----
@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
public class MyPreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}
}
@Component
public class MyPostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
// ... authorization logic
@@ -1400,11 +1403,14 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
class MyPreAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocation> {
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationResult {
// ... authorization logic
}
}
@Component
class MyPostAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocationResult> {
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
// ... authorization logic
}
@@ -1427,13 +1433,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);
}
}
@@ -1446,15 +1452,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)
}
}
@@ -1471,13 +1477,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>
----
======
@@ -1487,6 +1493,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
@@ -519,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;
}
----
@@ -529,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
}
----
======
@@ -595,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;
}
----
@@ -606,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
}
----
======
@@ -622,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;
}
----
@@ -633,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
}
----
======
@@ -649,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;
}
----
@@ -663,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
}
----
======
+2 -2
View File
@@ -4,7 +4,7 @@
"@antora/atlas-extension": "1.0.0-alpha.5",
"@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"
}
}
+2
View File
@@ -14,3 +14,5 @@
^http://schemas.openid.net/event/backchannel-logout
^http://host.docker.internal:8090/back-channel/logout
^http://host.docker.internal:8090/logout
^http://169.254.169.254/keys
+1 -1
View File
@@ -14,7 +14,7 @@
# limitations under the License.
#
springBootVersion=4.0.0-SNAPSHOT
version=7.0.4
version=7.0.5
samplesBranch=main
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+6 -6
View File
@@ -12,7 +12,7 @@ org-jetbrains-kotlin = "2.2.21"
org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.17.0"
org-opensaml5 = "5.1.6"
org-springframework = "7.0.6"
org-springframework = "7.0.7"
com-password4j = "1.8.4"
[libraries]
@@ -30,7 +30,7 @@ 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.9"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.4"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.5"
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" }
@@ -51,7 +51,7 @@ net-sourceforge-htmlunit = "net.sourceforge.htmlunit:htmlunit:2.70.0"
org-htmlunit-htmlunit = "org.htmlunit:htmlunit:4.11.1"
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents.client5:httpclient5:5.5.2"
org-apache-kerby-simplekdc='org.apache.kerby:kerb-simplekdc:2.1.1'
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.14"
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" }
@@ -81,8 +81,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.4"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.2"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.5"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.3"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.4"
@@ -102,7 +102,7 @@ org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanne
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.6'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.1.RELEASE'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.3.RELEASE'
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
[plugins]
@@ -226,6 +226,7 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
return userCode != null && userCode.getToken().getTokenValue().equals(token);
}
@SuppressWarnings("serial")
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization;
import java.io.Serial;
import java.time.Instant;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
@@ -32,6 +33,9 @@ import org.springframework.security.oauth2.core.AbstractOAuth2Token;
*/
public class OAuth2AuthorizationCode extends AbstractOAuth2Token {
@Serial
private static final long serialVersionUID = 3789328028057414501L;
/**
* Constructs an {@code OAuth2AuthorizationCode} using the provided parameters.
* @param tokenValue the token value
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.util.Map;
import org.springframework.lang.Nullable;
@@ -36,6 +37,9 @@ import org.springframework.util.Assert;
*/
public class OAuth2AuthorizationCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
@Serial
private static final long serialVersionUID = 4629166286850598162L;
private final String code;
private final String redirectUri;
@@ -16,6 +16,8 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -33,6 +35,9 @@ import org.springframework.security.oauth2.core.OAuth2Error;
*/
public class OAuth2AuthorizationCodeRequestAuthenticationException extends OAuth2AuthenticationException {
@Serial
private static final long serialVersionUID = -3791188557904282453L;
private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication;
/**
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
@@ -36,6 +37,9 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
*/
public class OAuth2ClientCredentialsAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
@Serial
private static final long serialVersionUID = -220223451609576578L;
private final Set<String> scopes;
/**
@@ -0,0 +1,89 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An {@link OAuth2AuthenticationContext} that holds an
* {@link OAuth2ClientRegistrationAuthenticationToken} and additional information and is
* used when validating the OAuth 2.0 Client Registration Request parameters.
*
* @author addcontent
* @since 7.0.5
* @see OAuth2AuthenticationContext
* @see OAuth2ClientRegistrationAuthenticationToken
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
*/
public final class OAuth2ClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
private final Map<Object, Object> context;
private OAuth2ClientRegistrationAuthenticationContext(Map<Object, Object> context) {
this.context = Collections.unmodifiableMap(new HashMap<>(context));
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <V> V get(Object key) {
return hasKey(key) ? (V) this.context.get(key) : null;
}
@Override
public boolean hasKey(Object key) {
Assert.notNull(key, "key cannot be null");
return this.context.containsKey(key);
}
/**
* Constructs a new {@link Builder} with the provided
* {@link OAuth2ClientRegistrationAuthenticationToken}.
* @param authentication the {@link OAuth2ClientRegistrationAuthenticationToken}
* @return the {@link Builder}
*/
public static Builder with(OAuth2ClientRegistrationAuthenticationToken authentication) {
return new Builder(authentication);
}
/**
* A builder for {@link OAuth2ClientRegistrationAuthenticationContext}.
*/
public static final class Builder extends AbstractBuilder<OAuth2ClientRegistrationAuthenticationContext, Builder> {
private Builder(OAuth2ClientRegistrationAuthenticationToken authentication) {
super(authentication);
}
/**
* Builds a new {@link OAuth2ClientRegistrationAuthenticationContext}.
* @return the {@link OAuth2ClientRegistrationAuthenticationContext}
*/
@Override
public OAuth2ClientRegistrationAuthenticationContext build() {
return new OAuth2ClientRegistrationAuthenticationContext(getContext());
}
}
}
@@ -16,12 +16,10 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -35,12 +33,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -49,7 +45,6 @@ import org.springframework.security.oauth2.server.authorization.converter.OAuth2
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -67,8 +62,6 @@ import org.springframework.util.StringUtils;
*/
public final class OAuth2ClientRegistrationAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
private final Log logger = LogFactory.getLog(getClass());
@@ -85,6 +78,8 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
private boolean openRegistrationAllowed;
private Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator;
/**
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
* provided parameters.
@@ -99,6 +94,7 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
this.clientRegistrationConverter = new RegisteredClientOAuth2ClientRegistrationConverter();
this.registeredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
this.authenticationValidator = new OAuth2ClientRegistrationAuthenticationValidator();
}
@Override
@@ -197,14 +193,35 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
this.openRegistrationAllowed = openRegistrationAllowed;
}
/**
* Sets the {@code Consumer} providing access to the
* {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
* validating specific OAuth 2.0 Client Registration Request parameters associated in
* the {@link OAuth2ClientRegistrationAuthenticationToken}. The default authentication
* validator is {@link OAuth2ClientRegistrationAuthenticationValidator}.
*
* <p>
* <b>NOTE:</b> The authentication validator MUST throw
* {@link OAuth2AuthenticationException} if validation fails.
* @param authenticationValidator the {@code Consumer} providing access to the
* {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
* validating specific OAuth 2.0 Client Registration Request parameters
* @since 7.0.5
*/
public void setAuthenticationValidator(
Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator) {
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
this.authenticationValidator = authenticationValidator;
}
private OAuth2ClientRegistrationAuthenticationToken registerClient(
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
OAuth2Authorization authorization) {
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
OAuth2ClientRegistrationAuthenticationContext authenticationContext = OAuth2ClientRegistrationAuthenticationContext
.with(clientRegistrationAuthentication)
.build();
this.authenticationValidator.accept(authenticationContext);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated client registration request parameters");
@@ -277,29 +294,4 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
}
}
private static boolean isValidRedirectUris(List<String> redirectUris) {
if (CollectionUtils.isEmpty(redirectUris)) {
return true;
}
for (String redirectUri : redirectUris) {
try {
URI validRedirectUri = new URI(redirectUri);
if (validRedirectUri.getFragment() != null) {
return false;
}
}
catch (URISyntaxException ex) {
return false;
}
}
return true;
}
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
}
@@ -0,0 +1,244 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.util.CollectionUtils;
/**
* A {@code Consumer} providing access to the
* {@link OAuth2ClientRegistrationAuthenticationContext} containing an
* {@link OAuth2ClientRegistrationAuthenticationToken} and is the default
* {@link OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
* authentication validator} used for validating specific OAuth 2.0 Dynamic Client
* Registration Request parameters (RFC 7591).
*
* <p>
* The default implementation validates {@link OAuth2ClientRegistration#getRedirectUris()
* redirect_uris}, {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}, and
* {@link OAuth2ClientRegistration#getScopes() scope}. If validation fails, an
* {@link OAuth2AuthenticationException} is thrown.
*
* <p>
* Each validated field is backed by two public constants:
* <ul>
* <li>{@code DEFAULT_*_VALIDATOR} -- strict validation that rejects unsafe values. This
* is the default behavior and may reject input that was previously accepted.</li>
* <li>{@code SIMPLE_*_VALIDATOR} -- lenient validation preserving the behavior from prior
* releases. Use only when strictly required for backward compatibility and with full
* understanding that it may accept values that enable attacks against the authorization
* server.</li>
* </ul>
*
* @author addcontent
* @since 7.0.5
* @see OAuth2ClientRegistrationAuthenticationContext
* @see OAuth2ClientRegistrationAuthenticationToken
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
*/
public final class OAuth2ClientRegistrationAuthenticationValidator
implements Consumer<OAuth2ClientRegistrationAuthenticationContext> {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientRegistrationAuthenticationValidator.class);
/**
* The default validator for {@link OAuth2ClientRegistration#getRedirectUris()
* redirect_uris}. Rejects URIs that contain a fragment, have no scheme (e.g.
* protocol-relative {@code //host/path}), or use an unsafe scheme
* ({@code javascript}, {@code data}, {@code vbscript}).
*/
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUris;
/**
* The simple validator for {@link OAuth2ClientRegistration#getRedirectUris()
* redirect_uris} that preserves prior behavior (fragment-only check). Use only when
* backward compatibility is required; values that enable open redirect and XSS
* attacks may be accepted.
*/
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
/**
* The default validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}.
* Rejects URIs that do not use the {@code https} scheme.
*/
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUri;
/**
* The simple validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}
* that preserves prior behavior (no validation). Use only when backward compatibility
* is required; values that enable SSRF attacks may be accepted.
*/
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
/**
* The default validator for {@link OAuth2ClientRegistration#getScopes() scope}.
* Rejects any request that includes a non-empty scope value. Deployers that need to
* accept scopes during Dynamic Client Registration must configure their own validator
* (for example, by chaining on top of {@link #SIMPLE_SCOPE_VALIDATOR}).
*/
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScope;
/**
* The simple validator for {@link OAuth2ClientRegistration#getScopes() scope} that
* preserves prior behavior (accepts any scope). Use only when backward compatibility
* is required; values that enable arbitrary scope injection may be accepted.
*/
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScopeSimple;
private final Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(DEFAULT_SCOPE_VALIDATOR);
@Override
public void accept(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
this.authenticationValidator.accept(authenticationContext);
}
private static void validateRedirectUris(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
if (CollectionUtils.isEmpty(redirectUris)) {
return;
}
for (String redirectUri : redirectUris) {
URI parsed;
try {
parsed = new URI(redirectUri);
}
catch (URISyntaxException ex) {
if (LOGGER.isDebugEnabled()) {
LOGGER
.debug(LogMessage.format("Invalid request: redirect_uri is not parseable ('%s')", redirectUri));
}
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
return;
}
if (parsed.getFragment() != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
LogMessage.format("Invalid request: redirect_uri contains a fragment ('%s')", redirectUri));
}
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
String scheme = parsed.getScheme();
if (scheme == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format("Invalid request: redirect_uri has no scheme ('%s')", redirectUri));
}
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
if (isUnsafeScheme(scheme)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
LogMessage.format("Invalid request: redirect_uri uses unsafe scheme ('%s')", redirectUri));
}
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
}
}
private static void validateRedirectUrisSimple(
OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
if (CollectionUtils.isEmpty(redirectUris)) {
return;
}
for (String redirectUri : redirectUris) {
try {
URI parsed = new URI(redirectUri);
if (parsed.getFragment() != null) {
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
}
catch (URISyntaxException ex) {
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
}
}
private static void validateJwkSetUri(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
URL jwkSetUrl = clientRegistrationAuthentication.getClientRegistration().getJwkSetUrl();
if (jwkSetUrl == null) {
return;
}
if (!"https".equalsIgnoreCase(jwkSetUrl.getProtocol())) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format("Invalid request: jwks_uri does not use https ('%s')", jwkSetUrl));
}
throwInvalidClientRegistration("invalid_client_metadata", OAuth2ClientMetadataClaimNames.JWKS_URI);
}
}
private static void validateJwkSetUriSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
// No validation. Preserves prior behavior.
}
private static void validateScope(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> scopes = clientRegistrationAuthentication.getClientRegistration().getScopes();
if (!CollectionUtils.isEmpty(scopes)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format(
"Invalid request: scope must not be set during Dynamic Client Registration ('%s')", scopes));
}
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ClientMetadataClaimNames.SCOPE);
}
}
private static void validateScopeSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
// No validation. Preserves prior behavior.
}
private static boolean isUnsafeScheme(String scheme) {
return "javascript".equalsIgnoreCase(scheme) || "data".equalsIgnoreCase(scheme)
|| "vbscript".equalsIgnoreCase(scheme);
}
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
}
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.util.Map;
import org.springframework.lang.Nullable;
@@ -34,6 +35,9 @@ import org.springframework.util.Assert;
*/
public class OAuth2DeviceCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
@Serial
private static final long serialVersionUID = 8364555864666204030L;
private final String deviceCode;
/**
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
@@ -36,6 +37,9 @@ import org.springframework.util.Assert;
*/
public class OAuth2RefreshTokenAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
@Serial
private static final long serialVersionUID = 328697547826078993L;
private final String refreshToken;
private final Set<String> scopes;
@@ -16,6 +16,8 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
@@ -33,7 +35,10 @@ import org.springframework.util.Assert;
* @since 7.0
* @see OAuth2TokenExchangeCompositeAuthenticationToken
*/
public final class OAuth2TokenExchangeActor implements ClaimAccessor {
public final class OAuth2TokenExchangeActor implements ClaimAccessor, Serializable {
@Serial
private static final long serialVersionUID = -3966261411784615574L;
private final Map<String, Object> claims;
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -37,6 +38,9 @@ import org.springframework.util.Assert;
*/
public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
@Serial
private static final long serialVersionUID = 2484741634669297785L;
private final String requestedTokenType;
private final String subjectToken;
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -35,6 +36,9 @@ import org.springframework.util.Assert;
*/
public class OAuth2TokenExchangeCompositeAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 1912280308201180854L;
private final Authentication subject;
private final List<OAuth2TokenExchangeActor> actors;
@@ -0,0 +1,90 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationContext;
import org.springframework.util.Assert;
/**
* An {@link OAuth2AuthenticationContext} that holds an
* {@link OidcClientRegistrationAuthenticationToken} and additional information and is
* used when validating the OpenID Connect 1.0 Client Registration Request parameters.
*
* @author addcontent
* @since 7.0.5
* @see OAuth2AuthenticationContext
* @see OidcClientRegistrationAuthenticationToken
* @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
*/
public final class OidcClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
private final Map<Object, Object> context;
private OidcClientRegistrationAuthenticationContext(Map<Object, Object> context) {
this.context = Collections.unmodifiableMap(new HashMap<>(context));
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <V> V get(Object key) {
return hasKey(key) ? (V) this.context.get(key) : null;
}
@Override
public boolean hasKey(Object key) {
Assert.notNull(key, "key cannot be null");
return this.context.containsKey(key);
}
/**
* Constructs a new {@link Builder} with the provided
* {@link OidcClientRegistrationAuthenticationToken}.
* @param authentication the {@link OidcClientRegistrationAuthenticationToken}
* @return the {@link Builder}
*/
public static Builder with(OidcClientRegistrationAuthenticationToken authentication) {
return new Builder(authentication);
}
/**
* A builder for {@link OidcClientRegistrationAuthenticationContext}.
*/
public static final class Builder extends AbstractBuilder<OidcClientRegistrationAuthenticationContext, Builder> {
private Builder(OidcClientRegistrationAuthenticationToken authentication) {
super(authentication);
}
/**
* Builds a new {@link OidcClientRegistrationAuthenticationContext}.
* @return the {@link OidcClientRegistrationAuthenticationContext}
*/
@Override
public OidcClientRegistrationAuthenticationContext build() {
return new OidcClientRegistrationAuthenticationContext(getContext());
}
}
}
@@ -16,14 +16,12 @@
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -60,7 +58,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -102,6 +99,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
private PasswordEncoder passwordEncoder;
private Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator;
/**
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the
* provided parameters.
@@ -121,6 +120,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter();
this.registeredClientConverter = new OidcClientRegistrationRegisteredClientConverter();
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
this.authenticationValidator = new OidcClientRegistrationAuthenticationValidator();
}
@Override
@@ -206,20 +206,35 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
this.passwordEncoder = passwordEncoder;
}
/**
* Sets the {@code Consumer} providing access to the
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
* validating specific OpenID Connect 1.0 Client Registration Request parameters
* associated in the {@link OidcClientRegistrationAuthenticationToken}. The default
* authentication validator is {@link OidcClientRegistrationAuthenticationValidator}.
*
* <p>
* <b>NOTE:</b> The authentication validator MUST throw
* {@link OAuth2AuthenticationException} if validation fails.
* @param authenticationValidator the {@code Consumer} providing access to the
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
* validating specific OpenID Connect 1.0 Client Registration Request parameters
* @since 7.0.5
*/
public void setAuthenticationValidator(
Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator) {
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
this.authenticationValidator = authenticationValidator;
}
private OidcClientRegistrationAuthenticationToken registerClient(
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
OAuth2Authorization authorization) {
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OidcClientMetadataClaimNames.REDIRECT_URIS);
}
if (!isValidRedirectUris(
clientRegistrationAuthentication.getClientRegistration().getPostLogoutRedirectUris())) {
throwInvalidClientRegistration("invalid_client_metadata",
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
OidcClientRegistrationAuthenticationContext authenticationContext = OidcClientRegistrationAuthenticationContext
.with(clientRegistrationAuthentication)
.build();
this.authenticationValidator.accept(authenticationContext);
if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationAuthentication.getClientRegistration())) {
throwInvalidClientRegistration("invalid_client_metadata",
@@ -351,26 +366,6 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
}
}
private static boolean isValidRedirectUris(List<String> redirectUris) {
if (CollectionUtils.isEmpty(redirectUris)) {
return true;
}
for (String redirectUri : redirectUris) {
try {
URI validRedirectUri = new URI(redirectUri);
if (validRedirectUri.getFragment() != null) {
return false;
}
}
catch (URISyntaxException ex) {
return false;
}
}
return true;
}
private static boolean isValidTokenEndpointAuthenticationMethod(OidcClientRegistration clientRegistration) {
String authenticationMethod = clientRegistration.getTokenEndpointAuthenticationMethod();
String authenticationSigningAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
@@ -0,0 +1,285 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
import org.springframework.util.CollectionUtils;
/**
* A {@code Consumer} providing access to the
* {@link OidcClientRegistrationAuthenticationContext} containing an
* {@link OidcClientRegistrationAuthenticationToken} and is the default
* {@link OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
* authentication validator} used for validating specific OpenID Connect 1.0 Dynamic
* Client Registration Request parameters.
*
* <p>
* The default implementation validates {@link OidcClientRegistration#getRedirectUris()
* redirect_uris}, {@link OidcClientRegistration#getPostLogoutRedirectUris()
* post_logout_redirect_uris}, {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}, and
* {@link OidcClientRegistration#getScopes() scope}. If validation fails, an
* {@link OAuth2AuthenticationException} is thrown.
*
* <p>
* Each validated field is backed by two public constants:
* <ul>
* <li>{@code DEFAULT_*_VALIDATOR} -- strict validation that rejects unsafe values. This
* is the default behavior and may reject input that was previously accepted.</li>
* <li>{@code SIMPLE_*_VALIDATOR} -- lenient validation preserving the behavior from prior
* releases. Use only when strictly required for backward compatibility and with full
* understanding that it may accept values that enable attacks against the authorization
* server.</li>
* </ul>
*
* @author addcontent
* @since 7.0.5
* @see OidcClientRegistrationAuthenticationContext
* @see OidcClientRegistrationAuthenticationToken
* @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
*/
public final class OidcClientRegistrationAuthenticationValidator
implements Consumer<OidcClientRegistrationAuthenticationContext> {
private static final String ERROR_URI = "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError";
private static final Log LOGGER = LogFactory.getLog(OidcClientRegistrationAuthenticationValidator.class);
/**
* The default validator for {@link OidcClientRegistration#getRedirectUris()
* redirect_uris}. Rejects URIs that contain a fragment, have no scheme (e.g.
* protocol-relative {@code //host/path}), or use an unsafe scheme
* ({@code javascript}, {@code data}, {@code vbscript}).
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUris;
/**
* The simple validator for {@link OidcClientRegistration#getRedirectUris()
* redirect_uris} that preserves prior behavior (fragment-only check). Use only when
* backward compatibility is required; values that enable open redirect and XSS
* attacks may be accepted.
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
/**
* The default validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
* post_logout_redirect_uris}. Applies the same rules as
* {@link #DEFAULT_REDIRECT_URI_VALIDATOR}.
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUris;
/**
* The simple validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
* post_logout_redirect_uris} that preserves prior behavior (fragment-only check). Use
* only when backward compatibility is required; values that enable XSS attacks on the
* authorization server origin may be accepted.
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUrisSimple;
/**
* The default validator for {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}.
* Rejects URIs that do not use the {@code https} scheme.
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUri;
/**
* The simple validator for {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}
* that preserves prior behavior (no validation). Use only when backward compatibility
* is required; values that enable SSRF attacks may be accepted.
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
/**
* The default validator for {@link OidcClientRegistration#getScopes() scope}. Rejects
* any request that includes a non-empty scope value. Deployers that need to accept
* scopes during Dynamic Client Registration must configure their own validator (for
* example, by chaining on top of {@link #SIMPLE_SCOPE_VALIDATOR}).
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScope;
/**
* The simple validator for {@link OidcClientRegistration#getScopes() scope} that
* preserves prior behavior (accepts any scope). Use only when backward compatibility
* is required; values that enable arbitrary scope injection may be accepted.
*/
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScopeSimple;
private final Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
.andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(DEFAULT_SCOPE_VALIDATOR);
@Override
public void accept(OidcClientRegistrationAuthenticationContext authenticationContext) {
this.authenticationValidator.accept(authenticationContext);
}
private static void validateRedirectUris(OidcClientRegistrationAuthenticationContext authenticationContext) {
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
validateRedirectUrisStrict(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OidcClientMetadataClaimNames.REDIRECT_URIS);
}
private static void validatePostLogoutRedirectUris(
OidcClientRegistrationAuthenticationContext authenticationContext) {
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
.getPostLogoutRedirectUris();
validateRedirectUrisStrict(postLogoutRedirectUris, "invalid_client_metadata",
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
private static void validateRedirectUrisStrict(List<String> redirectUris, String errorCode, String fieldName) {
if (CollectionUtils.isEmpty(redirectUris)) {
return;
}
for (String redirectUri : redirectUris) {
URI parsed;
try {
parsed = new URI(redirectUri);
}
catch (URISyntaxException ex) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
LogMessage.format("Invalid request: %s is not parseable ('%s')", fieldName, redirectUri));
}
throwInvalidClientRegistration(errorCode, fieldName);
return;
}
if (parsed.getFragment() != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format("Invalid request: %s contains a fragment ('%s')", fieldName,
redirectUri));
}
throwInvalidClientRegistration(errorCode, fieldName);
}
String scheme = parsed.getScheme();
if (scheme == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format("Invalid request: %s has no scheme ('%s')", fieldName, redirectUri));
}
throwInvalidClientRegistration(errorCode, fieldName);
}
if (isUnsafeScheme(scheme)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
LogMessage.format("Invalid request: %s uses unsafe scheme ('%s')", fieldName, redirectUri));
}
throwInvalidClientRegistration(errorCode, fieldName);
}
}
}
private static void validateRedirectUrisSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
validateRedirectUrisFragmentOnly(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OidcClientMetadataClaimNames.REDIRECT_URIS);
}
private static void validatePostLogoutRedirectUrisSimple(
OidcClientRegistrationAuthenticationContext authenticationContext) {
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
.getPostLogoutRedirectUris();
validateRedirectUrisFragmentOnly(postLogoutRedirectUris, "invalid_client_metadata",
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
private static void validateRedirectUrisFragmentOnly(List<String> redirectUris, String errorCode,
String fieldName) {
if (CollectionUtils.isEmpty(redirectUris)) {
return;
}
for (String redirectUri : redirectUris) {
try {
URI parsed = new URI(redirectUri);
if (parsed.getFragment() != null) {
throwInvalidClientRegistration(errorCode, fieldName);
}
}
catch (URISyntaxException ex) {
throwInvalidClientRegistration(errorCode, fieldName);
}
}
}
private static void validateJwkSetUri(OidcClientRegistrationAuthenticationContext authenticationContext) {
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
URL jwkSetUrl = clientRegistrationAuthentication.getClientRegistration().getJwkSetUrl();
if (jwkSetUrl == null) {
return;
}
if (!"https".equalsIgnoreCase(jwkSetUrl.getProtocol())) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format("Invalid request: jwks_uri does not use https ('%s')", jwkSetUrl));
}
throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.JWKS_URI);
}
}
private static void validateJwkSetUriSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
// No validation. Preserves prior behavior.
}
private static void validateScope(OidcClientRegistrationAuthenticationContext authenticationContext) {
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
.getAuthentication();
List<String> scopes = clientRegistrationAuthentication.getClientRegistration().getScopes();
if (!CollectionUtils.isEmpty(scopes)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format(
"Invalid request: scope must not be set during Dynamic Client Registration ('%s')", scopes));
}
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, OidcClientMetadataClaimNames.SCOPE);
}
}
private static void validateScopeSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
// No validation. Preserves prior behavior.
}
private static boolean isUnsafeScheme(String scheme) {
return "javascript".equalsIgnoreCase(scheme) || "data".equalsIgnoreCase(scheme)
|| "vbscript".equalsIgnoreCase(scheme);
}
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
}
@@ -24,6 +24,9 @@ import java.util.Date;
import java.util.UUID;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
@@ -141,7 +144,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
SessionInformation sessionInformation = context.get(SessionInformation.class);
if (sessionInformation != null) {
claimsBuilder.claim("sid", sessionInformation.getSessionId());
claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, sessionInformation.getLastRequest());
claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, getAuthenticationTime(context.getPrincipal()));
}
}
else if (AuthorizationGrantType.REFRESH_TOKEN.equals(context.getAuthorizationGrantType())) {
@@ -222,4 +225,17 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
this.clock = clock;
}
static Date getAuthenticationTime(Authentication authentication) {
Instant authenticationTime = null;
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (grantedAuthority instanceof FactorGrantedAuthority factorGrantedAuthority) {
if (authenticationTime == null || factorGrantedAuthority.getIssuedAt().isAfter(authenticationTime)) {
authenticationTime = factorGrantedAuthority.getIssuedAt();
}
}
}
Assert.notNull(authenticationTime, "authenticationTime cannot be null");
return Date.from(authenticationTime);
}
}
@@ -154,6 +154,7 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
this.clock = clock;
}
@SuppressWarnings("serial")
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
private final Map<String, Object> claims;
@@ -24,6 +24,9 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
@@ -85,6 +88,9 @@ public final class TestOAuth2Authorizations {
.additionalParameters(authorizationRequestAdditionalParameters)
.state("state")
.build();
Authentication principal = new TestingAuthenticationToken("principal", null,
new SimpleGrantedAuthority("ROLE_A"), new SimpleGrantedAuthority("ROLE_B"),
FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY));
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient)
.id("id")
.principalName("principal")
@@ -93,8 +99,7 @@ public final class TestOAuth2Authorizations {
.token(authorizationCode)
.attribute(OAuth2ParameterNames.STATE, "consent-state")
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
.attribute(Principal.class.getName(),
new TestingAuthenticationToken("principal", null, "ROLE_A", "ROLE_B"));
.attribute(Principal.class.getName(), principal);
if (accessToken != null) {
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now(),
Instant.now().plus(1, ChronoUnit.HOURS));
@@ -360,6 +360,11 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
@Test
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
this.authenticationProvider
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
@@ -412,6 +417,10 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
@Test
public void authenticateWhenOpenRegistrationThenReturnClientRegistration() {
this.authenticationProvider.setOpenRegistrationAllowed(true);
this.authenticationProvider
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
@@ -0,0 +1,197 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link OAuth2ClientRegistrationAuthenticationValidator}.
*
* @author addcontent
*/
public class OAuth2ClientRegistrationAuthenticationValidatorTests {
private final OAuth2ClientRegistrationAuthenticationValidator validator = new OAuth2ClientRegistrationAuthenticationValidator();
@Test
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
assertRejected(context("//client.example.com/path", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
@Test
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
assertRejected(context("javascript:alert(document.cookie)", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
@Test
public void defaultRedirectUriValidatorWhenDataSchemeThenRejected() {
assertRejected(context("data:text/html,<h1>content</h1>", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
@Test
public void defaultRedirectUriValidatorWhenVbscriptSchemeThenRejected() {
assertRejected(context("vbscript:msgbox(\"content\")", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
@Test
public void defaultRedirectUriValidatorWhenFragmentThenRejected() {
assertRejected(context("https://client.example.com/cb#fragment", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
@Test
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
}
@Test
public void defaultRedirectUriValidatorWhenCustomSchemeForNativeAppThenAccepted() {
assertThatNoException().isThrownBy(() -> this.validator.accept(context("myapp://callback", null)));
}
@Test
public void defaultRedirectUriValidatorWhenHttpLoopbackThenAccepted() {
assertThatNoException().isThrownBy(() -> this.validator.accept(context("http://127.0.0.1:8080", null)));
}
@Test
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
assertRejected(context("https://client.example.com", "http://169.254.169.254/keys"), "invalid_client_metadata",
OAuth2ClientMetadataClaimNames.JWKS_URI);
}
@Test
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
assertThatNoException().isThrownBy(
() -> this.validator.accept(context("https://client.example.com", "https://client.example.com/jwks")));
}
@Test
public void defaultJwkSetUriValidatorWhenAbsentThenAccepted() {
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
}
@Test
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.scope("write")
.build()))
.build();
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
.extracting(OAuth2AuthenticationException::getError)
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
}
@Test
public void defaultScopeValidatorWhenEmptyThenAccepted() {
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
}
@Test
public void simpleRedirectUriValidatorWhenProtocolRelativeThenAccepted() {
OAuth2ClientRegistrationAuthenticationContext context = context("//client.example.com/path", null);
assertThatNoException().isThrownBy(
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
}
@Test
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
OAuth2ClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null);
assertThatNoException().isThrownBy(
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
}
@Test
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
OAuth2ClientRegistrationAuthenticationContext context = context("https://client.example.com",
"http://169.254.169.254/keys");
assertThatNoException().isThrownBy(
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(context));
}
@Test
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.scope("write")
.build()))
.build();
assertThatNoException()
.isThrownBy(() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
}
@Test
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
Consumer<OAuth2ClientRegistrationAuthenticationContext> composed = OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR);
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.jwkSetUrl("https://client.example.com/jwks")
.scope("openid")
.scope("profile")
.build()))
.build();
assertThatNoException().isThrownBy(() -> composed.accept(context));
}
private static OAuth2ClientRegistrationAuthenticationContext context(String redirectUri, String jwkSetUrl) {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder();
if (redirectUri != null) {
builder.redirectUri(redirectUri);
}
if (jwkSetUrl != null) {
builder.jwkSetUrl(jwkSetUrl);
}
return OAuth2ClientRegistrationAuthenticationContext
.with(new OAuth2ClientRegistrationAuthenticationToken(null, builder.build()))
.build();
}
private void assertRejected(OAuth2ClientRegistrationAuthenticationContext context, String errorCode,
String fieldName) {
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
.extracting(OAuth2AuthenticationException::getError)
.satisfies((error) -> {
assertThat(error.getErrorCode()).isEqualTo(errorCode);
assertThat(error.getDescription()).contains(fieldName);
});
}
}
@@ -561,6 +561,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
@Test
public void authenticateWhenTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
this.authenticationProvider
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
@@ -612,6 +617,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
@Test
public void authenticateWhenRegistrationAccessTokenNotGeneratedThenThrowOAuth2AuthenticationException() {
this.authenticationProvider
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
@@ -653,6 +663,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
@Test
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
this.authenticationProvider
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
@@ -0,0 +1,188 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link OidcClientRegistrationAuthenticationValidator}.
*
* @author addcontent
*/
public class OidcClientRegistrationAuthenticationValidatorTests {
private final OidcClientRegistrationAuthenticationValidator validator = new OidcClientRegistrationAuthenticationValidator();
@Test
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
assertRejected(context("//client.example.com/path", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OidcClientMetadataClaimNames.REDIRECT_URIS);
}
@Test
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
assertRejected(context("javascript:alert(document.cookie)", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OidcClientMetadataClaimNames.REDIRECT_URIS);
}
@Test
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
assertThatNoException()
.isThrownBy(() -> this.validator.accept(context("https://client.example.com", null, null)));
}
@Test
public void defaultPostLogoutRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
assertRejected(context("https://client.example.com", "javascript:alert(document.cookie)", null),
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
@Test
public void defaultPostLogoutRedirectUriValidatorWhenProtocolRelativeThenRejected() {
assertRejected(context("https://client.example.com", "//client.example.com/post-logout", null),
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
@Test
public void defaultPostLogoutRedirectUriValidatorWhenHttpsThenAccepted() {
assertThatNoException().isThrownBy(() -> this.validator
.accept(context("https://client.example.com", "https://client.example.com/post-logout", null)));
}
@Test
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
assertRejected(context("https://client.example.com", null, "http://169.254.169.254/keys"),
"invalid_client_metadata", OidcClientMetadataClaimNames.JWKS_URI);
}
@Test
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
assertThatNoException().isThrownBy(() -> this.validator
.accept(context("https://client.example.com", null, "https://client.example.com/jwks")));
}
@Test
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
.with(new OidcClientRegistrationAuthenticationToken(principal(),
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
.build();
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
.extracting(OAuth2AuthenticationException::getError)
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
}
@Test
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
OidcClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null, null);
assertThatNoException().isThrownBy(
() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
}
@Test
public void simplePostLogoutRedirectUriValidatorWhenJavascriptThenAccepted() {
OidcClientRegistrationAuthenticationContext context = context("https://client.example.com",
"javascript:alert(document.cookie)", null);
assertThatNoException()
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR
.accept(context));
}
@Test
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
OidcClientRegistrationAuthenticationContext ctx = context("https://client.example.com", null,
"http://169.254.169.254/keys");
assertThatNoException()
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(ctx));
}
@Test
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
.with(new OidcClientRegistrationAuthenticationToken(principal(),
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
.build();
assertThatNoException()
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
}
@Test
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
Consumer<OidcClientRegistrationAuthenticationContext> composed = OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR);
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
.with(new OidcClientRegistrationAuthenticationToken(principal(),
OidcClientRegistration.builder()
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/post-logout")
.jwkSetUrl("https://client.example.com/jwks")
.scope("openid")
.scope("profile")
.build()))
.build();
assertThatNoException().isThrownBy(() -> composed.accept(context));
}
private static Authentication principal() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password");
principal.setAuthenticated(true);
return principal;
}
private static OidcClientRegistrationAuthenticationContext context(String redirectUri, String postLogoutRedirectUri,
String jwkSetUrl) {
OidcClientRegistration.Builder builder = OidcClientRegistration.builder();
if (redirectUri != null) {
builder.redirectUri(redirectUri);
}
if (postLogoutRedirectUri != null) {
builder.postLogoutRedirectUri(postLogoutRedirectUri);
}
if (jwkSetUrl != null) {
builder.jwkSetUrl(jwkSetUrl);
}
return OidcClientRegistrationAuthenticationContext
.with(new OidcClientRegistrationAuthenticationToken(principal(), builder.build()))
.build();
}
private void assertRejected(OidcClientRegistrationAuthenticationContext context, String errorCode,
String fieldName) {
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
.extracting(OAuth2AuthenticationException::getError)
.satisfies((error) -> {
assertThat(error.getErrorCode()).isEqualTo(errorCode);
assertThat(error.getDescription()).contains(fieldName);
});
}
}
@@ -363,7 +363,7 @@ public class JwtGeneratorTests {
SessionInformation sessionInformation = tokenContext.get(SessionInformation.class);
assertThat(jwtClaimsSet.<String>getClaim("sid")).isEqualTo(sessionInformation.getSessionId());
assertThat(jwtClaimsSet.<Date>getClaim(IdTokenClaimNames.AUTH_TIME))
.isEqualTo(sessionInformation.getLastRequest());
.isEqualTo(JwtGenerator.getAuthenticationTime(tokenContext.getPrincipal()));
}
else if (tokenContext.getAuthorizationGrantType().equals(AuthorizationGrantType.REFRESH_TOKEN)) {
OidcIdToken currentIdToken = tokenContext.getAuthorization().getToken(OidcIdToken.class).getToken();
@@ -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());
@@ -266,6 +266,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;
}
}
@@ -23,6 +23,7 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames
/**
* @author Joe Grandja
*/
@SuppressWarnings("serial")
public class TestOidcAuthorizationRequest extends OAuth2AuthorizationRequest {
private final String nonce;
@@ -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;
@@ -232,7 +232,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));
}
/**
@@ -301,6 +302,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;
@@ -441,6 +444,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);
@@ -479,7 +488,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) {
@@ -332,7 +332,10 @@ 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();
}
@@ -350,6 +353,18 @@ public class NimbusJwtDecoderTests {
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;
@@ -258,6 +258,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
*/
@@ -113,9 +113,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;
}

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