1
0
mirror of synced 2026-07-13 06:40:03 +00:00

Compare commits

...

135 Commits

Author SHA1 Message Date
github-actions[bot] 8f69721f24 Next development version 2026-06-09 16:02:59 +00:00
github-actions[bot] 73b077790f Release 6.5.11 2026-06-09 15:25:02 +00:00
Joe Grandja fd5dae5e4d Sync branch '6.5.x' 2026-06-09 09:57:02 -04:00
dependabot[bot] 700a4533aa Bump ch.qos.logback:logback-classic from 1.5.32 to 1.5.34
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.32 to 1.5.34.
- [Release notes](https://github.com/qos-ch/logback/releases)
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.32...v_1.5.34)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 08:27:37 -04:00
Joe Grandja 3a394c633f Update micrometer-bom to 1.15.12
Closes gh-19302
2026-06-09 08:18:31 -04:00
Joe Grandja 79f9bec818 Update to reactor-bom 2024.0.18
Closes gh-19301
2026-06-09 08:18:30 -04:00
dependabot[bot] 5b019365b7 Bump org.hibernate.orm:hibernate-core from 6.6.51.Final to 6.6.53.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.51.Final to 6.6.53.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.53/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.51...6.6.53)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 07:37:50 -04:00
dependabot[bot] f9b4afdfc5 Bump com.fasterxml.jackson:jackson-bom from 2.18.7 to 2.18.8
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.7 to 2.18.8.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.7...jackson-bom-2.18.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 07:36:02 -04:00
dependabot[bot] f325ddbdaa Bump org.springframework:spring-framework-bom from 6.2.18 to 6.2.19
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.18 to 6.2.19.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.18...v6.2.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 07:32:45 -04:00
dependabot[bot] 4adfdf66b9 Bump org.apache.maven:maven-resolver-provider from 3.9.15 to 3.9.16
Bumps org.apache.maven:maven-resolver-provider from 3.9.15 to 3.9.16.

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 05:50:28 -04:00
dependabot[bot] e91b81a5aa Bump org-bouncycastle from 1.80 to 1.80.2
Bumps `org-bouncycastle` from 1.80 to 1.80.2.

Updates `org.bouncycastle:bcpkix-jdk18on` from 1.80 to 1.80.2
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

Updates `org.bouncycastle:bcprov-jdk18on` from 1.80 to 1.80.2
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

---
updated-dependencies:
- dependency-name: org.bouncycastle:bcpkix-jdk18on
  dependency-version: 1.80.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.bouncycastle:bcprov-jdk18on
  dependency-version: 1.80.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 05:49:36 -04:00
dependabot[bot] 0949d3b3ea Bump org.slf4j:slf4j-api from 2.0.17 to 2.0.18
Bumps org.slf4j:slf4j-api from 2.0.17 to 2.0.18.

---
updated-dependencies:
- dependency-name: org.slf4j:slf4j-api
  dependency-version: 2.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 05:33:00 -04:00
dependabot[bot] d44efc899d Bump org.hibernate.orm:hibernate-core from 6.6.50.Final to 6.6.51.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.50.Final to 6.6.51.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.51/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.50...6.6.51)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 05:29:40 -04:00
dependabot[bot] c5870a746a Bump @springio/antora-extensions from 1.14.11 to 1.14.12 in /docs
Bumps [@springio/antora-extensions](https://github.com/spring-io/antora-extensions) from 1.14.11 to 1.14.12.
- [Changelog](https://github.com/spring-io/antora-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/antora-extensions/compare/v1.14.11...v1.14.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 05:28:18 -04:00
dependabot[bot] 5eb41b8a06 Bump actions/checkout from 6.0.2 to 6.0.3
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 05:21:32 -04:00
Josh Cummings e3ad551ab3 Backport SubjectX500PrincipalExtractor
This commit backports SubjectX500PrincipalExtractor so as to provide
folks moving from 6.x to 7.x a migration path from
SubjectDnX509PrincipalExtractor.

Issue gh-16980

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-06-01 14:30:33 -06:00
Josh Cummings cf06871200 Rework Logout Validation Logic
This commit simplifies the code, favoring the construction
of a list of error codes over composing a set of consumers.
This will simplify future analysis by making the code more
readable.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-29 14:45:44 -06:00
Josh Cummings e50c2a6a74 Rework Login Validation Logic
This commit simplifies the validation code, favoring the
construction of a list of error codes over using an
accumulating Saml2ResponseValidationResult. This will
simplify future analysis by making the code more readable.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-29 14:45:36 -06:00
Josh Cummings 356131b1ea Use FormPostRedirectStrategy
Closes gh-16673

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-29 11:56:50 -06:00
Josh Cummings a14c9d66b1 Favor Relative URIs in CookieRequestCache
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-28 06:54:59 -04:00
Josh Cummings 9d4d9065b4 Cap Deflate Size
Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-28 06:39:29 -04:00
Josh Cummings fe2e52f646 Add Micrometer Version Override
Closes gh-19187

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-26 15:18:01 -06:00
Josh Cummings 0ea87ca3bf Upgrade Micrometer to 1.15.11
Closes gh-19224

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-26 15:17:57 -06:00
Josh Cummings 85514a9b34 Update NodeJS to 24.15
This commit declares a NodeJS value whereas previously,
Antora would use the latest LTS version. When it updated to
Node 24.16, site production stopped, possibly due to
https://github.com/nodejs/node/commit/e95570c054.

Either way, it is helpful to have a pinned version in order
to stabilize builds, allowing us to update it in an orderly
fashion like other dependencies.

Issue gh-19206

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-22 22:27:00 -06:00
dependabot[bot] 86e0f1341e Bump com.fasterxml.jackson:jackson-bom from 2.18.6 to 2.18.7
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.6 to 2.18.7.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.6...jackson-bom-2.18.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 12:38:00 -06:00
dependabot[bot] cea6618265 Bump org.hibernate.orm:hibernate-core from 6.6.49.Final to 6.6.50.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.49.Final to 6.6.50.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.50/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.49...6.6.50)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 12:36:57 -06:00
dependabot[bot] 5bf8f641f1 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-05-21 12:36:52 -06:00
dependabot[bot] 8b9f556d8f Bump gradle-wrapper from 8.14.4 to 8.14.5
Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 8.14.4 to 8.14.5.
- [Release notes](https://github.com/gradle/gradle/releases)
- [Commits](https://github.com/gradle/gradle/compare/v8.14.4...v8.14.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 12:36:46 -06:00
dependabot[bot] d9b8a8ec54 Bump spring-io/spring-release-actions from 0.0.4 to 0.0.5
Bumps [spring-io/spring-release-actions](https://github.com/spring-io/spring-release-actions) from 0.0.4 to 0.0.5.
- [Release notes](https://github.com/spring-io/spring-release-actions/releases)
- [Commits](https://github.com/spring-io/spring-release-actions/compare/2420148725bebe44bd59a575a9b1961ca4459b0b...a1f321783a0769dd2aea4fad6c2ae2f95a52b885)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 12:36:42 -06:00
dependabot[bot] 4e04c266be Bump antora from 3.2.0-alpha.11 to 3.2.0-alpha.12 in /docs
---
updated-dependencies:
- dependency-name: antora
  dependency-version: 3.2.0-alpha.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 12:36:37 -06:00
Josh Cummings 9b14465243 Align Assertions in Builder with Deprecated Constructor
The deprecated (introspectionUri, clientId, clientSecret) constructors
that the builders replaced explicitly asserted non-null clientId and
clientSecret. Bring the builder's build() in line with that contract by
asserting at the API boundary rather than relying on downstream classes
to enforce it.

Closes gh-19201

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-05-21 10:36:10 -06:00
Josh Cummings b075f0df02 Decode percent-encoded values
Closes gh-19136

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
2026-04-29 08:57:16 -06:00
github-actions[bot] 6343002b32 Next development version 2026-04-20 18:55:26 +00:00
github-actions[bot] 0a9d4dc8fc Release 6.5.10 2026-04-20 17:54:21 +00:00
Josh Cummings 3d4e20597a Merge remote-tracking branch 'oss/6.5.x' into 6.5.x 2026-04-20 11:49:17 -06:00
dependabot[bot] 81bd52ae48 Bump org.hibernate.orm:hibernate-core from 6.6.48.Final to 6.6.49.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.48.Final to 6.6.49.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.49/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.48...6.6.49)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-27 16:09:50 -06:00
dependabot[bot] 7441ce7f16 Bump spring-io/spring-security-release-tools/.github/workflows/perform-release.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/perform-release.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 13:25:46 -06:00
dependabot[bot] 9dbcd8cf00 Bump spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

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

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

Issue gh-18300

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

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

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

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

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

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

Fixes gh-16656

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 15:22:06 -06:00
dependabot[bot] 381047e386 Bump spring-io/spring-security-release-tools from 1.0.14 to 1.0.15
Bumps [spring-io/spring-security-release-tools](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 15:21:53 -06:00
dependabot[bot] 376b40a735 Bump io.spring.gradle:spring-security-release-plugin
Bumps [io.spring.gradle:spring-security-release-plugin](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.14...v1.0.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:58:20 -06:00
dependabot[bot] 89fa1cbdd2 Bump spring-io/spring-security-release-tools/.github/workflows/build.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/build.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:56:46 -06:00
dependabot[bot] 01758c4c59 Bump spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:56:10 -06:00
dependabot[bot] f37833a59c Bump spring-io/spring-security-release-tools/.github/workflows/test.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/test.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 14:55:52 -06:00
dependabot[bot] 52e6c4c4be Bump spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml
Bumps [spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml](https://github.com/spring-io/spring-security-release-tools) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Changelog](https://github.com/spring-io/spring-security-release-tools/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/729fed56d42122f88583aff1be35c0800b7d77e9...b92832ecbc7cbe969201e6beafbde0ee400cf095)

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

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

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

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

Closes gh-18643

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

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

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

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

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

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

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

This commit ensures the parameters are restored.

Closes gh-18204

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes gh-16708

Signed-off-by: [CLOUD4] 한현 <gusgus1467@naver.com>
2026-02-26 16:56:22 -07:00
Guillaume Husta 68a02ff176 Update Link to CRSF Docs in FAQ
Signed-off-by: Guillaume Husta <guillaume.husta@gmail.com>
2026-02-26 14:47:21 -07:00
dependabot[bot] f37a706d62 Bump org-apache-maven-resolver from 1.9.26 to 1.9.27
Bumps `org-apache-maven-resolver` from 1.9.26 to 1.9.27.

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 14:38:30 -07:00
Rob Winch 522c48b3b5 Merge Add Missing OnCommitedResponseWrapper Header Overrides
Add Missing OnCommitedResponseWrapper Header Overrides
2026-02-24 20:16:24 -06:00
Robert Winch 1dae9aa459 Add Missing OnCommitedResponseWrapper Header Overrides
Spring Security's `OnCommitedResponseWrapper` does not override the `setHeader`, `setIntHeader`, `addIntHeader`
methods. This means that if the `Content-Length` response header is specified using any of those methods then
the response body length is not tracked and can be committed before the response headers are written.

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

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

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

Issue gh-16530

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

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

Signed-off-by: onhann <gusgus1467@naver.com>
2026-02-24 14:24:11 -07:00
Rob Winch d29c984881 Merge pull request #18544 from Khyojae/gh-18543
Fix GrantedAuthority.authority null in AuthoritiesAuthorizationManager
2026-02-23 11:16:42 -06:00
Robert Winch 1116241ee3 Fix Checks for NullPointerException in AuthoritiesAuthorizationManager
- Fix checkstyle
- Fix the test to use Collection that throws NullPointerException on .contains(null) to replicate the reported issue

Closes gh-18544

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

Signed-off-by: Khyojae <khjae201@gmail.com>
2026-02-23 09:30:28 -06:00
dependabot[bot] ac556a45f9 Bump org.hibernate.orm:hibernate-core from 6.6.42.Final to 6.6.43.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.6.42.Final to 6.6.43.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.6.43/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.6.42...6.6.43)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 08:12:09 -06:00
Robert Winch 3e3eeda560 Bump ch.qos.logback:logback-classic from 1.5.29 to 1.5.32 2026-02-19 13:28:49 -06:00
dependabot[bot] 58df50c3a3 Bump org-apache-maven-resolver from 1.9.25 to 1.9.26
Bumps `org-apache-maven-resolver` from 1.9.25 to 1.9.26.

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 03:06:15 +00:00
github-actions[bot] 6c2b2a7611 Next development version 2026-02-13 18:24:26 +00:00
github-actions[bot] 0fab34f359 Release 6.5.8 2026-02-13 17:54:05 +00:00
dependabot[bot] 08e5b375ac Bump io.projectreactor:reactor-bom from 2024.0.14 to 2024.0.15
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.14 to 2024.0.15.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2024.0.14...2024.0.15)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 10:31:30 -07:00
151 changed files with 4306 additions and 813 deletions
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
actions: read
contents: read
security-events: write
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@e415dadd0910c901e7a7fabd67bbb355b2324500 # 1
@@ -17,7 +17,7 @@ permissions:
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
@@ -29,7 +29,7 @@ jobs:
secrets: inherit
test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
strategy:
matrix:
include:
@@ -39,16 +39,16 @@ jobs:
toolchain: 17
with:
java-version: ${{ matrix.java-version }}
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=6.2.+ -PreactorVersion=2023.0.+ -PspringDataVersion=2024.0.+ --stacktrace
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=6.2.+ -PreactorVersion=2023.0.+ -PspringDataVersion=2024.0.+ -PmicrometerVersion=1.15.+ --stacktrace
secrets: inherit
check-samples:
name: Check Samples
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'spring-projects' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
with:
java-version: 17
distribution: temurin
@@ -67,21 +67,21 @@ jobs:
deploy-artifacts:
name: Deploy Artifacts
needs: [ build, test, check-samples ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-schema:
name: Deploy Schema
needs: [ build, test, check-samples ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
perform-release:
name: Perform Release
needs: [ deploy-artifacts, deploy-schema ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
@@ -97,6 +97,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+76
View File
@@ -0,0 +1,76 @@
name: Defer Issues
on:
workflow_dispatch:
permissions:
contents: read
jobs:
defer-issues:
name: Defer Issues
runs-on: ubuntu-latest
if: github.repository_owner == 'spring-projects'
permissions:
issues: write
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Compute Version
id: compute-version
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
- name: Get Today's Release Version
id: todays-release
uses: spring-io/spring-release-actions/get-todays-release-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
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@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
with:
version: ${{ steps.todays-release.outputs.release-version }}
- name: Schedule Next Milestone
uses: spring-io/spring-release-actions/schedule-milestone@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
with:
version: ${{ steps.next-version.outputs.version }}
version-date: ${{ steps.next-version.outputs.version-date }}
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Move Open Issues to Next Milestone
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT_MILESTONE: ${{ steps.todays-release.outputs.release-version }}
NEXT_MILESTONE: ${{ steps.next-version.outputs.version }}
run: |
current_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
--jq ".[] | select(.title == \"$CURRENT_MILESTONE\") | .number")
if [ -z "$current_milestone_number" ]; then
echo "No milestone found for $CURRENT_MILESTONE"
exit 0
fi
next_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
--jq ".[] | select(.title == \"$NEXT_MILESTONE\") | .number")
if [ -z "$next_milestone_number" ]; then
echo "No milestone found for $NEXT_MILESTONE"
exit 1
fi
echo "Moving open issues from milestone '$CURRENT_MILESTONE' (#$current_milestone_number) to '$NEXT_MILESTONE' (#$next_milestone_number)"
page=1
while true; do
issues=$(gh api "repos/${{ github.repository }}/issues?milestone=$current_milestone_number&state=open&per_page=100&page=$page" \
--jq '.[].number')
if [ -z "$issues" ]; then
break
fi
for issue in $issues; do
echo "Moving issue/PR #$issue to milestone $NEXT_MILESTONE"
gh api repos/${{ github.repository }}/issues/$issue \
--method PATCH \
--field milestone=$next_milestone_number \
--silent
done
page=$((page + 1))
done
echo "Done."
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
if: github.repository_owner == 'spring-projects'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: docs-build
fetch-depth: 1
@@ -19,7 +19,7 @@ jobs:
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up JDK 17
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
@@ -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 }}
+6 -6
View File
@@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- 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'
@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docs
path: docs/build/site
@@ -46,6 +46,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 1
- name: Dispatch
@@ -18,7 +18,7 @@ jobs:
matrix:
branch: [ '5.8.x', '6.2.x', '6.3.x', 'main' ]
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
name: Update
with:
docs-branch: ${{ matrix.branch }}
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
name: Update on docs-build
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
name: Update
with:
docs-branch: 'docs-build'
@@ -9,7 +9,7 @@ permissions:
jobs:
update-scheduled-release-version:
name: Update Scheduled Release Version
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
secrets: inherit
send-notification:
name: Send Notification
@@ -18,6 +18,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
+21
View File
@@ -68,6 +68,27 @@ The https://github.com/spring-projects/spring-security/tree/docs-build[playbook
Discover more commands with `./gradlew tasks`.
=== IDE setup (IntelliJ)
No special steps are needed to open Spring Security in IntelliJ.
=== IDE setup (Eclipse and VS Code)
To work in Eclipse or VS Code, first generate Eclipse metadata so you can import the project into Eclipse or VS Code:
[indent=0]
----
./gradlew cleanEclipse eclipse
----
If you have not built the project yet, run `./gradlew publishToMavenLocal` first so dependencies are resolved.
*VS Code:* Open the repository root as a folder. The repository includes `.vscode/settings.json` which disables automatic Gradle import so that the generated Eclipse metadata (`.classpath`, `.project`) is used. Do not use the Gradle for Java extension to import the project.
*Eclipse:* File → Import → General → Existing Projects into Workspace, then select the repository root.
The build uses a custom Eclipse plugin to work around Gradle dependency cycles that confuse IDE metadata generation. You may see Eclipse warnings about `xml-apis` from some test dependencies; those are excluded in the build and can be ignored.
== Getting Support
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
https://spring.io/support[Commercial support] is available too.
@@ -36,6 +36,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
import org.springframework.security.web.webauthn.management.CredentialRecordOwnerAuthorizationManager;
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
@@ -166,6 +167,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
rpOperations);
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
rpOperations);
if (creationOptionsRepository != null) {
@@ -74,6 +74,7 @@ import org.springframework.security.web.context.RequestAttributeSecurityContextR
*
* @author Rob Winch
* @author Ngoc Nhan
* @author Max Batischev
* @since 3.2
*/
public final class X509Configurer<H extends HttpSecurityBuilder<H>>
@@ -161,7 +162,10 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
* @param subjectPrincipalRegex the regex to extract the user principal from the
* certificate (i.e. "CN=(.*?)(?:,|$)").
* @return the {@link X509Configurer} for further customizations
* @deprecated Please use {@link #x509PrincipalExtractor(X509PrincipalExtractor)}
* instead
*/
@Deprecated
public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);
@@ -521,11 +521,23 @@ final class AuthenticationConfigBuilder {
filterBuilder.addPropertyValue("authenticationManager", authManager);
filterBuilder.addPropertyValue("securityContextHolderStrategy",
authenticationFilterSecurityContextHolderStrategyRef);
String regex = x509Elt.getAttribute("subject-principal-regex");
if (StringUtils.hasText(regex)) {
String principalExtractorRef = x509Elt.getAttribute("principal-extractor-ref");
String subjectPrincipalRegex = x509Elt.getAttribute("subject-principal-regex");
boolean hasPrincipalExtractorRef = StringUtils.hasText(principalExtractorRef);
boolean hasSubjectPrincipalRegex = StringUtils.hasText(subjectPrincipalRegex);
if (hasPrincipalExtractorRef && hasSubjectPrincipalRegex) {
this.pc.getReaderContext()
.error("The attribute 'principal-extractor-ref' cannot be used together with the 'subject-principal-regex' attribute within <"
+ Elements.X509 + ">", this.pc.extractSource(x509Elt));
}
if (hasPrincipalExtractorRef) {
RuntimeBeanReference principalExtractor = new RuntimeBeanReference(principalExtractorRef);
filterBuilder.addPropertyValue("principalExtractor", principalExtractor);
}
if (hasSubjectPrincipalRegex) {
BeanDefinitionBuilder extractor = BeanDefinitionBuilder
.rootBeanDefinition(SubjectDnX509PrincipalExtractor.class);
extractor.addPropertyValue("subjectDnRegex", regex);
extractor.addPropertyValue("subjectDnRegex", subjectPrincipalRegex);
filterBuilder.addPropertyValue("principalExtractor", extractor.getBeanDefinition());
}
injectAuthenticationDetailsSource(x509Elt, filterBuilder);
@@ -119,7 +119,7 @@ import org.springframework.security.oauth2.server.resource.web.server.BearerToke
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.PortMapper;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
@@ -943,8 +943,8 @@ public class ServerHttpSecurity {
* }
* </pre>
*
* Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor}
* will be used. If authenticationManager is not specified,
* Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will
* be used. If authenticationManager is not specified,
* {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
* @return the {@link X509Spec} to customize
* @since 5.2
@@ -978,8 +978,8 @@ public class ServerHttpSecurity {
* }
* </pre>
*
* Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor}
* will be used. If authenticationManager is not specified,
* Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will
* be used. If authenticationManager is not specified,
* {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
* @param x509Customizer the {@link Customizer} to provide more options for the
* {@link X509Spec}
@@ -4180,7 +4180,7 @@ public class ServerHttpSecurity {
if (this.principalExtractor != null) {
return this.principalExtractor;
}
return new SubjectDnX509PrincipalExtractor();
return new SubjectX500PrincipalExtractor();
}
private ReactiveAuthenticationManager getAuthenticationManager() {
@@ -1050,6 +1050,9 @@ x509.attlist &=
x509.attlist &=
## Reference to an AuthenticationDetailsSource which will be used by the authentication filter
attribute authentication-details-source-ref {xsd:token}?
x509.attlist &=
## Reference to an X509PrincipalExtractor which will be used by the authentication filter
attribute principal-extractor-ref {xsd:token}?
jee =
## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication.
@@ -2911,6 +2911,12 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="principal-extractor-ref" type="xs:token">
<xs:annotation>
<xs:documentation>Reference to an X509PrincipalExtractor which will be used by the authentication filter
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="jee">
<xs:annotation>
@@ -211,6 +211,7 @@ import org.springframework.security.web.webauthn.api.AuthenticationExtensionsCli
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
@@ -225,6 +226,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestO
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses;
import org.springframework.security.web.webauthn.api.TestBytes;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
@@ -654,6 +656,8 @@ final class SerializationSamples {
generatorByClassName.put(CredentialPropertiesOutput.class, (o) -> credentialOutput);
generatorByClassName.put(ImmutableAuthenticationExtensionsClientOutputs.class, (o) -> outputs);
generatorByClassName.put(AuthenticatorAssertionResponse.class, (r) -> response);
generatorByClassName.put(AuthenticatorAttestationResponse.class,
(r) -> TestAuthenticatorAttestationResponses.createAuthenticatorAttestationResponse().build());
generatorByClassName.put(RelyingPartyAuthenticationRequest.class, (r) -> authRequest);
generatorByClassName.put(PublicKeyCredential.class, (r) -> credential);
generatorByClassName.put(WebAuthnAuthenticationRequestToken.class, (r) -> requestToken);
@@ -33,10 +33,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.commons.lang3.ObjectUtils;
@@ -207,10 +207,7 @@ class SpringSecurityCoreVersionSerializableTests {
boolean hasSerialVersion = Stream.of(clazz.getDeclaredFields())
.map(Field::getName)
.anyMatch((n) -> n.equals("serialVersionUID"));
SuppressWarnings suppressWarnings = clazz.getAnnotation(SuppressWarnings.class);
boolean hasSerialIgnore = suppressWarnings == null
|| Arrays.asList(suppressWarnings.value()).contains("Serial");
if (!hasSerialVersion && !hasSerialIgnore) {
if (!hasSerialVersion && !hasSuppressSerialInSource(clazz)) {
classes.add(clazz);
continue;
}
@@ -249,6 +246,58 @@ class SpringSecurityCoreVersionSerializableTests {
return classes.stream();
}
private static boolean hasSuppressSerialInSource(Class<?> clazz) {
try {
Class<?> fileClass = clazz;
while (fileClass.getEnclosingClass() != null) {
fileClass = fileClass.getEnclosingClass();
}
var codeSource = fileClass.getProtectionDomain().getCodeSource();
if (codeSource == null) {
return false;
}
Path sourceFile = findSourceFile(Path.of(codeSource.getLocation().toURI()), fileClass);
if (sourceFile == null) {
return false;
}
return hasSuppressSerialAnnotation(Files.readAllLines(sourceFile), clazz.getSimpleName());
}
catch (Exception ex) {
return false;
}
}
private static Path findSourceFile(Path start, Class<?> clazz) {
String relativePath = clazz.getName().replace('.', '/') + ".java";
Path dir = start;
for (int i = 0; i < 10 && dir != null; i++) {
for (String sourceRoot : List.of("src/main/java", "src/test/java")) {
Path candidate = dir.resolve(sourceRoot).resolve(relativePath);
if (Files.exists(candidate)) {
return candidate;
}
}
dir = dir.getParent();
}
return null;
}
private static boolean hasSuppressSerialAnnotation(List<String> lines, String simpleClassName) {
Pattern classDeclaration = Pattern
.compile("\\b(?:class|interface|enum|record)\\s+" + Pattern.quote(simpleClassName) + "\\b");
for (int i = 0; i < lines.size(); i++) {
if (classDeclaration.matcher(lines.get(i)).find()) {
for (int j = Math.max(0, i - 5); j < i; j++) {
String line = lines.get(j);
if (line.contains("@SuppressWarnings") && line.contains("\"serial\"")) {
return true;
}
}
}
}
return false;
}
private static String getCurrentVersion() {
String version = System.getProperty("springSecurityVersion");
String[] parts = version.split("\\.");
@@ -42,8 +42,15 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
import org.springframework.test.web.servlet.MockMvc;
@@ -56,6 +63,7 @@ import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@@ -247,6 +255,24 @@ public class WebAuthnConfigurerTests {
.andExpect(content().string(expectedBody));
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToUserThenNoContent() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"))))
.andExpect(status().isNoContent());
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("other-user", "password", "ROLE_USER"))))
.andExpect(status().isForbidden());
}
@Configuration
@EnableWebSecurity
static class ConfigCredentialCreationOptionsRepository {
@@ -417,4 +443,47 @@ public class WebAuthnConfigurerTests {
}
@Configuration
@EnableWebSecurity
static class DeleteCredentialConfiguration {
static final String CREDENTIAL_ID_BASE64URL = "NauGCN7bZ5jEBwThcde51g";
static final Bytes USER_ENTITY_ID = Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM");
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager();
}
@Bean
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
return mock(WebAuthnRelyingPartyOperations.class);
}
@Bean
UserCredentialRepository userCredentialRepository() {
MapUserCredentialRepository repository = new MapUserCredentialRepository();
repository.save(TestCredentialRecords.userCredential().build());
return repository;
}
@Bean
PublicKeyCredentialUserEntityRepository userEntityRepository() {
MapPublicKeyCredentialUserEntityRepository repository = new MapPublicKeyCredentialUserEntityRepository();
repository.save(ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(USER_ENTITY_ID)
.displayName("User")
.build());
return repository;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
}
}
}
@@ -43,7 +43,9 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
@@ -123,6 +125,28 @@ public class X509ConfigurerTests {
// @formatter:on
}
@Test
public void x509WhenSubjectX500PrincipalExtractor() throws Exception {
this.spring.register(SubjectX500PrincipalExtractorConfig.class).autowire();
X509Certificate certificate = loadCert("rod.cer");
// @formatter:off
this.mvc.perform(get("/").with(x509(certificate)))
.andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull())
.andExpect(authenticated().withUsername("rod"));
// @formatter:on
}
@Test
public void x509WhenSubjectX500PrincipalExtractorBean() throws Exception {
this.spring.register(SubjectX500PrincipalExtractorEmailConfig.class).autowire();
X509Certificate certificate = X509TestUtils.buildTestCertificate();
// @formatter:off
this.mvc.perform(get("/").with(x509(certificate)))
.andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull())
.andExpect(authenticated().withUsername("luke@monkeymachine"));
// @formatter:on
}
@Test
public void x509WhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception {
this.spring.register(UserDetailsServiceBeanConfig.class).autowire();
@@ -277,6 +301,62 @@ public class X509ConfigurerTests {
}
@Configuration
@EnableWebSecurity
static class SubjectX500PrincipalExtractorConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.x509((x509) -> x509
.x509PrincipalExtractor(new SubjectX500PrincipalExtractor())
);
// @formatter:on
return http.build();
}
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("rod")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
@Configuration
@EnableWebSecurity
static class SubjectX500PrincipalExtractorEmailConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
principalExtractor.setExtractPrincipalNameFromEmail(true);
// @formatter:off
http
.x509((x509) -> x509
.x509PrincipalExtractor(principalExtractor)
);
// @formatter:on
return http.build();
}
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("luke@monkeymachine")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
@Configuration
@EnableWebSecurity
static class UserDetailsServiceBeanConfig {
@@ -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
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.security.AccessController;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@@ -91,6 +92,7 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
@@ -398,6 +400,27 @@ public class MiscHttpConfigTests {
.containsSubsequence(CsrfFilter.class, X509AuthenticationFilter.class, ExceptionTranslationFilter.class);
}
@Test
public void getWhenUsingX509PrincipalExtractorRef() throws Exception {
this.spring.configLocations(xml("X509PrincipalExtractorRef")).autowire();
X509Certificate certificate = X509TestUtils.buildTestCertificate();
RequestPostProcessor x509 = x509(certificate);
// @formatter:off
this.mvc.perform(get("/protected").with(x509))
.andExpect(status().isOk());
// @formatter:on
}
@Test
public void getWhenUsingX509PrincipalExtractorRefAndSubjectPrincipalRegex() throws Exception {
String xmlResourceName = "X509PrincipalExtractorRefAndSubjectPrincipalRegex";
// @formatter:off
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(xml(xmlResourceName)).autowire())
.withMessage("Configuration problem: The attribute 'principal-extractor-ref' cannot be used together with the 'subject-principal-regex' attribute within <x509>\n" + "Offending resource: class path resource [org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRefAndSubjectPrincipalRegex.xml]");
// @formatter:on
}
@Test
public void getWhenUsingX509AndPropertyPlaceholderThenSubjectPrincipalRegexIsConfigured() throws Exception {
System.setProperty("subject_principal_regex", "OU=(.*?)(?:,|$)");
@@ -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,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 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:p="http://www.springframework.org/schema/p"
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>
<x509 principal-extractor-ref="principalExtractor"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
<user-service id="us">
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
p:extractPrincipalNameFromEmail="true"/>
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
</b:beans>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 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:p="http://www.springframework.org/schema/p"
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>
<x509 principal-extractor-ref="principalExtractor" subject-principal-regex="(.*)"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
<user-service id="us">
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
p:extractPrincipalNameFromEmail="true"/>
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
</b:beans>
@@ -92,6 +92,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
private boolean alwaysPerformAdditionalChecksOnUser = true;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
/**
@@ -146,8 +148,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
@@ -157,8 +158,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
@@ -171,6 +171,25 @@ public abstract class AbstractUserDetailsAuthenticationProvider
return createSuccessAuthentication(principalToReturn, authentication, user);
}
private void performPreCheck(UserDetails user, UsernamePasswordAuthenticationToken authentication) {
try {
this.preAuthenticationChecks.check(user);
}
catch (AuthenticationException ex) {
if (!this.alwaysPerformAdditionalChecksOnUser) {
throw ex;
}
try {
additionalAuthenticationChecks(user, authentication);
}
catch (AuthenticationException ignored) {
// preserve the original failed check
}
throw ex;
}
additionalAuthenticationChecks(user, authentication);
}
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}
@@ -313,6 +332,22 @@ public abstract class AbstractUserDetailsAuthenticationProvider
this.postAuthenticationChecks = postAuthenticationChecks;
}
/**
* Set whether to always perform the additional checks on the user, even if the
* pre-authentication checks fail. This is useful to ensure that regardless of the
* state of the user account, authentication takes the same amount of time to
* complete.
*
* <p>
* For applications that rely on the additional checks running only once should set
* this value to {@code false}
* @param alwaysPerformAdditionalChecksOnUser
* @since 5.7.23
*/
public void setAlwaysPerformAdditionalChecksOnUser(boolean alwaysPerformAdditionalChecksOnUser) {
this.alwaysPerformAdditionalChecksOnUser = alwaysPerformAdditionalChecksOnUser;
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
@@ -152,7 +152,9 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
return null;
}
OneTimeToken token = tokens.get(0);
deleteOneTimeToken(token);
if (deleteOneTimeToken(token) == 0) {
return null;
}
if (isExpired(token)) {
return null;
}
@@ -170,11 +172,11 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper);
}
private void deleteOneTimeToken(OneTimeToken oneTimeToken) {
private int deleteOneTimeToken(OneTimeToken oneTimeToken) {
List<SqlParameterValue> parameters = List
.of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue()));
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
return this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
}
private ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
@@ -67,7 +67,11 @@ public final class AuthoritiesAuthorizationManager implements AuthorizationManag
private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
if (authorities.contains(grantedAuthority.getAuthority())) {
String authority = grantedAuthority.getAuthority();
if (authority == null) {
continue;
}
if (authorities.contains(authority)) {
return true;
}
}
@@ -28,6 +28,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
* instead
*/
@Deprecated
@SuppressWarnings("serial")
public class ExpressionAttributeAuthorizationDecision extends AuthorizationDecision {
private final ExpressionAttribute expressionAttribute;
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.springframework.cache.Cache;
import org.springframework.dao.DataRetrievalFailureException;
@@ -42,6 +43,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -62,6 +64,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -452,12 +455,10 @@ public class DaoAuthenticationProviderTests {
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
}
/**
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
* SEC-2056 is fixed.
*/
public void IGNOREtestSec2056() {
// SEC-2056
@Test
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
public void testSec2056() {
UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod",
"koala");
UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken
@@ -491,6 +492,41 @@ public class DaoAuthenticationProviderTests {
.isTrue();
}
// related to SEC-2056
@Test
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
public void testDisabledUserTiming() {
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
PasswordEncoder encoder = new BCryptPasswordEncoder();
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(encoder);
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
users.password = encoder.encode((CharSequence) user.getCredentials());
provider.setUserDetailsService(users);
int sampleSize = 100;
List<Long> enabledTimes = new ArrayList<>(sampleSize);
for (int i = 0; i < sampleSize; i++) {
long start = System.currentTimeMillis();
provider.authenticate(user);
enabledTimes.add(System.currentTimeMillis() - start);
}
UserDetailsChecker preChecks = mock(UserDetailsChecker.class);
willThrow(new DisabledException("User is disabled")).given(preChecks).check(any(UserDetails.class));
provider.setPreAuthenticationChecks(preChecks);
List<Long> disabledTimes = new ArrayList<>(sampleSize);
for (int i = 0; i < sampleSize; i++) {
long start = System.currentTimeMillis();
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> provider.authenticate(user));
disabledTimes.add(System.currentTimeMillis() - start);
}
double enabledAvg = avg(enabledTimes);
double disabledAvg = avg(disabledTimes);
assertThat(Math.abs(disabledAvg - enabledAvg) <= 3)
.withFailMessage("Disabled user average " + disabledAvg + " should be within 3ms of enabled user average "
+ enabledAvg)
.isTrue();
}
private double avg(List<Long> counts) {
return counts.stream().mapToLong(Long::longValue).average().orElse(0);
}
@@ -21,19 +21,24 @@ import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@@ -145,6 +150,27 @@ class JdbcOneTimeTokenServiceTests {
assertThat(consumedOneTimeToken).isNull();
}
@Test
void consumeWhenTokenIsDeletedConcurrentlyThenReturnNull() throws Exception {
// Simulates a concurrent consume: SELECT finds the token but DELETE affects
// 0 rows because another caller already consumed it.
JdbcOperations jdbcOperations = mock(JdbcOperations.class);
Instant notExpired = Instant.now().plus(5, ChronoUnit.MINUTES);
OneTimeToken token = new DefaultOneTimeToken(TOKEN_VALUE, USERNAME, notExpired);
given(jdbcOperations.query(any(String.class), any(PreparedStatementSetter.class),
ArgumentMatchers.<RowMapper<OneTimeToken>>any()))
.willReturn(List.of(token));
given(jdbcOperations.update(any(String.class), any(PreparedStatementSetter.class))).willReturn(0);
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcOperations);
try {
OneTimeToken consumed = service.consume(new OneTimeTokenAuthenticationToken(TOKEN_VALUE));
assertThat(consumed).isNull();
}
finally {
service.destroy();
}
}
@Test
void consumeWhenTokenIsExpiredThenReturnNull() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
@@ -17,7 +17,9 @@
package org.springframework.security.authorization;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
@@ -30,11 +32,13 @@ import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
/**
* Tests for {@link AuthoritiesAuthorizationManager}.
*
* @author Evgeniy Cheban
* @author Khyojae
*/
class AuthoritiesAuthorizationManagerTests {
@@ -84,4 +88,20 @@ class AuthoritiesAuthorizationManagerTests {
assertThat(manager.check(authentication, Collections.singleton("ROLE_USER")).isGranted()).isTrue();
}
@Test
// gh-18543
void authorizeWhenAuthorityIsNullThenDoesNotThrowNullPointerException() {
AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager();
Authentication authentication = new TestingAuthenticationToken("user", "password",
Collections.singletonList(() -> null));
Collection<String> authoritiesContainsThrowsNPE = Set.of("ROLE_USER");
// must be Collection that throws NPE when .contains(null) is invoked
// to replicate the issue in gh-18543
assertThatNullPointerException().isThrownBy(() -> authoritiesContainsThrowsNPE.contains(null));
assertThat(manager.authorize(() -> authentication, authoritiesContainsThrowsNPE).isGranted()).isFalse();
}
}
+6
View File
@@ -1,3 +1,7 @@
plugins {
id 'java-test-fixtures'
}
apply plugin: 'io.spring.convention.spring-module'
dependencies {
@@ -6,6 +10,8 @@ dependencies {
optional 'org.springframework:spring-core'
optional 'org.bouncycastle:bcpkix-jdk18on'
testFixturesImplementation "org.assertj:assertj-core"
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.junit.jupiter:junit-jupiter-params"
@@ -0,0 +1,47 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.crypto.assertions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link CryptoAssertions} and {@link CryptoStringAssert}.
*/
class CryptoAssertionsTests {
@Test
void doesNotDecryptToPassesWhenSupplierThrows() {
CryptoAssertions.assertThat((() -> {
throw new RuntimeException("decrypt failed");
})).doesNotDecryptTo("any");
}
@Test
void doesNotDecryptToPassesWhenResultDiffersFromExpected() {
CryptoAssertions.assertThat(() -> "other").doesNotDecryptTo("plaintext");
}
@Test
void doesNotDecryptToFailsWhenResultEqualsExpected() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> CryptoAssertions.assertThat(() -> "plaintext").doesNotDecryptTo("plaintext"))
.withMessageContaining("Expected supplier not to return <plaintext> but it did");
}
}
@@ -22,8 +22,9 @@ import java.security.interfaces.RSAPublicKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.assertions.CryptoAssertions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* @author Dave Syer
@@ -86,13 +87,15 @@ public class RsaSecretEncryptorTests {
@Test
public void roundTripWithMixedAlgorithm() {
RsaSecretEncryptor oaep = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
assertThatIllegalStateException().isThrownBy(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")));
CryptoAssertions.assertThat(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")))
.doesNotDecryptTo("encryptor");
}
@Test
public void roundTripWithMixedSalt() {
RsaSecretEncryptor other = new RsaSecretEncryptor(this.encryptor.getPublicKey(), RsaAlgorithm.DEFAULT, "salt");
assertThatIllegalStateException().isThrownBy(() -> this.encryptor.decrypt(other.encrypt("encryptor")));
CryptoAssertions.assertThat(() -> this.encryptor.decrypt(other.encrypt("encryptor")))
.doesNotDecryptTo("encryptor");
}
@Test
@@ -106,7 +109,8 @@ public class RsaSecretEncryptorTests {
public void publicKeyCannotDecrypt() {
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
assertThat(encryptor.canDecrypt()).as("Encryptor schould not be able to decrypt").isFalse();
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encryptor.encrypt("encryptor")));
CryptoAssertions.assertThat(() -> encryptor.decrypt(encryptor.encrypt("encryptor")))
.doesNotDecryptTo("encryptor");
}
@Test
@@ -0,0 +1,44 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.crypto.assertions;
import java.util.function.Supplier;
/**
* AssertJ entry point for crypto-related assertions. Use {@link #assertThat(Supplier)} to
* assert on a {@link Supplier}&lt;{@link String}&gt; (e.g. a decryption lambda).
* <p>
* Example: <pre>
* assertThat(() -&gt; encryptor.decrypt(ciphertext)).doesNotDecryptTo("plaintext");
* </pre>
*/
public final class CryptoAssertions {
private CryptoAssertions() {
}
/**
* Create assertions for the given supplier (e.g. a decryption expression).
* @param actual the supplier to assert on
* @return assertion object with methods like
* {@link CryptoStringAssert#doesNotDecryptTo(String)}
*/
public static CryptoStringAssert assertThat(Supplier<String> actual) {
return new CryptoStringAssert(actual);
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.crypto.assertions;
import java.util.Objects;
import java.util.function.Supplier;
import org.assertj.core.api.AbstractObjectAssert;
/**
* AssertJ assertion for {@link Supplier}&lt;{@link String}&gt;, supporting
* decryption-related checks such as {@link #doesNotDecryptTo(String)}.
*/
public final class CryptoStringAssert extends AbstractObjectAssert<CryptoStringAssert, Supplier<String>> {
CryptoStringAssert(Supplier<String> actual) {
super(actual, CryptoStringAssert.class);
}
/**
* Asserts that either the supplier throws an exception when invoked, or the value it
* returns is not equal to the given string. Use this to assert that a decryption
* attempt does not yield a specific plaintext (e.g. wrong key or tampered
* ciphertext).
* @param expected the value that the supplier must not return
* @return this assertion for chaining
*/
public CryptoStringAssert doesNotDecryptTo(String expected) {
isNotNull();
try {
String result = this.actual.get();
if (Objects.equals(result, expected)) {
failWithMessage("Expected supplier not to return <%s> but it did", expected);
}
}
catch (Exception ex) {
// Exception thrown: supplier does not "decrypt to" the expected value
}
return this;
}
}
+3
View File
@@ -11,13 +11,16 @@ dependencies {
def springFrameworkVersion = project.property("springFrameworkVersion")
def reactorVersion = project.property("reactorVersion")
def springDataVersion = project.property("springDataVersion")
def micrometerVersion = project.property("micrometerVersion")
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
api platform("io.projectreactor:reactor-bom:$reactorVersion")
api platform("org.springframework.data:spring-data-bom:$springDataVersion")
api platform("io.micrometer:micrometer-bom:$micrometerVersion")
} else {
api platform(libs.org.springframework.spring.framework.bom)
api platform(libs.io.projectreactor.reactor.bom)
api platform(libs.org.springframework.data.spring.data.bom)
api platform(libs.io.micrometer.micrometer.bom)
}
api platform(libs.io.rsocket.rsocket.bom)
api platform(libs.org.junit.junit.bom)
+1 -1
View File
@@ -31,7 +31,7 @@ urls:
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.26/ui-bundle.zip
snapshot: true
runtime:
log:
+1
View File
@@ -18,3 +18,4 @@ asciidoc:
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
include-java: 'example$docs-src/test/java/org/springframework/security/docs'
include-kotlin: 'example$docs-src/test/kotlin/org/springframework/security/kt/docs'
include-xml: 'example$docs-src/test/resources/org/springframework/security/docs'
@@ -5,98 +5,16 @@ Similar to xref:servlet/authentication/x509.adoc#servlet-x509[Servlet X.509 auth
The following example shows a reactive x509 security configuration:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.x509(withDefaults())
.authorizeExchange(exchanges -> exchanges
.anyExchange().permitAll()
);
return http.build();
}
----
include-code::./DefaultX509Configuration[tag=springSecurity,indent=0]
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
x509 { }
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
----
======
In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used. The default principal extractor is `SubjectDnX509PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client. The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired.
In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used.
The default principal extractor is `SubjectX500PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client.
The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired.
The following example demonstrates how these defaults can be overridden:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
SubjectDnX509PrincipalExtractor principalExtractor =
new SubjectDnX509PrincipalExtractor();
include-code::./CustomX509Configuration[tag=springSecurity,indent=0]
principalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)");
ReactiveAuthenticationManager authenticationManager = authentication -> {
authentication.setAuthenticated("Trusted Org Unit".equals(authentication.getName()));
return Mono.just(authentication);
};
http
.x509(x509 -> x509
.principalExtractor(principalExtractor)
.authenticationManager(authenticationManager)
)
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
val customPrincipalExtractor = SubjectDnX509PrincipalExtractor()
customPrincipalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)")
val customAuthenticationManager = ReactiveAuthenticationManager { authentication: Authentication ->
authentication.isAuthenticated = "Trusted Org Unit" == authentication.name
Mono.just(authentication)
}
return http {
x509 {
principalExtractor = customPrincipalExtractor
authenticationManager = customAuthenticationManager
}
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
----
======
In the previous example, a username is extracted from the OU field of a client certificate instead of CN, and account lookup using `ReactiveUserDetailsService` is not performed at all. Instead, if the provided certificate issued to an OU named "`Trusted Org Unit`", a request is authenticated.
In the previous example, a username is extracted from the `emailAddress` field of a client certificate instead of CN, and account lookup uses a custom `ReactiveAuthenticationManager` instance.
For an example of configuring Netty and `WebClient` or `curl` command-line tool to use mutual TLS and enable X.509 authentication, see https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509.
@@ -39,17 +39,18 @@ This endpoint is referred to as a https://openid.net/specs/openid-connect-discov
When this property and these dependencies are used, Resource Server automatically configures itself to validate JWT-encoded Bearer Tokens.
It achieves this through a deterministic startup process:
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property.
. Configure the validation strategy to query `jwks_url` for valid public keys.
. Configure the validation strategy to validate each JWT's `iss` claim against `https://idp.example.com`.
A consequence of this process is that the authorization server must be receiving requests in order for Resource Server to successfully start up.
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
[NOTE]
====
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup fails.
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder[`SupplierReactiveJwtDecoder`].
Consider wrapping any <<webflux-oauth2resourceserver-decoder-bean,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
====
=== Runtime Expectations
@@ -85,7 +86,7 @@ From here, consider jumping to:
[[webflux-oauth2resourceserver-jwt-jwkseturi]]
=== Specifying the Authorization Server JWK Set Uri Directly
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, you can supply `jwk-set-uri` as well:
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, you can supply `jwk-set-uri` as well:
[source,yaml]
----
@@ -375,7 +376,9 @@ Java::
----
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
return jwtDecoder;
}
----
@@ -385,7 +388,9 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
return jwtDecoder
}
----
======
@@ -451,8 +456,10 @@ Java::
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -462,8 +469,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -478,8 +487,10 @@ Java::
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -489,8 +500,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -505,11 +518,13 @@ Java::
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -519,12 +534,14 @@ Kotlin::
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}
.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -319,7 +319,7 @@ If you have trouble working out where a session is being created, you can add so
[[appendix-faq-forbidden-csrf]]
=== I get a 403 Forbidden when performing a POST. What is wrong?
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to https://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to xref:features/exploits/csrf.adoc#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
[[appendix-faq-no-security-on-forward]]
=== I am forwarding a request to another URL by using the RequestDispatcher, but my security constraints are not being applied.
@@ -2212,6 +2212,10 @@ A `PreAuthenticatedAuthenticationProvider` will also be created which delegates
* **authentication-details-source-ref**
A reference to an `AuthenticationDetailsSource`
[[nsa-x509-principal-extractor-ref]]
* **principal-extractor-ref**
Reference to an `X509PrincipalExtractor` which will be used by the authentication filter.
[[nsa-x509-subject-principal-regex]]
* **subject-principal-regex**
@@ -2223,7 +2227,6 @@ Defines a regular expression which will be used to extract the username from the
Allows a specific `UserDetailsService` to be used with X.509 in the case where multiple instances are configured.
If not set, an attempt will be made to locate a suitable instance automatically and use that.
[[nsa-filter-chain-map]]
== <filter-chain-map>
Used to explicitly configure a FilterChainProxy instance with a FilterChainMap
@@ -259,7 +259,7 @@ Java::
+
[source,java,role="primary"]
----
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directives.ALL));
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL));
http
.logout((logout) -> logout.addLogoutHandler(clearSiteData))
----
@@ -268,7 +268,7 @@ Kotlin::
+
[source,kotlin,role="secondary"]
----
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directives.ALL))
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directive.ALL))
http {
logout {
addLogoutHandler(clearSiteData)
@@ -3,7 +3,10 @@
Once you have got an application that is xref:servlet/authentication/index.adoc[authenticating requests], it is important to consider how that resulting authentication will be persisted and restored on future requests.
This is done automatically by default, so no additional code is necessary, though it is important to know what `requireExplicitSave` means in `HttpSecurity`.
This is done automatically by default.
If you have a custom filter or controller that is setting the security context, you will need to use a `SecurityContextRepository` to persist it across requests.
If you are upgrading from an older version, you may be interested in the `requireExplicitSave` setting that preserves Spring Security 5's default, though note that this is primarily for migration purposes.
If you like, <<how-it-works-requireexplicitsave,you can read more about what requireExplicitSave is doing>> or <<requireexplicitsave,why it's important>>. Otherwise, in most cases you are done with this section.
@@ -14,37 +14,27 @@ You should get this working before trying it out with Spring Security.
The Spring Security X.509 module extracts the certificate by using a filter.
It maps the certificate to an application user and loads that user's set of granted authorities for use with the standard Spring Security infrastructure.
[[servlet-x509-config]]
== Adding X.509 Authentication to Your Web Application
Enabling X.509 client authentication is very straightforward.
To do so, add the `<x509/>` element to your http security namespace configuration:
[source,xml]
----
<http>
...
<x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
</http>
----
Similar to xref:reactive/authentication/x509.adoc[Reactive X.509 authentication], the servlet x509 authentication filter allows extracting an authentication token from a certificate provided by a client.
The element has two optional attributes:
The following example shows a reactive x509 security configuration:
* `subject-principal-regex`.
The regular expression used to extract a username from the certificate's subject name.
The default value is shown in the preceding listing.
This is the username that is passed to the `UserDetailsService` to load the authorities for the user.
* `user-service-ref`.
This is the bean ID of the `UserDetailsService` to be used with X.509.
It is not needed if there is only one defined in your application context.
include-code::./DefaultX509Configuration[tag=springSecurity,indent=0]
In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used.
The default principal extractor is `SubjectX500PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client.
The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired.
The following example demonstrates how these defaults can be overridden:
include-code::./CustomX509Configuration[tag=springSecurity,indent=0]
In the previous example, a username is extracted from the `emailAddress` field of a client certificate instead of CN, and account lookup uses a custom `ReactiveAuthenticationManager` instance.
For an example of configuring Netty and `WebClient` or `curl` command-line tool to use mutual TLS and enable X.509 authentication, see https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509.
The `subject-principal-regex` should contain a single group.
For example, the default expression (`CN=(.*?)`) matches the common name field.
So, if the subject name in the certificate is "CN=Jimi Hendrix, OU=...", this gives a user name of "Jimi Hendrix".
The matches are case insensitive.
So "emailAddress=(+.*?+)," matches "EMAILADDRESS=jimi@hendrix.org,CN=...", giving a user name "jimi@hendrix.org".
If the client presents a certificate and a valid username is successfully extracted, there should be a valid `Authentication` object in the security context.
If no certificate is found or no corresponding user could be found, the security context remains empty.
This means that you can use X.509 authentication with other options, such as a form-based login.
[[x509-ssl-config]]
== Setting up SSL in Tomcat
@@ -1377,14 +1377,17 @@ Java::
[source,java,role="primary"]
----
@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
public class MyPreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}
}
@Component
public class MyPostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
// ... authorization logic
}
}
@@ -1395,12 +1398,15 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
class MyPreAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocation> {
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationResult {
// ... authorization logic
}
}
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
@Component
class MyPostAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocationResult> {
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
// ... authorization logic
}
}
@@ -1422,13 +1428,13 @@ Java::
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(MyAuthorizationManager manager) {
Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(MyAuthorizationManager manager) {
Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
}
}
@@ -1441,15 +1447,15 @@ Kotlin::
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun preAuthorize(manager: MyAuthorizationManager) : Advisor {
fun preAuthorize(manager: MyPreAuthorizeAuthorizationManager): Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize(manager: MyAuthorizationManager) : Advisor {
fun postAuthorize(manager: MyPostAuthorizeAuthorizationManager): Advisor {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
}
}
@@ -1466,13 +1472,13 @@ Xml::
<bean id="preAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="preAuthorize">
<constructor-arg ref="myAuthorizationManager"/>
<constructor-arg ref="myPreAuthorizeAuthorizationManager"/>
</bean>
<bean id="postAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
factory-method="postAuthorize">
<constructor-arg ref="myAuthorizationManager"/>
<constructor-arg ref="myPostAuthorizeAuthorizationManager"/>
</bean>
----
======
@@ -1482,6 +1488,8 @@ Xml::
You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
====
You can also implement `MethodAuthorizationDeniedHandler` in the same manager class to override the default exception-handling behavior.
[[customizing-expression-handling]]
=== Customizing Expression Handling
@@ -63,7 +63,9 @@ To use this tag, you must also have an instance of `WebInvocationPrivilegeEvalua
If you are using the namespace, one is automatically registered.
This is an instance of `DefaultWebInvocationPrivilegeEvaluator`, which creates a dummy web request for the supplied URL and invokes the security interceptor to see whether the request would succeed or fail.
This lets you delegate to the access-control setup you defined by using `intercept-url` declarations within the `<http>` namespace configuration and saves having to duplicate the information (such as the required roles) within your JSPs.
You can also combine this approach with a `method` attribute (supplying the HTTP method, such as `POST`) for a more specific match.
If you have xref:servlet/authorization/authorize-http-requests.adoc#match-by-httpmethod[method-based authorization rules], you should combine this approach with the `method` attribute (supplying the HTTP method, such as `POST`) to activate the intended method-based rule.
For example, if you have a rule `.requestMatchers(POST, "/admin").hasRole("ADMIN")`, then you should do `<sec:authorize method="POST" url="/admin">` to match.
You can store the Boolean result of evaluating the tag (whether it grants or denies access) in a page context scope variable by setting the `var` attribute to the variable name, avoiding the need for duplicating and re-evaluating the condition at other points in the page.
@@ -40,17 +40,20 @@ And that's it!
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
It achieves this through a deterministic startup process:
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
1. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property
2. Query the `jwks_url` endpoint for supported algorithms
3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found
4. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
[NOTE]
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
====
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierJwtDecoder[`SupplierJwtDecoder`].
Consider wrapping any <<oauth2resourceserver-jwt-decoder,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
====
=== Runtime Expectations
@@ -66,7 +69,7 @@ So long as this scheme is indicated, Resource Server will attempt to process the
Given a well-formed JWT, Resource Server will:
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup or on first request, depending on configuration, and matched against the JWT
2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and
3. Map each scope to an authority with the prefix `SCOPE_`.
@@ -111,7 +114,7 @@ Ultimately, the returned `JwtAuthenticationToken` will be set on the xref:servle
[[oauth2resourceserver-jwt-jwkseturi]]
== Specifying the Authorization Server JWK Set Uri Directly
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
[source,yaml]
----
@@ -516,7 +519,9 @@ Java::
----
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
return jwtDecoder;
}
----
@@ -526,7 +531,9 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
return jwtDecoder
}
----
======
@@ -592,8 +599,10 @@ Java::
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -603,8 +612,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -619,8 +630,10 @@ Java::
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -630,8 +643,10 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
@@ -646,11 +661,13 @@ Java::
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
return jwtDecoder;
}
----
@@ -660,11 +677,13 @@ Kotlin::
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}.build()
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
return jwtDecoder
}
----
======
+4 -4
View File
@@ -1,10 +1,10 @@
{
"dependencies": {
"antora": "3.2.0-alpha.11",
"antora": "3.2.0-alpha.12",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.2",
"@antora/collector-extension": "1.0.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
"@springio/antora-extensions": "1.14.12",
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
}
}
+4
View File
@@ -10,6 +10,10 @@ plugins {
apply plugin: 'io.spring.convention.docs'
apply plugin: 'java'
node {
version = "24.15.0"
}
antora {
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
environment = [
@@ -0,0 +1,74 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.reactive.authentication.reactivex509;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@EnableWebFlux
public class CustomX509Configuration {
// tag::springSecurity[]
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
principalExtractor.setExtractPrincipalNameFromEmail(true);
// @formatter:off
UserDetails user = User
.withUsername("luke@monkeymachine")
.password("password")
.roles("USER")
.build();
// @formatter:on
ReactiveUserDetailsService users = new MapReactiveUserDetailsService(user);
ReactiveAuthenticationManager authenticationManager = new ReactivePreAuthenticatedAuthenticationManager(users);
// @formatter:off
http
.x509(x509 -> x509
.principalExtractor(principalExtractor)
.authenticationManager(authenticationManager)
)
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
);
// @formatter:on
return http.build();
}
// end::springSecurity[]
}
@@ -0,0 +1,67 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.reactive.authentication.reactivex509;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@EnableWebFlux
public class DefaultX509Configuration {
// tag::springSecurity[]
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
// @formatter:off
http
.x509(Customizer.withDefaults())
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
);
// @formatter:on
return http.build();
}
// end::springSecurity[]
@Bean
ReactiveUserDetailsService userDetailsService() {
// @formatter:off
UserDetails user = User
.withUsername("rod")
.password("password")
.roles("USER")
.build();
// @formatter:on
return new MapReactiveUserDetailsService(user);
}
}
@@ -0,0 +1,148 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.reactive.authentication.reactivex509;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.SslInfo;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
/**
* Tests {@link CustomX509Configuration}.
*
* @author Rob Winch
*/
@ExtendWith(SpringTestContextExtension.class)
public class X509ConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
WebTestClient client;
@Autowired
void setSpringSecurityFilterChain(WebFilter springSecurityFilterChain) {
this.client = WebTestClient.bindToController(WebTestClientBuilder.Http200RestController.class)
.webFilter(springSecurityFilterChain)
.apply(springSecurity())
.configureClient()
.build();
}
@Test
void x509WhenDefaultX509Configuration() throws Exception {
this.spring.register(DefaultX509Configuration.class).autowire();
X509Certificate certificate = loadCert("rod.cer");
// @formatter:off
this.client
.mutateWith(x509(certificate))
.get()
.uri("/")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Test
void x509WhenCustomX509Configuration() throws Exception {
this.spring.register(CustomX509Configuration.class).autowire();
X509Certificate certificate = X509TestUtils.buildTestCertificate();
// @formatter:off
this.client
.mutateWith(x509(certificate))
.get()
.uri("/")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
private static @NotNull WebTestClientConfigurer x509(X509Certificate certificate) {
return (builder, httpHandlerBuilder, connector) -> {
builder.apply(new WebTestClientConfigurer() {
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
@Nullable ClientHttpConnector connector) {
SslInfo sslInfo = new SslInfo() {
@Override
public @Nullable String getSessionId() {
return "sessionId";
}
@Override
public X509Certificate @Nullable [] getPeerCertificates() {
return new X509Certificate[] { certificate };
}
};
httpHandlerBuilder.filters((filters) -> filters.add(0, new SslInfoOverrideWebFilter(sslInfo)));
}
});
};
}
private static class SslInfoOverrideWebFilter implements WebFilter {
private final SslInfo sslInfo;
private SslInfoOverrideWebFilter(SslInfo sslInfo) {
this.sslInfo = sslInfo;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo)
.build();
ServerWebExchange sslInfoExchange = exchange.mutate().request(sslInfoRequest).build();
return chain.filter(sslInfoExchange);
}
}
private <T extends Certificate> T loadCert(String location) {
try (InputStream is = new ClassPathResource(location).getInputStream()) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
return (T) certFactory.generateCertificate(is);
}
catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.authentication.servletx509config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@EnableWebMvc
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class CustomX509Configuration {
// tag::springSecurity[]
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
principalExtractor.setExtractPrincipalNameFromEmail(true);
// @formatter:off
http
.x509((x509) -> x509
.x509PrincipalExtractor(principalExtractor)
)
.authorizeHttpRequests((exchanges) -> exchanges
.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
// end::springSecurity[]
@Bean
UserDetailsService userDetailsService() {
// @formatter:off
UserDetails user = User
.withUsername("luke@monkeymachine")
.password("password")
.roles("USER")
.build();
// @formatter:on
return new InMemoryUserDetailsManager(user);
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.authentication.servletx509config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@EnableWebMvc
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class DefaultX509Configuration {
// tag::springSecurity[]
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
// @formatter:off
http
.x509(Customizer.withDefaults())
.authorizeHttpRequests(exchanges -> exchanges
.anyRequest().authenticated()
);
// @formatter:on
return http.build();
}
// end::springSecurity[]
@Bean
UserDetailsService userDetailsService() {
// @formatter:off
UserDetails user = User
.withUsername("rod")
.password("password")
.roles("USER")
.build();
// @formatter:on
return new InMemoryUserDetailsManager(user);
}
}
@@ -0,0 +1,103 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.servlet.authentication.servletx509config;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.SslInfo;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests {@link CustomX509Configuration}.
*
* @author Rob Winch
*/
@ExtendWith(SpringTestContextExtension.class)
public class X509ConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
MockMvc mockMvc;
@Test
void x509WhenDefaultX509Configuration() throws Exception {
this.spring.register(DefaultX509Configuration.class, Http200Controller.class).autowire();
// @formatter:off
this.mockMvc.perform(get("/").with(x509("rod.cer")))
.andExpect(status().isOk())
.andExpect(authenticated().withUsername("rod"));
// @formatter:on
}
@Test
void x509WhenDefaultX509ConfigurationXml() throws Exception {
this.spring.testConfigLocations("DefaultX509Configuration.xml").autowire();
// @formatter:off
this.mockMvc.perform(get("/").with(x509("rod.cer")))
.andExpect(authenticated().withUsername("rod"));
// @formatter:on
}
@Test
void x509WhenCustomX509Configuration() throws Exception {
this.spring.register(CustomX509Configuration.class, Http200Controller.class).autowire();
X509Certificate certificate = X509TestUtils.buildTestCertificate();
// @formatter:off
this.mockMvc.perform(get("/").with(x509(certificate)))
.andExpect(status().isOk())
.andExpect(authenticated().withUsername("luke@monkeymachine"));
// @formatter:on
}
@RestController
static class Http200Controller {
@GetMapping("/**")
String ok() {
return "ok";
}
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.reactive.authentication.reactivex509
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity.http
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@EnableWebFlux
@EnableWebFluxSecurity
@Configuration(proxyBeanMethods = false)
class CustomX509Configuration {
// tag::springSecurity[]
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
val extractor = SubjectX500PrincipalExtractor()
extractor.setExtractPrincipalNameFromEmail(true)
// @formatter:off
val user = User
.withUsername("luke@monkeymachine")
.password("password")
.roles("USER")
.build()
// @formatter:on
val users: ReactiveUserDetailsService = MapReactiveUserDetailsService(user)
val authentication: ReactiveAuthenticationManager = ReactivePreAuthenticatedAuthenticationManager(users)
return http {
x509 {
principalExtractor = extractor
authenticationManager = authentication
}
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
// end::springSecurity[]
}
@@ -0,0 +1,64 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.reactive.authentication.reactivex509
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.web.reactive.config.EnableWebFlux
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@EnableWebFlux
@EnableWebFluxSecurity
@Configuration(proxyBeanMethods = false)
class DefaultX509Configuration {
// tag::springSecurity[]
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
x509 { }
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
// end::springSecurity[]
@Bean
fun userDetailsService(): MapReactiveUserDetailsService {
// @formatter:off
val user = User
.withUsername("rod")
.password("password")
.roles("USER")
.build()
// @formatter:on
return MapReactiveUserDetailsService(user)
}
}
@@ -0,0 +1,131 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.reactive.authentication.reactivex509
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.io.ClassPathResource
import org.springframework.http.client.reactive.ClientHttpConnector
import org.springframework.http.server.reactive.SslInfo
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder.Http200RestController
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.WebTestClientConfigurer
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
import reactor.core.publisher.Mono
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.function.Consumer
/**
* Tests [CustomX509Configuration].
*
* @author Rob Winch
*/
@ExtendWith(SpringTestContextExtension::class)
class X509ConfigurationTests {
@JvmField
val spring: SpringTestContext = SpringTestContext(this)
var client: WebTestClient? = null
@Autowired
fun setSpringSecurityFilterChain(springSecurityFilterChain: WebFilter) {
this.client = WebTestClient
.bindToController(Http200RestController::class.java)
.webFilter<WebTestClient.ControllerSpec>(springSecurityFilterChain)
.apply<WebTestClient.ControllerSpec>(SecurityMockServerConfigurers.springSecurity())
.configureClient()
.build()
}
@Test
fun x509WhenDefaultX509Configuration() {
this.spring.register(DefaultX509Configuration::class.java).autowire()
val certificate = loadCert<X509Certificate>("rod.cer")
// @formatter:off
this.client!!.mutateWith(x509(certificate))
.get()
.uri("/")
.exchange()
.expectStatus().isOk()
// @formatter:on
}
@Test
fun x509WhenCustomX509Configuration() {
this.spring.register(CustomX509Configuration::class.java).autowire()
val certificate = X509TestUtils.buildTestCertificate()
// @formatter:off
this.client!!.mutateWith(x509(certificate))
.get()
.uri("/")
.exchange()
.expectStatus().isOk()
// @formatter:on
}
private class SslInfoOverrideWebFilter(private val sslInfo: SslInfo) : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo)
.build()
val sslInfoExchange = exchange.mutate().request(sslInfoRequest).build()
return chain.filter(sslInfoExchange)
}
}
private fun <T : Certificate?> loadCert(location: String): T {
try {
ClassPathResource(location).getInputStream().use { `is` ->
val certFactory = CertificateFactory.getInstance("X.509")
return certFactory.generateCertificate(`is`) as T
}
} catch (ex: Exception) {
throw IllegalArgumentException(ex)
}
}
companion object {
private fun x509(certificate: X509Certificate): WebTestClientConfigurer {
return WebTestClientConfigurer { builder: WebTestClient.Builder, httpHandlerBuilder: WebHttpHandlerBuilder?, connector: ClientHttpConnector? ->
val sslInfo: SslInfo = object : SslInfo {
override fun getSessionId(): String {
return "sessionId"
}
override fun getPeerCertificates(): Array<X509Certificate?> {
return arrayOf(certificate)
}
}
httpHandlerBuilder?.filters(Consumer { filters: MutableList<WebFilter> ->
filters.add(
0,
SslInfoOverrideWebFilter(sslInfo)
)
})
}
}
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.authentication.servlet509config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
import org.springframework.web.servlet.config.annotation.EnableWebMvc
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@EnableWebMvc
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
class CustomX509Configuration {
// tag::springSecurity[]
@Bean
fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? {
val principalExtractor = SubjectX500PrincipalExtractor()
principalExtractor.setExtractPrincipalNameFromEmail(true)
// @formatter:off
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
x509 {
x509PrincipalExtractor = principalExtractor
}
}
return http.build()
}
// end::springSecurity[]
@Bean
fun userDetailsService(): UserDetailsService {
// @formatter:off
val user = User
.withUsername("luke@monkeymachine")
.password("password")
.roles("USER")
.build()
// @formatter:on
return InMemoryUserDetailsManager(user)
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.authentication.servlet509config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.web.servlet.config.annotation.EnableWebMvc
/**
* Demonstrates custom configuration for x509 reactive configuration.
*
* @author Rob Winch
*/
@EnableWebMvc
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
class DefaultX509Configuration {
// tag::springSecurity[]
@Bean
fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? {
// @formatter:off
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
x509 { }
}
// @formatter:on
return http.build()
}
// end::springSecurity[]
@Bean
fun userDetailsService(): UserDetailsService {
// @formatter:off
val user = User
.withUsername("rod")
.password("password")
.roles("USER")
.build()
// @formatter:on
return InMemoryUserDetailsManager(user)
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.authentication.servlet509config
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
/**
* Tests [CustomX509Configuration].
*
* @author Rob Winch
*/
@ExtendWith(SpringTestContextExtension::class)
class X509ConfigurationTests {
@JvmField
val spring: SpringTestContext = SpringTestContext(this)
@Autowired
var mockMvc: MockMvc? = null
@Test
@Throws(Exception::class)
fun x509WhenDefaultX509Configuration() {
this.spring.register(DefaultX509Configuration::class.java, Http200Controller::class.java).autowire()
// @formatter:off
this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
.andExpect(status().isOk())
.andExpect(authenticated().withUsername("rod"))
// @formatter:on
}
@Test
@Throws(Exception::class)
fun x509WhenCustomX509Configuration() {
this.spring.register(CustomX509Configuration::class.java, Http200Controller::class.java).autowire()
val certificate = X509TestUtils.buildTestCertificate()
// @formatter:off
this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509(certificate)))
.andExpect(status().isOk())
.andExpect(authenticated().withUsername("luke@monkeymachine"))
// @formatter:on
}
@RestController
internal class Http200Controller {
@GetMapping("/**")
fun ok(): String {
return "ok"
}
}
}
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 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:p="http://www.springframework.org/schema/p"
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">
<!-- tag::springSecurity[] -->
<http>
<intercept-url pattern="/**" access="authenticated"/>
<x509 principal-extractor-ref="principalExtractor"/>
</http>
<b:bean id="principalExtractor"
class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
p:extractPrincipalNameFromEmail="true"/>
<!-- end::springSecurity[] -->
<user-service id="us">
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
</b:beans>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 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:p="http://www.springframework.org/schema/p"
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">
<!-- tag::springSecurity[] -->
<http>
<intercept-url pattern="/**" access="authenticated"/>
<x509 />
</http>
<!-- end::springSecurity[] -->
<user-service>
<user name="rod" password="{noop}password" authorities="ROLE_USER"/>
</user-service>
</b:beans>
+1 -1
View File
@@ -14,7 +14,7 @@
# limitations under the License.
#
springBootVersion=3.3.3
version=6.5.8-SNAPSHOT
version=6.5.12-SNAPSHOT
samplesBranch=6.5.x
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+13 -11
View File
@@ -1,24 +1,25 @@
[versions]
com-squareup-okhttp3 = "3.14.9"
io-micrometer = "1.15.12"
io-rsocket = "1.1.5"
io-spring-javaformat = "0.0.47"
io-spring-nohttp = "0.0.11"
jakarta-websocket = "2.2.0"
org-apache-directory-server = "1.5.5"
org-apache-maven-resolver = "1.9.25"
org-apache-maven-resolver = "1.9.27"
org-aspectj = "1.9.25.1"
org-bouncycastle = "1.80"
org-bouncycastle = "1.80.2"
org-eclipse-jetty = "11.0.26"
org-jetbrains-kotlin = "1.9.25"
org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.17.0"
org-opensaml = "4.3.2"
org-opensaml5 = "5.1.2"
org-springframework = "6.2.15"
org-springframework = "6.2.19"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.29"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.5"
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.34"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.8"
com-google-inject-guice = "com.google.inject:guice:3.0"
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.4"
@@ -29,15 +30,16 @@ com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
com-unboundid-unboundid-ldapsdk7 = "com.unboundid:unboundid-ldapsdk:7.0.1"
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-micrometer-micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "io-micrometer" }
io-micrometer-micrometer-observation = { module = "io.micrometer:micrometer-observation", version.ref = "io-micrometer" }
io-mockk = "io.mockk:mockk:1.14.7"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.14"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.18"
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.13"
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.15"
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:2.1.1"
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
@@ -57,7 +59,7 @@ org-apache-directory-server-apacheds-protocol-shared = { module = "org.apache.di
org-apache-directory-server-apacheds-server-jndi = { module = "org.apache.directory.server:apacheds-server-jndi", version.ref = "org-apache-directory-server" }
org-apache-directory-shared-shared-ldap = "org.apache.directory.shared:shared-ldap:0.9.15"
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents:httpclient:4.5.14"
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.12"
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.16"
org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "org-apache-maven-resolver" }
org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" }
org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" }
@@ -71,7 +73,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.42.Final"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.53.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
@@ -88,7 +90,7 @@ org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.18"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.1.13"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.16"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=f1771298a70f6db5a29daf62378c4e18a17fc33c9ba6b14362e0cdf40610380d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
distributionSha256Sum=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -300,7 +300,11 @@ public final class OidcAuthorizedClientRefreshedEventListener
return;
}
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
// The auth_time claim MUST represent the time of the original authentication OR
// the most recent time when the end-user reauthenticated when "prompt=login" is
// passed in the authentication request
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())
&& !idToken.getAuthenticatedAt().isAfter(existingOidcUser.getIdToken().getAuthenticatedAt())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
@@ -269,6 +269,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
}
@SuppressWarnings("serial")
private static final class OAuth2AuthorizationRequestException extends AuthenticationException {
OAuth2AuthorizationRequestException(Throwable cause) {
@@ -419,8 +419,8 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
}
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.SECONDS);
public void onApplicationEventWhenIdTokenAuthenticatedAtBeforeCurrentAuthenticatedAtThenThrowsOAuth2AuthenticationException() {
Instant authTime = this.oidcUser.getAuthenticatedAt().minus(5, ChronoUnit.MINUTES);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
@@ -440,8 +440,26 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
// gh-18839
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesThenOidcUserRefreshedEventPublished() {
public void onApplicationEventWhenIdTokenAuthenticatedAtAfterCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.MINUTES);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
this.accessTokenResponse, this.authorizedClient);
this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
verify(this.applicationEventPublisher).publishEvent(any(OidcUserRefreshedEvent.class));
verifyNoMoreInteractions(this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
Instant authTime = this.authentication.getPrincipal().getAttribute(IdTokenClaimNames.AUTH_TIME);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.core.oidc.user;
import java.io.Serial;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
@@ -114,4 +115,38 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
return this.userInfo;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
DefaultOidcUser that = (DefaultOidcUser) obj;
if (!this.getName().equals(that.getName())) {
return false;
}
if (!this.getAuthorities().equals(that.getAuthorities())) {
return false;
}
if (this.getIdToken().getIssuer() == null || that.getIdToken().getIssuer() == null) {
return false;
}
return Objects.equals(this.getIdToken().getIssuer().toExternalForm(),
that.getIdToken().getIssuer().toExternalForm())
&& Objects.equals(this.getIdToken().getSubject(), that.getIdToken().getSubject());
}
@Override
public int hashCode() {
int result = this.getName().hashCode();
result = 31 * result + this.getAuthorities().hashCode();
result = 31 * result + ((this.getIdToken().getIssuer() != null)
? this.getIdToken().getIssuer().toExternalForm().hashCode() : 0);
result = 31 * result
+ ((this.getIdToken().getSubject() != null) ? this.getIdToken().getSubject().hashCode() : 0);
return result;
}
}
@@ -17,6 +17,7 @@
package org.springframework.security.oauth2.core.oidc.user;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -147,4 +148,31 @@ public class DefaultOidcUserTests {
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
}
// gh-18622
@Test
public void equalsWhenOidcUserPrincipalSameThenTrue() {
String issuer = "https://example.com";
String subject = "subject-1";
// @formatter:off
OidcIdToken idToken1 = OidcIdToken.withTokenValue("id-token-value-1")
.issuer(issuer)
.subject(subject)
.issuedAt(Instant.now())
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
.build();
OidcIdToken idToken2 = OidcIdToken.withTokenValue("id-token-value-2")
.issuer(issuer)
.subject(subject)
.issuedAt(Instant.now())
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
.build();
// @formatter:on
DefaultOidcUser user1 = new DefaultOidcUser(AUTHORITIES, idToken1, USER_INFO);
DefaultOidcUser user2 = new DefaultOidcUser(AUTHORITIES, idToken2, USER_INFO);
assertThat(user1).isEqualTo(user2);
}
}
@@ -185,6 +185,7 @@ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory<DPoPP
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
@SuppressWarnings("serial")
private static final class JtiCache extends LinkedHashMap<String, Long> {
private static final int MAX_SIZE = 1000;
@@ -230,7 +230,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
.getConfigurationForIssuerLocation(issuer, rest);
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
return configuration.get("jwks_uri").toString();
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
.validator(JwtValidators.createDefaultWithIssuer(issuer));
}
/**
@@ -289,6 +290,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
this.jwkSetUri = (rest) -> jwkSetUri;
@@ -423,6 +426,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
return this;
}
JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
Assert.notNull(validator, "validator cannot be null");
this.validator = validator;
return this;
}
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
if (this.signatureAlgorithms.isEmpty()) {
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
@@ -461,7 +470,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
* @return the configured {@link NimbusJwtDecoder}
*/
public NimbusJwtDecoder build() {
return new NimbusJwtDecoder(processor());
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor());
decoder.setJwtValidator(this.validator);
return decoder;
}
private static final class SpringJWKSource<C extends SecurityContext> implements JWKSetSource<C> {
@@ -241,7 +241,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
}
return Mono.just(configuration.get("jwks_uri").toString());
}),
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
.validator(JwtValidators.createDefaultWithIssuer(issuer));
}
/**
@@ -332,6 +333,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer;
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
@@ -456,6 +459,11 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
return this;
}
JwkSetUriReactiveJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
this.validator = validator;
return this;
}
JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer(
BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer) {
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
@@ -468,7 +476,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
* @return the configured {@link NimbusReactiveJwtDecoder}
*/
public NimbusReactiveJwtDecoder build() {
return new NimbusReactiveJwtDecoder(processor());
NimbusReactiveJwtDecoder decoder = new NimbusReactiveJwtDecoder(processor());
decoder.setJwtValidator(this.validator);
return decoder;
}
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
@@ -328,11 +328,26 @@ public class NimbusJwtDecoderTests {
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
.restOperations(restOperations)
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@Test
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
String issuer = "https://example.org/wrong-issuer";
RestOperations restOperations = mock(RestOperations.class);
given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class)))
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
assertThatExceptionOfType(JwtValidationException.class).isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT));
}
@Test
public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
// @formatter:off
@@ -617,11 +617,31 @@ public class NimbusReactiveJwtDecoderTests {
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
given(spec.retrieve()).willReturn(responseSpec);
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
.webClient(webClient)
.build();
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@Test
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
String issuer = "https://example.org/wrong-issuer";
WebClient real = WebClient.builder().build();
WebClient.RequestHeadersUriSpec spec = spy(real.get());
WebClient webClient = spy(WebClient.class);
given(webClient.get()).willReturn(spec);
WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
given(spec.retrieve()).willReturn(responseSpec);
ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
.webClient(webClient)
.build();
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
assertThatExceptionOfType(JwtValidationException.class)
.isThrownBy(() -> jwtDecoder.decode(this.messageReadToken).block());
}
@Test
@@ -30,6 +30,23 @@ import org.springframework.util.Assert;
* A {@link Jwt} to {@link GrantedAuthority} {@link Converter} that is a composite of
* converters.
*
* <p>
* This is handy when needing to read authorities from multiple locations in a JWT; each
* underlying converter is called in series and the results are aggregated into a single
* collection of authorities.
*
* <p>
* For example, you might have a claim called "scope" and another called "roles". With
* {@link DelegatingJwtGrantedAuthoritiesConverter}, you can do:
*
* <code>
* JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
* JwtGrantedAuthoritiesConverter roles = new JwtGrantedAUthoritiesConverter();
* roles.setAuthoritiesClaimName("roles");
* roles.setAuthorityPrefix("ROLE_");
* return new DelegatingJwtGrantedAuthoritiesConverter(scopes, roles);
* </code>
*
* @author Laszlo Stahorszki
* @author Josh Cummings
* @since 5.5
@@ -36,9 +36,6 @@ import org.springframework.util.Assert;
* Uses an expression for extracting the token claim value to use for mapping
* {@link GrantedAuthority authorities}.
*
* Note this can be used in combination with a
* {@link DelegatingJwtGrantedAuthoritiesConverter}.
*
* @author Thomas Darimont
* @since 6.4
*/
@@ -361,6 +361,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
* @since 6.5
*/
public SpringOpaqueTokenIntrospector build() {
Assert.notNull(this.clientId, "clientId cannot be null");
Assert.notNull(this.clientSecret, "clientSecret cannot be null");
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(this.clientId, this.clientSecret));
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
@@ -314,6 +314,8 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
* @since 6.5
*/
public SpringReactiveOpaqueTokenIntrospector build() {
Assert.notNull(this.clientId, "clientId cannot be null");
Assert.notNull(this.clientSecret, "clientSecret cannot be null");
WebClient webClient = WebClient.builder()
.defaultHeaders((h) -> h.setBasicAuth(this.clientId, this.clientSecret))
.build();
@@ -383,6 +383,13 @@ public class SpringOpaqueTokenIntrospectorTests {
}
}
// gh-19201
@Test
public void builderWhenMissingClientCredentialsThenThrowsException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> SpringOpaqueTokenIntrospector.withIntrospectionUri(INTROSPECTION_URL).build());
}
private static ResponseEntity<Map<String, Object>> response(String content) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -308,6 +308,13 @@ public class SpringReactiveOpaqueTokenIntrospectorTests {
}
}
// gh-19201
@Test
public void builderWhenMissingClientCredentialsThenThrowsException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(INTROSPECTION_URL).build());
}
private WebClient mockResponse(String response) {
return mockResponse(toMap(response));
}
@@ -18,6 +18,7 @@ package org.springframework.security.saml2.internal;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
@@ -64,7 +65,7 @@ final class Saml2Utils {
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ final class Saml2Utils {
}
static class CappedOutputStream extends OutputStream {
private static final long MAX_SIZE = 1024 * 1024;
private final OutputStream delegate;
private int size;
CappedOutputStream(OutputStream delegate) {
this.delegate = delegate;
}
@Override
public void write(int b) throws IOException {
if (this.size >= MAX_SIZE) {
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
}
this.delegate.write(b);
this.size++;
}
}
}
@@ -326,62 +326,75 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
boolean responseSigned = response.isSigned();
ResponseToken responseToken = new ResponseToken(response, token);
Saml2ResponseValidatorResult result = this.responseSignatureValidator.convert(responseToken);
Collection<Saml2Error> responseSignatureErrors = this.responseSignatureValidator.convert(responseToken)
.getErrors();
if (!responseSignatureErrors.isEmpty()) {
reportErrors(response, responseSignatureErrors);
return;
}
Collection<Saml2Error> errors = new ArrayList<>();
if (responseSigned) {
this.responseElementsDecrypter.accept(responseToken);
}
else if (!response.getEncryptedAssertions().isEmpty()) {
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Did not decrypt response [" + response.getID() + "] since it is not signed"));
}
if (!this.validateResponseAfterAssertions) {
result = result.concat(this.responseValidator.convert(responseToken));
errors.addAll(this.responseValidator.convert(responseToken).getErrors());
}
boolean allAssertionsSigned = true;
for (Assertion assertion : response.getAssertions()) {
AssertionToken assertionToken = new AssertionToken(assertion, token);
result = result.concat(this.assertionSignatureValidator.convert(assertionToken));
Collection<Saml2Error> assertionSignatureErrors = this.assertionSignatureValidator.convert(assertionToken)
.getErrors();
errors.addAll(assertionSignatureErrors);
allAssertionsSigned = allAssertionsSigned && assertion.isSigned();
if (!assertionSignatureErrors.isEmpty()) {
continue;
}
if (responseSigned || assertion.isSigned()) {
this.assertionElementsDecrypter.accept(new AssertionToken(assertion, token));
}
result = result.concat(this.assertionValidator.convert(assertionToken));
errors.addAll(this.assertionValidator.convert(assertionToken).getErrors());
}
if (!responseSigned && !allAssertionsSigned) {
String description = "Either the response or one of the assertions is unsigned. "
+ "Please either sign the response or all of the assertions.";
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, description));
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, description));
}
if (this.validateResponseAfterAssertions) {
result = result.concat(this.responseValidator.convert(responseToken));
errors.addAll(this.responseValidator.convert(responseToken).getErrors());
}
else {
Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions());
if (firstAssertion != null && !hasName(firstAssertion)) {
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
"Assertion [" + firstAssertion.getID() + "] is missing a subject");
result = result.concat(error);
errors.add(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
"Assertion [" + firstAssertion.getID() + "] is missing a subject"));
}
}
if (result.hasErrors()) {
Collection<Saml2Error> errors = result.getErrors();
if (this.logger.isTraceEnabled()) {
this.logger.trace("Found " + errors.size() + " validation errors in SAML response [" + response.getID()
+ "]: " + errors);
}
else if (this.logger.isDebugEnabled()) {
this.logger
.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID() + "]");
}
Saml2Error first = errors.iterator().next();
throw createAuthenticationException(first.getErrorCode(), first.getDescription(), null);
}
else {
reportErrors(response, errors);
}
private void reportErrors(Response response, Collection<Saml2Error> errors) {
if (errors.isEmpty()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Successfully processed SAML Response [" + response.getID() + "]");
}
return;
}
if (this.logger.isTraceEnabled()) {
this.logger.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID()
+ "]: " + errors);
}
else if (this.logger.isDebugEnabled()) {
this.logger
.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID() + "]");
}
Saml2Error first = errors.iterator().next();
throw createAuthenticationException(first.getErrorCode(), first.getDescription(), null);
}
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseSignatureValidator() {
@@ -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;
@@ -18,6 +18,7 @@ package org.springframework.security.saml2.provider.service.authentication;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
@@ -64,7 +65,7 @@ final class Saml2Utils {
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ final class Saml2Utils {
}
static class CappedOutputStream extends OutputStream {
private static final long MAX_SIZE = 1024 * 1024;
private final OutputStream delegate;
private int size;
CappedOutputStream(OutputStream delegate) {
this.delegate = delegate;
}
@Override
public void write(int b) throws IOException {
if (this.size >= MAX_SIZE) {
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
}
this.delegate.write(b);
this.size++;
}
}
}
@@ -16,8 +16,9 @@
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.Collections;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
@@ -56,84 +57,74 @@ class BaseOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator
LogoutRequest logoutRequest = this.saml.deserialize(Saml2Utils.withEncoded(request.getSamlRequest())
.inflate(request.getBinding() == Saml2MessageBinding.REDIRECT)
.decode());
return Saml2LogoutValidatorResult.withErrors()
.errors(verifySignature(request, logoutRequest, registration))
.errors(validateRequest(logoutRequest, registration, authentication))
.build();
Collection<Saml2Error> errors = verifySignature(request, logoutRequest, registration);
if (!errors.isEmpty()) {
return Saml2LogoutValidatorResult.withErrors(errors.toArray(Saml2Error[]::new)).build();
}
errors = validateRequest(logoutRequest, registration, authentication);
return errors.isEmpty() ? Saml2LogoutValidatorResult.success()
: Saml2LogoutValidatorResult.withErrors(errors.toArray(Saml2Error[]::new)).build();
}
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest,
private Collection<Saml2Error> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest,
RelyingPartyRegistration registration) {
AssertingPartyMetadata details = registration.getAssertingPartyMetadata();
Collection<Saml2X509Credential> credentials = details.getVerificationX509Credentials();
VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId());
return (errors) -> {
if (logoutRequest.isSigned()) {
errors.addAll(verify.verify(logoutRequest));
}
else {
RedirectParameters params = new RedirectParameters(request.getParameters(),
request.getParametersQuery(), logoutRequest);
errors.addAll(verify.verify(params));
}
};
if (logoutRequest.isSigned()) {
return verify.verify(logoutRequest);
}
RedirectParameters params = new RedirectParameters(request.getParameters(), request.getParametersQuery(),
logoutRequest);
return verify.verify(params);
}
private Consumer<Collection<Saml2Error>> validateRequest(LogoutRequest request,
RelyingPartyRegistration registration, Authentication authentication) {
return (errors) -> {
validateIssuer(request, registration).accept(errors);
validateDestination(request, registration).accept(errors);
validateSubject(request, registration, authentication).accept(errors);
};
private Collection<Saml2Error> validateRequest(LogoutRequest request, RelyingPartyRegistration registration,
Authentication authentication) {
Collection<Saml2Error> errors = new ArrayList<>();
errors.addAll(validateIssuer(request, registration));
errors.addAll(validateDestination(request, registration));
errors.addAll(validateSubject(request, registration, authentication));
return errors;
}
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutRequest request,
RelyingPartyRegistration registration) {
return (errors) -> {
if (request.getIssuer() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutRequest"));
return;
}
String issuer = request.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
errors
.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
};
private Collection<Saml2Error> validateIssuer(LogoutRequest request, RelyingPartyRegistration registration) {
if (request.getIssuer() == null) {
return Collections.singletonList(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutRequest"));
}
String issuer = request.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
return Collections.singletonList(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
return Collections.emptyList();
}
private Consumer<Collection<Saml2Error>> validateDestination(LogoutRequest request,
RelyingPartyRegistration registration) {
return (errors) -> {
if (request.getDestination() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutRequest"));
return;
}
String destination = request.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceLocation())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
};
private Collection<Saml2Error> validateDestination(LogoutRequest request, RelyingPartyRegistration registration) {
if (request.getDestination() == null) {
return Collections.singletonList(
new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, "Failed to find destination in LogoutRequest"));
}
String destination = request.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceLocation())) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
return Collections.emptyList();
}
private Consumer<Collection<Saml2Error>> validateSubject(LogoutRequest request,
RelyingPartyRegistration registration, Authentication authentication) {
return (errors) -> {
if (authentication == null) {
return;
}
NameID nameId = getNameId(request, registration);
if (nameId == null) {
errors
.add(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest"));
return;
}
validateNameId(nameId, authentication, errors);
};
private Collection<Saml2Error> validateSubject(LogoutRequest request, RelyingPartyRegistration registration,
Authentication authentication) {
if (authentication == null) {
return Collections.emptyList();
}
NameID nameId = getNameId(request, registration);
if (nameId == null) {
return Collections.singletonList(
new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest"));
}
return validateNameId(nameId, authentication);
}
private NameID getNameId(LogoutRequest request, RelyingPartyRegistration registration) {
@@ -141,12 +132,13 @@ class BaseOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator
return request.getNameID();
}
private void validateNameId(NameID nameId, Authentication authentication, Collection<Saml2Error> errors) {
private Collection<Saml2Error> validateNameId(NameID nameId, Authentication authentication) {
String name = nameId.getValue();
if (!name.equals(authentication.getName())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST,
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST,
"Failed to match subject in LogoutRequest with currently logged in user"));
}
return Collections.emptyList();
}
}
@@ -16,8 +16,9 @@
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.Collections;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
@@ -52,101 +53,90 @@ class BaseOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidato
LogoutResponse logoutResponse = this.saml.deserialize(Saml2Utils.withEncoded(response.getSamlResponse())
.inflate(response.getBinding() == Saml2MessageBinding.REDIRECT)
.decode());
return Saml2LogoutValidatorResult.withErrors()
.errors(verifySignature(response, logoutResponse, registration))
.errors(validateRequest(logoutResponse, registration))
.errors(validateLogoutRequest(logoutResponse, request.getId()))
.build();
Collection<Saml2Error> errors = verifySignature(response, logoutResponse, registration);
if (!errors.isEmpty()) {
return Saml2LogoutValidatorResult.withErrors(errors.toArray(Saml2Error[]::new)).build();
}
errors = validateRequest(logoutResponse, registration, request.getId());
return errors.isEmpty() ? Saml2LogoutValidatorResult.success()
: Saml2LogoutValidatorResult.withErrors(errors.toArray(Saml2Error[]::new)).build();
}
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutResponse response,
LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
return (errors) -> {
AssertingPartyMetadata details = registration.getAssertingPartyMetadata();
Collection<Saml2X509Credential> credentials = details.getVerificationX509Credentials();
VerificationConfigurer verify = this.saml.withVerificationKeys(credentials)
.entityId(details.getEntityId())
.entityId(details.getEntityId());
if (logoutResponse.isSigned()) {
errors.addAll(verify.verify(logoutResponse));
}
else {
RedirectParameters params = new RedirectParameters(response.getParameters(),
response.getParametersQuery(), logoutResponse);
errors.addAll(verify.verify(params));
}
};
}
private Consumer<Collection<Saml2Error>> validateRequest(LogoutResponse response,
private Collection<Saml2Error> verifySignature(Saml2LogoutResponse response, LogoutResponse logoutResponse,
RelyingPartyRegistration registration) {
return (errors) -> {
validateIssuer(response, registration).accept(errors);
validateDestination(response, registration).accept(errors);
validateStatus(response).accept(errors);
};
AssertingPartyMetadata details = registration.getAssertingPartyMetadata();
Collection<Saml2X509Credential> credentials = details.getVerificationX509Credentials();
VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId());
if (logoutResponse.isSigned()) {
return verify.verify(logoutResponse);
}
RedirectParameters params = new RedirectParameters(response.getParameters(), response.getParametersQuery(),
logoutResponse);
return verify.verify(params);
}
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
if (response.getIssuer() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
return;
}
String issuer = response.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
errors
.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
};
private Collection<Saml2Error> validateRequest(LogoutResponse response, RelyingPartyRegistration registration,
String logoutRequestId) {
Collection<Saml2Error> errors = new ArrayList<>();
errors.addAll(validateIssuer(response, registration));
errors.addAll(validateDestination(response, registration));
errors.addAll(validateStatus(response));
errors.addAll(validateLogoutRequest(response, logoutRequestId));
return errors;
}
private Consumer<Collection<Saml2Error>> validateDestination(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
if (response.getDestination() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutResponse"));
return;
}
String destination = response.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
};
private Collection<Saml2Error> validateIssuer(LogoutResponse response, RelyingPartyRegistration registration) {
if (response.getIssuer() == null) {
return Collections.singletonList(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
}
String issuer = response.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) {
return Collections.singletonList(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
return Collections.emptyList();
}
private Consumer<Collection<Saml2Error>> validateStatus(LogoutResponse response) {
return (errors) -> {
if (response.getStatus() == null) {
return;
}
if (response.getStatus().getStatusCode() == null) {
return;
}
if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) {
return;
}
if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) {
return;
}
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed"));
};
private Collection<Saml2Error> validateDestination(LogoutResponse response, RelyingPartyRegistration registration) {
if (response.getDestination() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutResponse"));
}
String destination = response.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
return Collections.emptyList();
}
private Consumer<Collection<Saml2Error>> validateLogoutRequest(LogoutResponse response, String id) {
return (errors) -> {
if (response.getInResponseTo() == null) {
return;
}
if (response.getInResponseTo().equals(id)) {
return;
}
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE,
"LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest"));
};
private Collection<Saml2Error> validateStatus(LogoutResponse response) {
if (response.getStatus() == null) {
return Collections.emptyList();
}
if (response.getStatus().getStatusCode() == null) {
return Collections.emptyList();
}
if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) {
return Collections.emptyList();
}
if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) {
return Collections.emptyList();
}
return Collections
.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed"));
}
private Collection<Saml2Error> validateLogoutRequest(LogoutResponse response, String id) {
if (response.getInResponseTo() == null) {
return Collections.emptyList();
}
if (response.getInResponseTo().equals(id)) {
return Collections.emptyList();
}
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE,
"LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest"));
}
}
@@ -18,6 +18,7 @@ package org.springframework.security.saml2.provider.service.authentication.logou
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
@@ -64,7 +65,7 @@ final class Saml2Utils {
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ final class Saml2Utils {
}
static class CappedOutputStream extends OutputStream {
private static final long MAX_SIZE = 1024 * 1024;
private final OutputStream delegate;
private int size;
CappedOutputStream(OutputStream delegate) {
this.delegate = delegate;
}
@Override
public void write(int b) throws IOException {
if (this.size >= MAX_SIZE) {
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
}
this.delegate.write(b);
this.size++;
}
}
}
@@ -18,6 +18,7 @@ package org.springframework.security.saml2.provider.service.metadata;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
@@ -64,7 +65,7 @@ final class Saml2Utils {
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ final class Saml2Utils {
}
static class CappedOutputStream extends OutputStream {
private static final long MAX_SIZE = 1024 * 1024;
private final OutputStream delegate;
private int size;
CappedOutputStream(OutputStream delegate) {
this.delegate = delegate;
}
@Override
public void write(int b) throws IOException {
if (this.size >= MAX_SIZE) {
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
}
this.delegate.write(b);
this.size++;
}
}
}
@@ -42,6 +42,7 @@ import org.springframework.security.saml2.core.Saml2X509Credential;
* </pre>
*/
@Deprecated
@SuppressWarnings("serial")
public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistration {
OpenSamlRelyingPartyRegistration(RelyingPartyRegistration registration) {
@@ -18,6 +18,7 @@ package org.springframework.security.saml2.provider.service.registration;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
@@ -64,7 +65,7 @@ final class Saml2Utils {
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ final class Saml2Utils {
}
static class CappedOutputStream extends OutputStream {
private static final long MAX_SIZE = 1024 * 1024;
private final OutputStream delegate;
private int size;
CappedOutputStream(OutputStream delegate) {
this.delegate = delegate;
}
@Override
public void write(int b) throws IOException {
if (this.size >= MAX_SIZE) {
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
}
this.delegate.write(b);
this.size++;
}
}
}
@@ -35,6 +35,7 @@ import org.springframework.util.Assert;
* that it was for the user trying to log in. Please see the reference for details.
*
* @author Josh Cummings
* @author Andrey Litvitski
* @since 6.5
*/
public final class CacheSaml2AuthenticationRequestRepository
@@ -52,7 +53,8 @@ public final class CacheSaml2AuthenticationRequestRepository
@Override
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
HttpServletRequest request, HttpServletResponse response) {
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
Assert.notNull(authenticationRequest, "authenticationRequest must not be null");
String relayState = authenticationRequest.getRelayState();
Assert.notNull(relayState, "relayState must not be null");
this.cache.put(relayState, authenticationRequest);
}
@@ -18,6 +18,7 @@ package org.springframework.security.saml2.provider.service.web;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
@@ -64,7 +65,7 @@ final class Saml2Utils {
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ final class Saml2Utils {
}
static class CappedOutputStream extends OutputStream {
private static final long MAX_SIZE = 1024 * 1024;
private final OutputStream delegate;
private int size;
CappedOutputStream(OutputStream delegate) {
this.delegate = delegate;
}
@Override
public void write(int b) throws IOException {
if (this.size >= MAX_SIZE) {
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
}
this.delegate.write(b);
this.size++;
}
}
}
@@ -24,17 +24,17 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver;
import org.springframework.security.web.FormPostRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
@@ -67,6 +67,8 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
private Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = new HttpSessionSaml2AuthenticationRequestRepository();
private final RedirectStrategy formPostRedirectStrategy = new FormPostRedirectStrategy();
/**
* Construct a {@link Saml2WebSsoAuthenticationRequestFilter} with the strategy for
* resolving the {@code AuthnRequest}
@@ -132,54 +134,11 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
private void sendPost(HttpServletRequest request, HttpServletResponse response,
Saml2PostAuthenticationRequest authenticationRequest) throws IOException {
this.authenticationRequestRepository.saveAuthenticationRequest(authenticationRequest, request, response);
String html = createSamlPostRequestFormData(authenticationRequest);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
}
private String createSamlPostRequestFormData(Saml2PostAuthenticationRequest authenticationRequest) {
String authenticationRequestUri = authenticationRequest.getAuthenticationRequestUri();
String relayState = authenticationRequest.getRelayState();
String samlRequest = authenticationRequest.getSamlRequest();
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n").append(" <head>\n");
html.append(" <meta http-equiv=\"Content-Security-Policy\" ")
.append("content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">\n");
html.append(" <meta charset=\"utf-8\" />\n");
html.append(" </head>\n");
html.append(" <body>\n");
html.append(" <noscript>\n");
html.append(" <p>\n");
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
html.append(" you must press the Continue button once to proceed.\n");
html.append(" </p>\n");
html.append(" </noscript>\n");
html.append(" \n");
html.append(" <form action=\"");
html.append(authenticationRequestUri);
html.append("\" method=\"post\">\n");
html.append(" <div>\n");
html.append(" <input type=\"hidden\" name=\"SAMLRequest\" value=\"");
html.append(HtmlUtils.htmlEscape(samlRequest));
html.append("\"/>\n");
if (StringUtils.hasText(relayState)) {
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
html.append(HtmlUtils.htmlEscape(relayState));
html.append("\"/>\n");
}
html.append(" </div>\n");
html.append(" <noscript>\n");
html.append(" <div>\n");
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
html.append(" </div>\n");
html.append(" </noscript>\n");
html.append(" </form>\n");
html.append(" \n");
html.append(" <script>window.onload = function() { document.forms[0].submit(); }</script>\n");
html.append(" </body>\n");
html.append("</html>");
return html.toString();
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authenticationRequest.getAuthenticationRequestUri());
addParameter(Saml2ParameterNames.SAML_REQUEST, authenticationRequest.getSamlRequest(), uriBuilder);
addParameter(Saml2ParameterNames.RELAY_STATE, authenticationRequest.getRelayState(), uriBuilder);
this.formPostRedirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
}

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