1
0
mirror of synced 2026-07-17 08:35:13 +00:00

Compare commits

...

107 Commits

Author SHA1 Message Date
github-actions[bot] c8a0601e6a Release 5.8.11 2024-03-18 11:10:03 +00:00
Marcus Hert Da Coregio 2c9dc08e43 Merge branch '5.7.x' into 5.8.x
Closes gh-14664
2024-03-18 06:40:34 -03:00
Marcus Hert Da Coregio 5a7f12f1a9 Check for null Authentication
Closes gh-14715
2024-03-18 06:39:08 -03:00
dependabot[bot] 3b355a0278 Bump org.springframework:spring-framework-bom from 5.3.32 to 5.3.33
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 5.3.32 to 5.3.33.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v5.3.32...v5.3.33)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-14 21:07:46 -07:00
Marcus Hert Da Coregio a6d362fa18 Make trigger-dependabot-auto-merge-forward.yml run on push
Issue gh-14721
2024-03-14 15:12:14 -03:00
Marcus Hert Da Coregio 37984c2e11 Add GH_TOKEN to set-milestone
Issue gh-14721
2024-03-14 14:45:36 -03:00
Marcus Hert Da Coregio b3a43d6154 Use token only for auto merge
Issue gh-14721
2024-03-14 14:42:33 -03:00
Marcus Hert Da Coregio a87fc4ea8a Use pull_request_target for merge-dependabot-pr.yml
Issue gh-14721
2024-03-14 14:33:00 -03:00
Marcus Hert Da Coregio e7bff4240a Use GH_ACTIONS_REPO_TOKEN for merge-dependabot-pr.yml
Issue gh-14721
2024-03-14 09:23:31 -03:00
dependabot[bot] 4e6b8e4d29 Bump io.projectreactor:reactor-bom from 2020.0.41 to 2020.0.42
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2020.0.41 to 2020.0.42.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2020.0.41...2020.0.42)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-13 04:24:14 +00:00
dependabot[bot] e550c48180 Bump io.projectreactor.netty:reactor-netty from 1.0.41 to 1.0.43
Bumps [io.projectreactor.netty:reactor-netty](https://github.com/reactor/reactor-netty) from 1.0.41 to 1.0.43.
- [Release notes](https://github.com/reactor/reactor-netty/releases)
- [Commits](https://github.com/reactor/reactor-netty/compare/v1.0.41...v1.0.43)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-13 04:11:01 +00:00
Marcus Hert Da Coregio a3c06d98ca Trigger Dependabot Workflow when PR is merged
Issue gh-14721
2024-03-12 10:26:17 -03:00
Marcus Hert Da Coregio 8f42c86a57 Use AuthorizationInterceptorsOrder for Post Authorize Method Interceptors
Closes gh-14720
2024-03-12 10:17:45 -03:00
Marcus Hert Da Coregio 5dae6da15a Remove branch filter
Issue gh-14535
2024-03-11 09:25:58 -03:00
Josh Cummings e1c4177cd8 Fix ServerLogoutHandler Order in Docs
Closes gh-14379
2024-03-04 15:42:09 -07:00
Marcus Hert Da Coregio c2e97d7661 Ignore dependabot branches on trigger auto merge 2024-02-29 10:52:44 -03:00
Josh Cummings 94a66f7a26 Change Structure101 Repository URL
Issug gh-14639
2024-02-26 10:03:09 -07:00
Christian Becker 5f80468de3 Updated copyright date 2024-02-22 13:19:05 -07:00
Christian Becker 2f762fefe1 Allow tab in HTTP header values.
Closes gh-14573
2024-02-22 13:19:05 -07:00
DingHao 45c37c4454 Remove duplicate setSecurityContextHolderStrategy
Closes gh-14592
2024-02-22 12:14:35 -07:00
Josh Cummings 0c70f358d5 Update to Structure101 7.0.24375
Closes gh-14629
2024-02-20 15:43:58 -07:00
github-actions[bot] 7d38686e09 Next development version 2024-02-16 19:29:53 +00:00
github-actions[bot] 8b9beb0e1f Release 5.8.10 2024-02-16 18:47:27 +00:00
Marcus Hert Da Coregio 6230806d56 Change branch pattern
Issue gh-14535
2024-02-16 11:51:53 -03:00
Marcus Hert Da Coregio 2159f3a93d Fix branch pattern
Issue gh-14535
2024-02-16 09:50:42 -03:00
dependabot[bot] dc366cf8da Bump org.springframework:spring-framework-bom from 5.3.31 to 5.3.32
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 5.3.31 to 5.3.32.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v5.3.31...v5.3.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-16 04:04:11 +00:00
Marcus Hert Da Coregio d32202bce3 Revert "Bump org-aspectj from 1.9.20.1 to 1.9.21.1"
This reverts commit a0b1d6317c.
2024-02-15 09:45:49 -03:00
dependabot[bot] 349ac07c10 Bump io.projectreactor:reactor-bom from 2020.0.40 to 2020.0.41
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2020.0.40 to 2020.0.41.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2020.0.40...2020.0.41)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-15 06:42:20 -06:00
Marcus Hert Da Coregio 59be5553a1 Fix workflow syntax
Issue gh-14535
2024-02-15 09:23:35 -03:00
Marcus Hert Da Coregio 2bfdb0c6cd Only run workflow for branches that matches [5-9]+.[5-9]+.x
Issue gh-14535
2024-02-14 01:15:16 -03:00
dependabot[bot] 2ed7b857c3 Bump org-eclipse-jetty from 9.4.53.v20231009 to 9.4.54.v20240208
Bumps `org-eclipse-jetty` from 9.4.53.v20231009 to 9.4.54.v20240208.

Updates `org.eclipse.jetty:jetty-server` from 9.4.53.v20231009 to 9.4.54.v20240208

Updates `org.eclipse.jetty:jetty-servlet` from 9.4.53.v20231009 to 9.4.54.v20240208

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-14 03:57:50 +00:00
dependabot[bot] a0b1d6317c Bump org-aspectj from 1.9.20.1 to 1.9.21.1
Bumps `org-aspectj` from 1.9.20.1 to 1.9.21.1.

Updates `org.aspectj:aspectjrt` from 1.9.20.1 to 1.9.21.1
- [Release notes](https://github.com/eclipse/org.aspectj/releases)
- [Commits](https://github.com/eclipse/org.aspectj/commits)

Updates `org.aspectj:aspectjweaver` from 1.9.20.1 to 1.9.21.1
- [Release notes](https://github.com/eclipse/org.aspectj/releases)
- [Commits](https://github.com/eclipse/org.aspectj/commits)

---
updated-dependencies:
- dependency-name: org.aspectj:aspectjrt
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.aspectj:aspectjweaver
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-14 03:57:41 +00:00
Marcus Hert Da Coregio 731cd6131b Do not try to run trigger dependabot automerge workflow in PRs
Issue gh-14535
2024-02-13 10:45:08 -03:00
dependabot[bot] c2a589734c Bump spring-io/spring-github-workflows
Bumps [spring-io/spring-github-workflows](https://github.com/spring-io/spring-github-workflows) from eaf17a1890b1ef1b337f015d6eb263baaf8c6dab to 1e8b0587a1f4f01697f9753fa3339c3e0d30f396.
- [Release notes](https://github.com/spring-io/spring-github-workflows/releases)
- [Commits](https://github.com/spring-io/spring-github-workflows/compare/eaf17a1890b1ef1b337f015d6eb263baaf8c6dab...1e8b0587a1f4f01697f9753fa3339c3e0d30f396)

---
updated-dependencies:
- dependency-name: spring-io/spring-github-workflows
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 00:57:15 +00:00
dependabot[bot] e5e67f91e9 Bump Gamesight/slack-workflow-status from 1.2.0 to 1.3.0
Bumps [Gamesight/slack-workflow-status](https://github.com/gamesight/slack-workflow-status) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/gamesight/slack-workflow-status/releases)
- [Commits](https://github.com/gamesight/slack-workflow-status/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: Gamesight/slack-workflow-status
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 00:57:09 +00:00
Marcus Hert Da Coregio ccb2f06d0d Partially revert fc658d10
OpenIDAuthenticationFilter exists in versions < 6.0

Issue gh-14531
2024-02-07 10:13:34 -03:00
DingHao fc658d10d3 fix security filter sort in javadoc
Closes gh-14531
2024-02-07 09:37:01 -03:00
Marcus Hert Da Coregio e0fc8f37b0 Use latest version of spring-merge-dependabot-pr workflow
Issue gh-14535
2024-02-06 11:05:56 -03:00
Marcus Hert Da Coregio 43ced5291b Only run workflow for spring-projects repositories
Issue gh-14535
2024-02-06 08:18:31 -03:00
Marcus Hert Da Coregio 56c5fc281e Automatically trigger auto merge for Dependabot PRs
Issue gh-14535
2024-02-06 08:06:27 -03:00
Josh Cummings 7c3a6a567e Fix Compilation Errors
Issue gh-14525
2024-02-05 15:18:31 -07:00
Andreas Asplund 07e0b1dc37 Saml2 LogoutFilter Is Placed Before Common LogoutFilter
Closes gh-14525
2024-02-05 15:18:31 -07:00
Marcus Hert Da Coregio 05d3c4b695 Add permission to Edit Dependabot PR workflow
Issue gh-14486
2024-02-02 09:30:50 -03:00
Marcus Hert Da Coregio 5f80cfc705 Automatically assign milestone to Dependabot PR
Issue gh-14486
2024-02-02 09:30:39 -03:00
dependabot[bot] 7011930305 Bump gradle/gradle-build-action from 2 to 3
Bumps [gradle/gradle-build-action](https://github.com/gradle/gradle-build-action) from 2 to 3.
- [Release notes](https://github.com/gradle/gradle-build-action/releases)
- [Commits](https://github.com/gradle/gradle-build-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: gradle/gradle-build-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 17:44:51 -07:00
dependabot[bot] 8a75382b2d Bump slackapi/slack-github-action from 1.24.0 to 1.25.0
Bumps [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) from 1.24.0 to 1.25.0.
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Commits](https://github.com/slackapi/slack-github-action/compare/v1.24.0...v1.25.0)

---
updated-dependencies:
- dependency-name: slackapi/slack-github-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 17:41:43 -07:00
Hans Lindner ca10187fd1 Enhance JWT decoding error handling
Previously, the `decode` method threw a `JwtException` directly when encountering an unsupported algorithm or any exception during parsing. This commit introduces a more robust error handling mechanism. Now, instead of throwing exceptions directly, it returns a `Mono.error()` with a `BadJwtException` containing detailed error information. This approach provides more flexibility and allows the caller to handle errors in a more granular way, by being able to use project reactors onError functionality.

Closes gh-14467
2024-01-25 17:32:10 -07:00
dependabot[bot] 56f486588f Bump io.spring.ge.conventions from 0.0.14 to 0.0.15
Bumps [io.spring.ge.conventions](https://github.com/spring-io/gradle-enterprise-conventions) from 0.0.14 to 0.0.15.
- [Release notes](https://github.com/spring-io/gradle-enterprise-conventions/releases)
- [Commits](https://github.com/spring-io/gradle-enterprise-conventions/compare/v0.0.14...v0.0.15)

---
updated-dependencies:
- dependency-name: io.spring.ge.conventions
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-17 14:31:01 -03:00
dependabot[bot] 3f5f79d835 Bump io.projectreactor.netty:reactor-netty from 1.0.40 to 1.0.41
Bumps [io.projectreactor.netty:reactor-netty](https://github.com/reactor/reactor-netty) from 1.0.40 to 1.0.41.
- [Release notes](https://github.com/reactor/reactor-netty/releases)
- [Commits](https://github.com/reactor/reactor-netty/compare/v1.0.40...v1.0.41)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-12 09:52:45 -06:00
dependabot[bot] 9985580534 Bump io.projectreactor:reactor-bom from 2020.0.39 to 2020.0.40
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2020.0.39 to 2020.0.40.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2020.0.39...2020.0.40)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-12 09:42:04 -06:00
dependabot[bot] 24d4abe5fd Bump io-spring-javaformat from 0.0.40 to 0.0.41
Bumps `io-spring-javaformat` from 0.0.40 to 0.0.41.

Updates `io.spring.javaformat:spring-javaformat-checkstyle` from 0.0.40 to 0.0.41
- [Release notes](https://github.com/spring-io/spring-javaformat/releases)
- [Commits](https://github.com/spring-io/spring-javaformat/compare/v0.0.40...v0.0.41)

Updates `io.spring.javaformat:spring-javaformat-gradle-plugin` from 0.0.40 to 0.0.41
- [Release notes](https://github.com/spring-io/spring-javaformat/releases)
- [Commits](https://github.com/spring-io/spring-javaformat/compare/v0.0.40...v0.0.41)

---
updated-dependencies:
- dependency-name: io.spring.javaformat:spring-javaformat-checkstyle
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.spring.javaformat:spring-javaformat-gradle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-12 09:37:19 -06:00
Steve Riesenberg 16dc6be3c8 Update copyright year
Issue gh-14329
2023-12-28 12:54:29 -06:00
Geir Hedemark c88aaedb48 Updated broken documentation link in javadocs 2023-12-28 12:54:29 -06:00
daniKir 9203567a20 improve Multitenancy Issuer Validator example Closes gh-14229 2023-12-19 18:04:50 -07:00
Josh Cummings e058b559b8 Polish Method Security Eager-Loading
Issue gh-11596
2023-12-18 15:18:09 -07:00
Josh Cummings 33800c0124 Address eager-loading of infrastructure beans
Closes gh-11596
2023-12-18 14:25:48 -07:00
github-actions[bot] b71962e87d Next development version 2023-12-18 19:49:40 +00:00
github-actions[bot] b97c310aba Release 5.8.9 2023-12-18 19:12:02 +00:00
Josh Cummings 59461d94b0 Clarify RSocket Configuration Docs
Closes gh-13718
2023-12-18 12:02:49 -07:00
Josh Cummings fc007aa373 Check OpenSAML Version in XML Support
Closes gh-12483
2023-12-18 11:51:15 -07:00
Josh Cummings eaaa813ede Fix header value typo
Closes gh-11948
2023-12-18 10:42:50 -07:00
dependabot[bot] 10b3a9b234 Bump Gamesight/slack-workflow-status from 1.0.1 to 1.2.0
Bumps [Gamesight/slack-workflow-status](https://github.com/gamesight/slack-workflow-status) from 1.0.1 to 1.2.0.
- [Release notes](https://github.com/gamesight/slack-workflow-status/releases)
- [Commits](https://github.com/gamesight/slack-workflow-status/compare/v1.0.1...v1.2.0)

---
updated-dependencies:
- dependency-name: Gamesight/slack-workflow-status
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 08:58:50 -03:00
dependabot[bot] e4cc0a4755 Bump slackapi/slack-github-action from 1.19.0 to 1.24.0
Bumps [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) from 1.19.0 to 1.24.0.
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Commits](https://github.com/slackapi/slack-github-action/compare/v1.19.0...v1.24.0)

---
updated-dependencies:
- dependency-name: slackapi/slack-github-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 08:58:37 -03:00
dependabot[bot] ff71ef46b7 Bump actions/setup-java from 3 to 4
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 08:58:19 -03:00
dependabot[bot] 5593655bbf Bump spring-io/spring-gradle-build-action from 1 to 2
Bumps [spring-io/spring-gradle-build-action](https://github.com/spring-io/spring-gradle-build-action) from 1 to 2.
- [Commits](https://github.com/spring-io/spring-gradle-build-action/compare/v1...v2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 08:58:09 -03:00
dependabot[bot] 47812e506f Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [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/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 08:58:00 -03:00
dependabot[bot] 89fd30f235 Bump io.projectreactor.netty:reactor-netty from 1.0.39 to 1.0.40
Bumps [io.projectreactor.netty:reactor-netty](https://github.com/reactor/reactor-netty) from 1.0.39 to 1.0.40.
- [Release notes](https://github.com/reactor/reactor-netty/releases)
- [Commits](https://github.com/reactor/reactor-netty/compare/v1.0.39...v1.0.40)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-13 09:16:42 -03:00
dependabot[bot] c3c068376a Bump io.projectreactor:reactor-bom from 2020.0.38 to 2020.0.39
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2020.0.38 to 2020.0.39.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2020.0.38...2020.0.39)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-13 09:15:35 -03:00
Marcus Hert Da Coregio 6d68f403fc Document that Shibboleth Repository is Required for SAML Support
Closes gh-14286
2023-12-13 09:11:27 -03:00
Josh Cummings 74d06f020d Update to Latest Boot Property
Closes gh-14252
2023-12-11 11:44:08 -07:00
Candelario e896b14046 Dropped Nimbus Error Message
Closes gh-13730
2023-12-11 10:19:02 -07:00
Josh Cummings be11812fe4 Account for Super-super-interface Inheritance
Closes gh-13625
2023-12-09 11:41:02 -07:00
Steve Riesenberg 9c44b700f5 Add Gradle Enterprise creds to PR builds
Closes gh-14249
2023-12-07 14:28:51 -06:00
dependabot[bot] 40e8b20257 Bump ch.qos.logback:logback-classic from 1.2.12 to 1.2.13
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.2.12 to 1.2.13.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.2.12...v_1.2.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-04 08:51:58 -07:00
Josh Cummings c336ca49fb Update Spring MVC Docs
Closes gh-14220
2023-12-01 12:57:46 -07:00
Josh Cummings c623303ca5 Add Logging
Now if the ServletRegistration API available message is shown, it will
also be accompanied with a startup warning in the logs.

Closes gh-14221
2023-12-01 12:57:46 -07:00
Josh Cummings a98baa7522 Polish ServletRegistration API Deferral
Tomcat uses different ServletContext instances from startup- and request-time.
This commit ensures that if the programmatic API isn't available at startup-time,
then use the ServletContext attached to the HttpServletRequest at runtime.

Issue gh-13794
2023-12-01 12:57:45 -07:00
dependabot[bot] bb0987db80 Bump org.springframework.data:spring-data-bom
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2021.2.17 to 2021.2.18.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2021.2.17...2021.2.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 08:51:36 -03:00
Josh Cummings d961307044 Polish RequestMatcher Description
Issue gh-13794
2023-11-17 12:24:38 -07:00
Josh Cummings 4ca54683ae Defer requestMatchers Validation to Runtime
Closes gh-13794
2023-11-17 11:23:21 -07:00
Marcus Hert Da Coregio a7da9491d9 Use assertj assertions 2023-11-17 09:03:36 -03:00
dependabot[bot] 382cfd63ed Bump io-spring-javaformat from 0.0.39 to 0.0.40
Bumps `io-spring-javaformat` from 0.0.39 to 0.0.40.

Updates `io.spring.javaformat:spring-javaformat-checkstyle` from 0.0.39 to 0.0.40
- [Release notes](https://github.com/spring-io/spring-javaformat/releases)
- [Commits](https://github.com/spring-io/spring-javaformat/compare/v0.0.39...v0.0.40)

Updates `io.spring.javaformat:spring-javaformat-gradle-plugin` from 0.0.39 to 0.0.40
- [Release notes](https://github.com/spring-io/spring-javaformat/releases)
- [Commits](https://github.com/spring-io/spring-javaformat/compare/v0.0.39...v0.0.40)

---
updated-dependencies:
- dependency-name: io.spring.javaformat:spring-javaformat-checkstyle
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.spring.javaformat:spring-javaformat-gradle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-17 09:03:36 -03:00
dependabot[bot] 7c98a39770 Bump org.springframework:spring-framework-bom from 5.3.30 to 5.3.31
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 5.3.30 to 5.3.31.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v5.3.30...v5.3.31)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-17 09:03:29 -03:00
dependabot[bot] 6afcc02ef3 Bump io.projectreactor:reactor-bom from 2020.0.37 to 2020.0.38
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2020.0.37 to 2020.0.38.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2020.0.37...2020.0.38)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-16 08:35:55 -03:00
dependabot[bot] 773673ac2c Bump io.projectreactor.netty:reactor-netty from 1.0.38 to 1.0.39
Bumps [io.projectreactor.netty:reactor-netty](https://github.com/reactor/reactor-netty) from 1.0.38 to 1.0.39.
- [Release notes](https://github.com/reactor/reactor-netty/releases)
- [Commits](https://github.com/reactor/reactor-netty/compare/v1.0.38...v1.0.39)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-16 08:35:05 -03:00
Steve Riesenberg 7335c5745c Document authentication helper method in WebClient integration
This commit re-applies 49f3c0ce53
which was lost while splitting pages for Antora.

Issue gh-13816
Issue gh-10120
2023-11-09 10:46:32 -06:00
Josh Cummings 52675c80b3 Check For Null Exception Message
Closes gh-13768
2023-11-07 17:19:35 -07:00
Josh Cummings b919ece045 Change Idempotent to Read-Only
Closes gh-13644
2023-11-07 16:25:28 -07:00
Josh Cummings 11a21896dd Defer SecurityContextHolderStrategy Lookup
Due to how early method interceptors are loaded during startup
it's reasonable to consider scenarios where applications are
changing the global security context holder strategy during
startup.

Closes gh-12877
2023-11-07 12:36:16 -07:00
Marcus Hert Da Coregio eff9814d7b Add links to WebFlux section where referenced
Closes gh-14100
2023-11-07 13:13:41 -03:00
Dong, Xue-Han 058495d463 Add the Authorization Section to Nav List
Closes gh-14099
2023-11-07 13:11:52 -03:00
Josh Cummings ffd12ee3b9 Refine requestMatcher Validation Rules
Closes gh-14078
2023-10-31 17:08:24 -06:00
Marcus Hert Da Coregio 3f64c6d745 Use version catalog to resolve nimbus dependency versions
Issue gh-14047
2023-10-30 16:11:51 -03:00
Marcus Hert Da Coregio 2cd302fb1b Revert "Use lenient configuration for prohibited dependencies check"
This reverts commit 602d4189d1.
2023-10-30 16:11:02 -03:00
Marcus Hert Da Coregio e8ec49b4c5 Resolve versions without downloading dependencies
Issue gh-14047
2023-10-30 14:46:12 -03:00
Marcus Hert Da Coregio 602d4189d1 Use lenient configuration for prohibited dependencies check
Issue gh-14047
2023-10-30 12:10:32 -03:00
Marcus Hert Da Coregio 884014c2fb Use lenient configuration to resolve artifacts
Issue gh-14047
2023-10-30 11:25:30 -03:00
Marcus Hert Da Coregio 47969ec7c9 Fix verifyDependenciesVersions not working with s101
Issue gh-14047
2023-10-30 08:28:15 -03:00
Marcus Da Coregio 6654479649 Polish VerifyDependenciesVersionsPlugin
Issue gh-14047
2023-10-27 09:45:19 -03:00
Steve Riesenberg bfc31bacab Polish Username/Password Authentication page
Issue gh-11926

(cherry picked from commit 781d575921)
2023-10-25 15:50:53 -05:00
Marcus Da Coregio ad2bea3350 Fail build if nimbus-jose-jwt version is not aligned
Closes gh-14047
2023-10-25 13:44:23 -03:00
Steve Riesenberg 5161712c35 Polish gh-13976
Closes gh-13757
2023-10-19 16:40:23 -05:00
Veli Döngelci a6b872dcf3 Fix caching error state in ReactiveRemoteJWKSource 2023-10-19 16:40:13 -05:00
Marcus Da Coregio 70ad3bf749 relay_state should not be included in signing calculation when it is null
Closes gh-13913
2023-10-19 09:58:47 -03:00
Scott Shidlovsky 19c4e427ee Update OpenSamlAuthenticationRequestResolverTests from Junit 4 to Junit 5
(cherry picked from commit 508f7d7b8a)
2023-10-19 09:50:28 -03:00
Marcus Da Coregio 736f1c27cd Remove updateDependencies plugin
Closes gh-13966
2023-10-18 14:12:25 -03:00
github-actions[bot] ec58bf0b28 Next development version 2023-10-16 16:06:01 +00:00
92 changed files with 1430 additions and 1288 deletions
@@ -31,7 +31,7 @@ jobs:
runjobs: ${{ steps.continue.outputs.runjobs }}
project_version: ${{ steps.continue.outputs.project_version }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- id: continue
name: Determine if should continue
run: |
@@ -49,15 +49,15 @@ jobs:
runs-on: ${{ matrix.os }}
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
cache: 'gradle'
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
- name: Set up gradle user name
run: echo 'systemProp.user.name=spring-builds+github' >> gradle.properties
- name: Build with Gradle
@@ -72,9 +72,9 @@ jobs:
runs-on: ubuntu-latest
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -90,9 +90,9 @@ jobs:
runs-on: ubuntu-latest
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -114,9 +114,9 @@ jobs:
runs-on: ubuntu-latest
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -131,9 +131,9 @@ jobs:
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -155,9 +155,9 @@ jobs:
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -176,9 +176,9 @@ jobs:
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -206,11 +206,11 @@ jobs:
TOKEN: ${{ github.token }}
VERSION: ${{ needs.prerequisites.outputs.project_version }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -243,7 +243,7 @@ jobs:
./gradlew createGitHubRelease -PnextVersion=$VERSION -Pbranch=$BRANCH -PcreateRelease=true -PgitHubAccessToken=$TOKEN
- name: Announce Release on Slack
id: spring-security-announcing
uses: slackapi/slack-github-action@v1.19.0
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
@@ -287,9 +287,9 @@ jobs:
TOKEN: ${{ github.token }}
VERSION: ${{ needs.prerequisites.outputs.project_version }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
if: github.repository_owner == 'spring-projects'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: docs-build
fetch-depth: 1
+52
View File
@@ -0,0 +1,52 @@
name: Merge Dependabot PR
on: pull_request_target
run-name: Merge Dependabot PR ${{ github.ref_name }}
permissions: write-all
jobs:
merge-dependabot-pr:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- uses: actions/checkout@v4
with:
show-progress: false
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Set Milestone to Dependabot Pull Request
id: set-milestone
run: |
if test -f pom.xml
then
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
else
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
fi
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
if [ -z $MILESTONE ]
then
gh run cancel ${{ github.run_id }}
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
else
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
echo mergeEnabled=true >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Dependabot pull request
if: steps.set-milestone.outputs.mergeEnabled
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
@@ -32,7 +32,7 @@ jobs:
actions: read
steps:
- name: Send Slack message
uses: Gamesight/slack-workflow-status@v1.0.1
uses: Gamesight/slack-workflow-status@v1.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
+7 -2
View File
@@ -2,6 +2,11 @@ name: PR Build
on: pull_request
env:
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
permissions:
contents: read
@@ -11,9 +16,9 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -0,0 +1,22 @@
name: Trigger Dependabot Auto Merge Forward
on:
push:
branches:
- '*.x'
permissions: read-all
jobs:
trigger-worflow:
name: Trigger Workflow
runs-on: ubuntu-latest
if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- id: trigger
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run dependabot-auto-merge-forward.yml -r main
@@ -23,11 +23,11 @@ jobs:
steps:
- id: checkout-source
name: Checkout Source Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
@@ -72,7 +72,7 @@ jobs:
- id: send-slack-notification
name: Send Slack message
if: failure()
uses: Gamesight/slack-workflow-status@v1.0.1
uses: Gamesight/slack-workflow-status@v1.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
+2 -53
View File
@@ -19,12 +19,12 @@ apply plugin: 'locks'
apply plugin: 's101'
apply plugin: 'io.spring.convention.root'
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'org.springframework.security.update-dependencies'
apply plugin: 'org.springframework.security.update-version'
apply plugin: 'org.springframework.security.sagan'
apply plugin: 'org.springframework.github.milestone'
apply plugin: 'org.springframework.github.changelog'
apply plugin: 'org.springframework.github.release'
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
group = 'org.springframework.security'
description = 'Spring Security'
@@ -86,58 +86,6 @@ tasks.named("dispatchGitHubWorkflow") {
}
}
tasks.named("updateDependencies") {
// we aren't Gradle 7 compatible yet
checkForGradleUpdate = false
}
updateDependenciesSettings {
gitHub {
organization = "spring-projects"
repository = "spring-security"
}
addFiles({
return [
project.file("buildSrc/src/main/groovy/io/spring/gradle/convention/CheckstylePlugin.groovy")
]
})
dependencyExcludes {
majorVersionBump()
minorVersionBump()
releaseCandidatesVersions()
alphaBetaVersions()
snapshotVersions()
addRule { components ->
components.withModule("org.python:jython") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("jython updates break integration tests");
}
}
components.withModule("com.nimbusds:nimbus-jose-jwt") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency");
}
}
components.withModule("io.mockk:mockk") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("mockk updates break tests");
}
}
components.all { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
// Do not compare version due to multiple versions existing
// will cause opensaml 3.x to be updated to 4.x
if (candidate.getGroup().equals("org.opensaml")) {
selection.reject("org.opensaml maintains two different versions, so it must be updated manually");
}
}
}
}
}
subprojects {
plugins.withType(JavaPlugin) {
project.sourceCompatibility='1.8'
@@ -200,5 +148,6 @@ tasks.register('cloneSamples', IncludeRepoTask) {
}
s101 {
repository = 'https://structure101.com/binaries/latest'
configurationDirectory = project.file("etc/s101")
}
+5 -6
View File
@@ -2,7 +2,6 @@ plugins {
id "java-gradle-plugin"
id "java"
id "groovy"
id 'com.apollographql.apollo' version '2.4.5'
}
sourceCompatibility = 1.8
@@ -41,10 +40,6 @@ gradlePlugin {
id = "io.spring.convention.management-configuration"
implementationClass = "io.spring.gradle.convention.ManagementConfigurationPlugin"
}
updateDependencies {
id = "org.springframework.security.update-dependencies"
implementationClass = "org.springframework.security.convention.versions.UpdateDependenciesPlugin"
}
updateProjectVersion {
id = "org.springframework.security.update-version"
implementationClass = "org.springframework.security.convention.versions.UpdateProjectVersionPlugin"
@@ -69,6 +64,10 @@ gradlePlugin {
id = "s101"
implementationClass = "s101.S101Plugin"
}
verifyDependenciesVersions {
id = "org.springframework.security.versions.verify-dependencies-versions"
implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin"
}
}
}
@@ -90,7 +89,6 @@ dependencies {
implementation libs.io.github.gradle.nexus.publish.plugin
implementation 'io.projectreactor:reactor-core'
implementation libs.org.gretty.gretty
implementation libs.com.apollographql.apollo.apollo.runtime
implementation libs.com.github.ben.manes.gradle.versions.plugin
implementation libs.com.github.spullara.mustache.java.compiler
implementation libs.io.spring.javaformat.spring.javaformat.gradle.plugin
@@ -99,6 +97,7 @@ dependencies {
implementation libs.org.hidetake.gradle.ssh.plugin
implementation libs.org.jfrog.buildinfo.build.info.extractor.gradle
implementation libs.org.sonarsource.scanner.gradle.sonarqube.gradle.plugin
implementation libs.com.squareup.okhttp3.okhttp
testImplementation platform(libs.org.junit.junit.bom)
testImplementation platform(libs.org.mockito.mockito.bom)
@@ -1,49 +0,0 @@
/*
* Copyright 2019-2022 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.convention.versions;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Scanner;
class CommandLineUtils {
static void runCommand(File dir, String... args) {
try {
Process process = new ProcessBuilder()
.directory(dir)
.command(args)
.start();
writeLinesTo(process.getInputStream(), System.out);
writeLinesTo(process.getErrorStream(), System.out);
if (process.waitFor() != 0) {
new RuntimeException("Failed to run " + Arrays.toString(args));
}
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Failed to run " + Arrays.toString(args), e);
}
}
private static void writeLinesTo(InputStream input, PrintStream out) {
Scanner scanner = new Scanner(input);
while(scanner.hasNextLine()) {
out.println(scanner.nextLine());
}
}
}
@@ -1,179 +0,0 @@
package org.springframework.security.convention.versions;
import com.apollographql.apollo.ApolloCall;
import com.apollographql.apollo.ApolloClient;
import com.apollographql.apollo.api.Input;
import com.apollographql.apollo.api.Response;
import com.apollographql.apollo.exception.ApolloException;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import reactor.util.retry.RetrySpec;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class GitHubApi {
private final ApolloClient apolloClient;
public GitHubApi(String githubToken) {
if (githubToken == null) {
throw new IllegalArgumentException("githubToken is required. You can set it using -PgitHubAccessToken=");
}
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
clientBuilder.addInterceptor(new AuthorizationInterceptor(githubToken));
this.apolloClient = ApolloClient.builder()
.serverUrl("https://api.github.com/graphql")
.okHttpClient(clientBuilder.build())
.build();
}
public Mono<FindCreateIssueResult> findCreateIssueInput(String owner, String name, String milestone) {
String label = "\"type: dependency-upgrade\"";
FindCreateIssueInputQuery findCreateIssueInputQuery = new FindCreateIssueInputQuery(owner, name, Input.optional(label), Input.optional(milestone));
return Mono.create( sink -> this.apolloClient.query(findCreateIssueInputQuery)
.enqueue(new ApolloCall.Callback<FindCreateIssueInputQuery.Data>() {
@Override
public void onResponse(@NotNull Response<FindCreateIssueInputQuery.Data> response) {
if (response.hasErrors()) {
sink.error(new RuntimeException(response.getErrors().stream().map(e -> e.getMessage()).collect(Collectors.joining(" "))));
} else {
FindCreateIssueInputQuery.Data data = response.getData();
FindCreateIssueInputQuery.Repository repository = data.repository();
List<String> labels = repository.labels().nodes().stream().map(FindCreateIssueInputQuery.Node::id).collect(Collectors.toList());
if (labels.isEmpty()) {
sink.error(new IllegalArgumentException("Could not find label for " + label));
return;
}
Optional<String> firstMilestoneId = repository.milestones().nodes().stream().map(FindCreateIssueInputQuery.Node1::id).findFirst();
if (!firstMilestoneId.isPresent()) {
sink.error(new IllegalArgumentException("Could not find OPEN milestone id for " + milestone));
return;
}
String milestoneId = firstMilestoneId.get();
String repositoryId = repository.id();
String assigneeId = data.viewer().id();
sink.success(new FindCreateIssueResult(repositoryId, labels, milestoneId, assigneeId));
}
}
@Override
public void onFailure(@NotNull ApolloException e) {
sink.error(e);
}
}));
}
public static class FindCreateIssueResult {
private final String repositoryId;
private final List<String> labelIds;
private final String milestoneId;
private final String assigneeId;
public FindCreateIssueResult(String repositoryId, List<String> labelIds, String milestoneId, String assigneeId) {
this.repositoryId = repositoryId;
this.labelIds = labelIds;
this.milestoneId = milestoneId;
this.assigneeId = assigneeId;
}
public String getRepositoryId() {
return repositoryId;
}
public List<String> getLabelIds() {
return labelIds;
}
public String getMilestoneId() {
return milestoneId;
}
public String getAssigneeId() {
return assigneeId;
}
}
public Mono<RateLimitQuery.RateLimit> findRateLimit() {
return Mono.create( sink -> this.apolloClient.query(new RateLimitQuery())
.enqueue(new ApolloCall.Callback<RateLimitQuery.Data>() {
@Override
public void onResponse(@NotNull Response<RateLimitQuery.Data> response) {
if (response.hasErrors()) {
sink.error(new RuntimeException(response.getErrors().stream().map(e -> e.getMessage()).collect(Collectors.joining(" "))));
} else {
sink.success(response.getData().rateLimit());
}
}
@Override
public void onFailure(@NotNull ApolloException e) {
sink.error(e);
}
}));
}
public Mono<Integer> createIssue(String repositoryId, String title, List<String> labelIds, String milestoneId, String assigneeId) {
CreateIssueInputMutation createIssue = new CreateIssueInputMutation.Builder()
.repositoryId(repositoryId)
.title(title)
.labelIds(labelIds)
.milestoneId(milestoneId)
.assigneeId(assigneeId)
.build();
return Mono.create( sink -> this.apolloClient.mutate(createIssue)
.enqueue(new ApolloCall.Callback<CreateIssueInputMutation.Data>() {
@Override
public void onResponse(@NotNull Response<CreateIssueInputMutation.Data> response) {
if (response.hasErrors()) {
String message = response.getErrors().stream().map(e -> e.getMessage() + " " + e.getCustomAttributes() + " " + e.getLocations()).collect(Collectors.joining(" "));
if (message.contains("was submitted too quickly")) {
sink.error(new SubmittedTooQuick(message));
} else {
sink.error(new RuntimeException(message));
}
} else {
sink.success(response.getData().createIssue().issue().number());
}
}
@Override
public void onFailure(@NotNull ApolloException e) {
sink.error(e);
}
}))
.retryWhen(
RetrySpec.fixedDelay(3, Duration.ofMinutes(1))
.filter(SubmittedTooQuick.class::isInstance)
.doBeforeRetry(r -> System.out.println("Pausing for 1 minute and then retrying due to receiving \"submitted too quickly\" error from GitHub API"))
)
.cast(Integer.class);
}
public static class SubmittedTooQuick extends RuntimeException {
public SubmittedTooQuick(String message) {
super(message);
}
}
private static class AuthorizationInterceptor implements Interceptor {
private final String token;
public AuthorizationInterceptor(String token) {
this.token = token;
}
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + this.token).build();
return chain.proceed(request);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -1,198 +0,0 @@
package org.springframework.security.convention.versions;
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionRulesWithCurrent;
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ResolutionStrategyWithCurrent;
import org.gradle.api.Action;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import java.util.regex.Pattern;
public class UpdateDependenciesExtension {
private Supplier<List<File>> files;
private UpdateMode updateMode = UpdateMode.COMMIT;
private DependencyExcludes dependencyExcludes = new DependencyExcludes();
private GitHub gitHub = new GitHub();
public UpdateDependenciesExtension(Supplier<List<File>> files) {
this.files = files;
}
public void setUpdateMode(UpdateMode updateMode) {
this.updateMode = updateMode;
}
public UpdateMode getUpdateMode() {
return updateMode;
}
GitHub getGitHub() {
return this.gitHub;
}
DependencyExcludes getExcludes() {
return dependencyExcludes;
}
Supplier<List<File>> getFiles() {
return files;
}
public void setFiles(Supplier<List<File>> files) {
this.files = files;
}
public void addFiles(Supplier<List<File>> files) {
Supplier<List<File>> original = this.files;
setFiles(() -> {
List<File> result = new ArrayList<>(original.get());
result.addAll(files.get());
return result;
});
}
public void dependencyExcludes(Action<DependencyExcludes> excludes) {
excludes.execute(this.dependencyExcludes);
}
public void gitHub(Action<GitHub> gitHub) {
gitHub.execute(this.gitHub);
}
public enum UpdateMode {
COMMIT,
GITHUB_ISSUE
}
public class GitHub {
private String organization;
private String repository;
private String accessToken;
private String milestone;
public String getOrganization() {
return organization;
}
public void setOrganization(String organization) {
this.organization = organization;
}
public String getRepository() {
return repository;
}
public void setRepository(String repository) {
this.repository = repository;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getMilestone() {
return milestone;
}
public void setMilestone(String milestone) {
this.milestone = milestone;
}
}
/**
* Consider creating some Predicates instead since they are composible
*/
public class DependencyExcludes {
private List<Action<ComponentSelectionWithCurrent>> actions = new ArrayList<>();
private List<Action<ComponentSelectionRulesWithCurrent>> components = new ArrayList<>();
List<Action<ComponentSelectionWithCurrent>> getActions() {
return actions;
}
public List<Action<ComponentSelectionRulesWithCurrent>> getComponents() {
return components;
}
public DependencyExcludes alphaBetaVersions() {
this.actions.add(excludeVersionWithRegex("(?i).*?(alpha|beta).*", "an alpha or beta version"));
return this;
}
public DependencyExcludes majorVersionBump() {
this.actions.add((selection) -> {
String currentVersion = selection.getCurrentVersion();
int separator = currentVersion.indexOf(".");
String major = separator > 0 ? currentVersion.substring(0, separator) : currentVersion;
String candidateVersion = selection.getCandidate().getVersion();
Pattern calVerPattern = Pattern.compile("\\d\\d\\d\\d.*");
boolean isCalVer = calVerPattern.matcher(candidateVersion).matches();
if (!isCalVer && !candidateVersion.startsWith(major)) {
selection.reject("Cannot upgrade to new Major Version");
}
});
return this;
}
public DependencyExcludes minorVersionBump() {
this.actions.add(createExcludeMinorVersionBump());
return this;
}
public Action<ComponentSelectionWithCurrent> createExcludeMinorVersionBump() {
return (selection) -> {
String currentVersion = selection.getCurrentVersion();
int majorSeparator = currentVersion.indexOf(".");
int separator = currentVersion.indexOf(".", majorSeparator + 1);
String majorMinor = separator > 0 ? currentVersion.substring(0, separator) : currentVersion;
String candidateVersion = selection.getCandidate().getVersion();
if (!candidateVersion.startsWith(majorMinor)) {
selection.reject("Cannot upgrade to new Minor Version");
}
};
}
public DependencyExcludes releaseCandidatesVersions() {
this.actions.add(excludeVersionWithRegex("(?i).*?rc.*", "a release candidate version"));
return this;
}
public DependencyExcludes milestoneVersions() {
this.actions.add(excludeVersionWithRegex("(?i).*?m\\d+.*", "a milestone version"));
return this;
}
public DependencyExcludes snapshotVersions() {
this.actions.add(excludeVersionWithRegex(".*?-SNAPSHOT.*", "a SNAPSHOT version"));
return this;
}
public DependencyExcludes addRule(Action<ComponentSelectionRulesWithCurrent> rule) {
this.components.add(rule);
return this;
}
private Action<ComponentSelectionWithCurrent> excludeVersionWithRegex(String regex, String reason) {
Pattern pattern = Pattern.compile(regex);
return (selection) -> {
String candidateVersion = selection.getCandidate().getVersion();
if (pattern.matcher(candidateVersion).matches()) {
selection.reject(candidateVersion + " is not allowed because it is " + reason);
}
};
}
}
}
@@ -1,244 +0,0 @@
/*
* Copyright 2019-2020 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.convention.versions;
import com.github.benmanes.gradle.versions.reporter.result.Dependency;
import com.github.benmanes.gradle.versions.reporter.result.DependencyOutdated;
import com.github.benmanes.gradle.versions.reporter.result.Result;
import com.github.benmanes.gradle.versions.reporter.result.VersionAvailable;
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask;
import com.github.benmanes.gradle.versions.updates.gradle.GradleUpdateResult;
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionRulesWithCurrent;
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ResolutionStrategyWithCurrent;
import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import reactor.core.publisher.Mono;
import java.io.File;
import java.time.Duration;
import java.util.*;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.springframework.security.convention.versions.TransitiveDependencyLookupUtils.NIMBUS_JOSE_JWT_NAME;
import static org.springframework.security.convention.versions.TransitiveDependencyLookupUtils.OIDC_SDK_NAME;
public class UpdateDependenciesPlugin implements Plugin<Project> {
private GitHubApi gitHubApi;
@Override
public void apply(Project project) {
UpdateDependenciesExtension updateDependenciesSettings = project.getExtensions().create("updateDependenciesSettings", UpdateDependenciesExtension.class, defaultFiles(project));
if (project.hasProperty("updateMode")) {
String updateMode = String.valueOf(project.findProperty("updateMode"));
updateDependenciesSettings.setUpdateMode(UpdateDependenciesExtension.UpdateMode.valueOf(updateMode));
}
if (project.hasProperty("nextVersion")) {
String nextVersion = String.valueOf(project.findProperty("nextVersion"));
updateDependenciesSettings.getGitHub().setMilestone(nextVersion);
}
if (project.hasProperty("gitHubAccessToken")) {
String gitHubAccessToken = String.valueOf(project.findProperty("gitHubAccessToken"));
updateDependenciesSettings.getGitHub().setAccessToken(gitHubAccessToken);
}
project.getTasks().register("updateDependencies", DependencyUpdatesTask.class, new Action<DependencyUpdatesTask>() {
@Override
public void execute(DependencyUpdatesTask updateDependencies) {
updateDependencies.setDescription("Update the dependencies");
updateDependencies.setCheckConstraints(true);
updateDependencies.setOutputFormatter(new Closure<Void>(null) {
@Override
public Void call(Object argument) {
Result result = (Result) argument;
if (gitHubApi == null && updateDependenciesSettings.getUpdateMode() != UpdateDependenciesExtension.UpdateMode.COMMIT) {
gitHubApi = new GitHubApi(updateDependenciesSettings.getGitHub().getAccessToken());
}
updateDependencies(result, project, updateDependenciesSettings);
updateGradleVersion(result, project, updateDependenciesSettings);
return null;
}
});
updateDependencies.resolutionStrategy(new Action<ResolutionStrategyWithCurrent>() {
@Override
public void execute(ResolutionStrategyWithCurrent resolution) {
resolution.componentSelection(new Action<ComponentSelectionRulesWithCurrent>() {
@Override
public void execute(ComponentSelectionRulesWithCurrent components) {
updateDependenciesSettings.getExcludes().getActions().forEach((action) -> {
components.all(action);
});
updateDependenciesSettings.getExcludes().getComponents().forEach((action) -> {
action.execute(components);
});
components.all((selection) -> {
ModuleComponentIdentifier candidate = selection.getCandidate();
if (candidate.getGroup().startsWith("org.apache.directory.") && !candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("org.apache.directory.* has breaking changes in newer versions");
}
});
String jaxbBetaRegex = ".*?b\\d+.*";
components.withModule("javax.xml.bind:jaxb-api", excludeWithRegex(jaxbBetaRegex, "Reject jaxb-api beta versions"));
components.withModule("com.sun.xml.bind:jaxb-impl", excludeWithRegex(jaxbBetaRegex, "Reject jaxb-api beta versions"));
components.withModule("commons-collections:commons-collections", excludeWithRegex("^\\d{3,}.*", "Reject commons-collections date based releases"));
}
});
}
});
}
});
}
private void updateDependencies(Result result, Project project, UpdateDependenciesExtension updateDependenciesSettings) {
SortedSet<DependencyOutdated> dependencies = result.getOutdated().getDependencies();
if (dependencies.isEmpty()) {
return;
}
Map<String, List<DependencyOutdated>> groups = new LinkedHashMap<>();
dependencies.forEach(outdated -> {
groups.computeIfAbsent(outdated.getGroup(), (key) -> new ArrayList<>()).add(outdated);
});
List<DependencyOutdated> nimbusds = groups.getOrDefault("com.nimbusds", new ArrayList<>());
DependencyOutdated oidcSdc = nimbusds.stream().filter(d -> d.getName().equals(OIDC_SDK_NAME)).findFirst().orElseGet(() -> null);
if(oidcSdc != null) {
String oidcVersion = updatedVersion(oidcSdc);
String jwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(oidcVersion);
Dependency nimbusJoseJwtDependency = result.getCurrent().getDependencies().stream().filter(d -> d.getName().equals(NIMBUS_JOSE_JWT_NAME)).findFirst().get();
DependencyOutdated outdatedJwt = new DependencyOutdated();
outdatedJwt.setVersion(nimbusJoseJwtDependency.getVersion());
outdatedJwt.setGroup(oidcSdc.getGroup());
outdatedJwt.setName(NIMBUS_JOSE_JWT_NAME);
VersionAvailable available = new VersionAvailable();
available.setRelease(jwtVersion);
outdatedJwt.setAvailable(available);
nimbusds.add(outdatedJwt);
}
File gradlePropertiesFile = project.getRootProject().file(Project.GRADLE_PROPERTIES);
Mono<GitHubApi.FindCreateIssueResult> createIssueResult = createIssueResultMono(updateDependenciesSettings);
List<File> filesWithDependencies = updateDependenciesSettings.getFiles().get();
groups.forEach((group, outdated) -> {
outdated.forEach((dependency) -> {
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
String originalDependency = ga + dependency.getVersion();
String replacementDependency = ga + updatedVersion(dependency);
System.out.println("Update " + originalDependency + " to " + replacementDependency);
filesWithDependencies.forEach((fileWithDependency) -> {
updateDependencyInlineVersion(fileWithDependency, dependency);
updateDependencyWithVersionVariable(fileWithDependency, gradlePropertiesFile, dependency);
});
});
// commit
DependencyOutdated firstDependency = outdated.get(0);
String updatedVersion = updatedVersion(firstDependency);
String title = outdated.size() == 1 ? "Update " + firstDependency.getName() + " to " + updatedVersion : "Update " + firstDependency.getGroup() + " to " + updatedVersion;
afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResult);
});
}
private void afterGroup(UpdateDependenciesExtension updateDependenciesExtension, File rootDir, String title, Mono<GitHubApi.FindCreateIssueResult> createIssueResultMono) {
String commitMessage = title;
if (updateDependenciesExtension.getUpdateMode() == UpdateDependenciesExtension.UpdateMode.GITHUB_ISSUE) {
GitHubApi.FindCreateIssueResult createIssueResult = createIssueResultMono.block();
Integer issueNumber = gitHubApi.createIssue(createIssueResult.getRepositoryId(), title, createIssueResult.getLabelIds(), createIssueResult.getMilestoneId(), createIssueResult.getAssigneeId()).delayElement(Duration.ofSeconds(1)).block();
commitMessage += "\n\nCloses gh-" + issueNumber;
}
CommandLineUtils.runCommand(rootDir, "git", "commit", "-am", commitMessage);
}
private Mono<GitHubApi.FindCreateIssueResult> createIssueResultMono(UpdateDependenciesExtension updateDependenciesExtension) {
return Mono.defer(() -> {
UpdateDependenciesExtension.GitHub gitHub = updateDependenciesExtension.getGitHub();
return gitHubApi.findCreateIssueInput(gitHub.getOrganization(), gitHub.getRepository(), gitHub.getMilestone()).cache();
});
}
private void updateGradleVersion(Result result, Project project, UpdateDependenciesExtension updateDependenciesSettings) {
if (!result.getGradle().isEnabled()) {
return;
}
GradleUpdateResult current = result.getGradle().getCurrent();
GradleUpdateResult running = result.getGradle().getRunning();
if (current.compareTo(running) > 0) {
String title = "Update Gradle to " + current.getVersion();
System.out.println(title);
CommandLineUtils.runCommand(project.getRootDir(), "./gradlew", "wrapper", "--gradle-version", current.getVersion(), "--no-daemon");
afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResultMono(updateDependenciesSettings));
}
}
private static Supplier<List<File>> defaultFiles(Project project) {
return () -> {
List<File> result = new ArrayList<>();
result.add(project.getBuildFile());
project.getChildProjects().values().forEach((childProject) ->
result.add(childProject.getBuildFile())
);
result.add(project.getRootProject().file("buildSrc/build.gradle"));
return result;
};
}
static Action<ComponentSelectionWithCurrent> excludeWithRegex(String regex, String reason) {
Pattern pattern = Pattern.compile(regex);
return (selection) -> {
String candidateVersion = selection.getCandidate().getVersion();
if (pattern.matcher(candidateVersion).matches()) {
selection.reject(candidateVersion + " is not allowed because it is " + reason);
}
};
}
static void updateDependencyInlineVersion(File buildFile, DependencyOutdated dependency){
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
String originalDependency = ga + dependency.getVersion();
String replacementDependency = ga + updatedVersion(dependency);
FileUtils.replaceFileText(buildFile, buildFileText -> buildFileText.replace(originalDependency, replacementDependency));
}
static void updateDependencyWithVersionVariable(File scanFile, File gradlePropertiesFile, DependencyOutdated dependency) {
if (!gradlePropertiesFile.exists()) {
return;
}
FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> {
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
Pattern pattern = Pattern.compile("\"" + ga + "\\$\\{?([^'\"]+?)\\}?\"");
String buildFileText = FileUtils.readString(scanFile);
Matcher matcher = pattern.matcher(buildFileText);
while (matcher.find()) {
String versionVariable = matcher.group(1);
gradlePropertiesText = gradlePropertiesText.replace(versionVariable + "=" + dependency.getVersion(), versionVariable + "=" + updatedVersion(dependency));
}
return gradlePropertiesText;
});
}
private static String updatedVersion(DependencyOutdated dependency) {
VersionAvailable available = dependency.getAvailable();
String release = available.getRelease();
if (release != null) {
return release;
}
return available.getMilestone();
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2002-2023 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.convention.versions;
import org.gradle.api.DefaultTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
import org.gradle.api.artifacts.VersionCatalog;
import org.gradle.api.artifacts.VersionCatalogsExtension;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskProvider;
public class VerifyDependenciesVersionsPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
TaskProvider<VerifyDependenciesVersionsTask> verifyDependenciesVersionsTaskProvider = project.getTasks().register("verifyDependenciesVersions", VerifyDependenciesVersionsTask.class, (task) -> {
task.setGroup("Verification");
task.setDescription("Verify that specific dependencies are using the same version");
VersionCatalog versionCatalog = project.getExtensions().getByType(VersionCatalogsExtension.class).named("libs");
MinimalExternalModuleDependency oauth2OidcSdk = versionCatalog.findLibrary("com-nimbusds-oauth2-oidc-sdk").get().get();
MinimalExternalModuleDependency nimbusJoseJwt = versionCatalog.findLibrary("com-nimbusds-nimbus-jose-jwt").get().get();
task.setOauth2OidcSdkVersion(oauth2OidcSdk.getVersionConstraint().getDisplayName());
task.setExpectedNimbusJoseJwtVersion(nimbusJoseJwt.getVersionConstraint().getDisplayName());
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(verifyDependenciesVersionsTaskProvider));
}
public static class VerifyDependenciesVersionsTask extends DefaultTask {
private String oauth2OidcSdkVersion;
private String expectedNimbusJoseJwtVersion;
public void setOauth2OidcSdkVersion(String oauth2OidcSdkVersion) {
this.oauth2OidcSdkVersion = oauth2OidcSdkVersion;
}
public void setExpectedNimbusJoseJwtVersion(String expectedNimbusJoseJwtVersion) {
this.expectedNimbusJoseJwtVersion = expectedNimbusJoseJwtVersion;
}
@TaskAction
public void verify() {
String transitiveNimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(this.oauth2OidcSdkVersion);
if (!transitiveNimbusJoseJwtVersion.equals(this.expectedNimbusJoseJwtVersion)) {
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, but the project contains a different version of nimbus-jose-jwt [%s]. Please align the versions.", transitiveNimbusJoseJwtVersion, this.oauth2OidcSdkVersion, this.expectedNimbusJoseJwtVersion);
throw new IllegalStateException(message);
}
}
}
}
+11 -46
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
package s101;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -34,18 +33,11 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
@@ -71,6 +63,10 @@ public class S101Configurer {
private final Path licenseDirectory;
private final String repository;
private final String version;
private final Project project;
private final Logger logger;
@@ -90,6 +86,9 @@ public class S101Configurer {
throw new UncheckedIOException(ex);
}
this.licenseDirectory = new File(System.getProperty("user.home") + "/.Structure101/java").toPath();
S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class);
this.repository = extension.getRepository().get();
this.version = extension.getVersion().get();
}
public void license(String licenseId) {
@@ -129,25 +128,7 @@ public class S101Configurer {
public void configure(File installationDirectory, File configurationDirectory) {
deleteDirectory(configurationDirectory);
String version = computeVersionFromInstallation(installationDirectory);
configureProject(version, configurationDirectory);
}
private String computeVersionFromInstallation(File installationDirectory) {
File buildJar = new File(installationDirectory, "structure101-java-build.jar");
try (JarInputStream input = new JarInputStream(new FileInputStream(buildJar))) {
JarEntry entry;
while ((entry = input.getNextJarEntry()) != null) {
if (entry.getName().contains("structure101-build.properties")) {
Properties properties = new Properties();
properties.load(input);
return properties.getProperty("s101-build");
}
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
throw new IllegalStateException("Unable to determine Structure101 version");
configureProject(this.version, configurationDirectory);
}
private boolean deleteDirectory(File directoryToBeDeleted) {
@@ -161,24 +142,8 @@ public class S101Configurer {
}
private String installBuildTool(File installationDirectory, File configurationDirectory) {
String source = "https://structure101.com/binaries/v6";
try (final WebClient webClient = new WebClient()) {
HtmlPage page = webClient.getPage(source);
Matcher matcher = null;
for (HtmlAnchor anchor : page.getAnchors()) {
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
if (candidate.find()) {
matcher = candidate;
}
}
if (matcher == null) {
return null;
}
copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2));
return matcher.group(2);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
copyZipToFilesystem(this.repository, installationDirectory, "structure101-build-java-all-" + this.version);
return this.version;
}
private void copyZipToFilesystem(String source, File destination, String name) {
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,12 @@
package s101;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.gradle.api.Project;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
@@ -25,6 +30,11 @@ import org.gradle.api.tasks.InputDirectory;
public class S101PluginExtension {
private final Property<String> licenseId;
private final Property<String> repository;
private final Property<String> version;
private final Property<File> installationDirectory;
private final Property<File> configurationDirectory;
private final Property<String> label;
@@ -65,6 +75,24 @@ public class S101PluginExtension {
this.label.set(label);
}
@Input
public Property<String> getRepository() {
return repository;
}
public void setRepository(String repository) {
this.repository.set(repository);
}
@Input
public Property<String> getVersion() {
return this.version;
}
public void setVersion(String version) {
this.version.set(version);
}
public S101PluginExtension(Project project) {
this.licenseId = project.getObjects().property(String.class);
if (project.hasProperty("s101.licenseId")) {
@@ -78,5 +106,31 @@ public class S101PluginExtension {
if (project.hasProperty("s101.label")) {
setLabel((String) project.findProperty("s101.label"));
}
this.repository = project.getObjects().property(String.class);
if (project.hasProperty("s101.repository")) {
setRepository((String) project.findProperty("s101.repository"));
} else {
setRepository("https://structure101.com/binaries/v6");
}
this.version = project.getObjects().property(String.class);
if (project.hasProperty("s101.version")) {
setVersion((String) project.findProperty("s101.version"));
} else {
try (final WebClient webClient = new WebClient()) {
HtmlPage page = webClient.getPage(getRepository().get());
Matcher matcher = null;
for (HtmlAnchor anchor : page.getAnchors()) {
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
if (candidate.find()) {
matcher = candidate;
}
}
if (matcher != null) {
setVersion(matcher.group(2));
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
}
@@ -1,86 +0,0 @@
/*
* Copyright 2019-2020 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.convention.versions;
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
import org.gradle.api.Action;
import org.gradle.api.artifacts.ComponentSelection;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;
public class DependencyExcludesTests {
@Test
public void createExcludeMinorVersionBumpWhenMajorVersionBumpThenReject() {
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "2.0.0");
verify(componentSelection).reject(any());
}
@Test
public void createExcludeMinorVersionBumpWhenMajorCalVersionBumpThenReject() {
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("2000.0.0", "2001.0.0");
verify(componentSelection).reject(any());
}
@Test
public void createExcludeMinorVersionBumpWhenMinorVersionBumpThenReject() {
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "1.1.0");
verify(componentSelection).reject(any());
}
@Test
public void createExcludeMinorVersionBumpWhenMinorCalVersionBumpThenReject() {
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("2000.0.0", "2000.1.0");
verify(componentSelection).reject(any());
}
@Test
public void createExcludeMinorVersionBumpWhenMinorAndPatchVersionBumpThenReject() {
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "1.1.1");
verify(componentSelection).reject(any());
}
@Test
public void createExcludeMinorVersionBumpWhenPatchVersionBumpThenDoesNotReject() {
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "1.0.1");
verify(componentSelection, times(0)).reject(any());
}
private ComponentSelection executeCreateExcludeMinorVersionBump(String currentVersion, String candidateVersion) {
ComponentSelection componentSelection = mock(ComponentSelection.class);
UpdateDependenciesExtension.DependencyExcludes excludes = new UpdateDependenciesExtension(() -> Collections.emptyList()).new DependencyExcludes();
Action<ComponentSelectionWithCurrent> excludeMinorVersionBump = excludes.createExcludeMinorVersionBump();
ComponentSelectionWithCurrent selection = currentVersionAndCandidateVersion(componentSelection, currentVersion, candidateVersion);
excludeMinorVersionBump.execute(selection);
return componentSelection;
}
private ComponentSelectionWithCurrent currentVersionAndCandidateVersion(ComponentSelection componentSelection, String currentVersion, String candidateVersion) {
ModuleComponentIdentifier candidate = mock(ModuleComponentIdentifier.class);
given(componentSelection.getCandidate()).willReturn(candidate);
ComponentSelectionWithCurrent selection = new ComponentSelectionWithCurrent(currentVersion, componentSelection);
given(candidate.getVersion()).willReturn(candidateVersion);
given(componentSelection.getCandidate()).willReturn(candidate);
return selection;
}
}
@@ -1,31 +0,0 @@
/*
* Copyright 2019-2020 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.convention.versions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TransitiveDependencyLookupUtilsTest {
@Test
public void lookupJwtVersionWhen93Then961() {
String s = TransitiveDependencyLookupUtils.lookupJwtVersion("9.3");
assertThat(s).isEqualTo("9.6.1");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2023 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.
@@ -149,7 +149,7 @@ public class AuthenticationManagerBuilder
* {@link #getDefaultUserDetailsService()} method. Note that additional
* {@link UserDetailsService}'s may override this {@link UserDetailsService} as the
* default. See the <a href=
* "https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#user-schema"
* "https://docs.spring.io/spring-security/reference/servlet/appendix/database-schema.html"
* >User Schema</a> section of the reference for the default schema.
* </p>
* @return a {@link JdbcUserDetailsManagerConfigurer} to allow customization of the
@@ -43,6 +43,7 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults;
* @since 5.0
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class ReactiveMethodSecurityConfiguration implements ImportAware {
private int advisorOrder;
@@ -51,16 +52,17 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMethodSecurityMetadataSource source) {
static MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMethodSecurityMetadataSource source,
ReactiveMethodSecurityConfiguration configuration) {
MethodSecurityMetadataSourceAdvisor advisor = new MethodSecurityMetadataSourceAdvisor(
"securityMethodInterceptor", source, "methodMetadataSource");
advisor.setOrder(this.advisorOrder);
advisor.setOrder(configuration.advisorOrder);
return advisor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
DelegatingMethodSecurityMetadataSource methodMetadataSource(
static DelegatingMethodSecurityMetadataSource methodMetadataSource(
MethodSecurityExpressionHandler methodSecurityExpressionHandler) {
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
methodSecurityExpressionHandler);
@@ -70,7 +72,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
}
@Bean
PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(AbstractMethodSecurityMetadataSource source,
static PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(AbstractMethodSecurityMetadataSource source,
MethodSecurityExpressionHandler handler) {
ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(handler);
ExpressionBasedPreInvocationAdvice preAdvice = new ExpressionBasedPreInvocationAdvice();
@@ -80,10 +82,11 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(
ReactiveMethodSecurityConfiguration configuration) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
if (this.grantedAuthorityDefaults != null) {
handler.setDefaultRolePrefix(this.grantedAuthorityDefaults.getRolePrefix());
if (configuration.grantedAuthorityDefaults != null) {
handler.setDefaultRolePrefix(configuration.grantedAuthorityDefaults.getRolePrefix());
}
return handler;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -22,10 +22,16 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
@@ -72,6 +78,8 @@ public abstract class AbstractRequestMatcherRegistry<C> {
AbstractRequestMatcherRegistry.class.getClassLoader());
}
private final Log logger = LogFactory.getLog(getClass());
protected final void setApplicationContext(ApplicationContext context) {
this.context = context;
}
@@ -314,18 +322,59 @@ public abstract class AbstractRequestMatcherRegistry<C> {
if (servletContext == null) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
boolean isProgrammaticApiAvailable = isProgrammaticApiAvailable(servletContext);
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : patterns) {
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
if (isProgrammaticApiAvailable) {
matchers.add(resolve(ant, mvc, servletContext));
}
else {
this.logger
.warn("The ServletRegistration API was not available at startup time. This may be due to a misconfiguration; "
+ "if you are using AbstractSecurityWebApplicationInitializer, please double-check the recommendations outlined in "
+ "https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#abstractsecuritywebapplicationinitializer-with-spring-mvc");
matchers.add(new DeferredRequestMatcher((request) -> resolve(ant, mvc, request.getServletContext()),
mvc, ant));
}
}
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
}
private static boolean isProgrammaticApiAvailable(ServletContext servletContext) {
try {
servletContext.getServletRegistrations();
return true;
}
catch (UnsupportedOperationException ex) {
return false;
}
}
private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc, ServletContext servletContext) {
Map<String, ? extends ServletRegistration> registrations = mappableServletRegistrations(servletContext);
if (registrations.isEmpty()) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
return ant;
}
if (!hasDispatcherServlet(registrations)) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
return ant;
}
if (registrations.size() > 1) {
String errorMessage = computeErrorMessage(registrations.values());
throw new IllegalArgumentException(errorMessage);
ServletRegistration dispatcherServlet = requireOneRootDispatcherServlet(registrations);
if (dispatcherServlet != null) {
if (registrations.size() == 1) {
return mvc;
}
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, servletContext);
}
return requestMatchers(createMvcMatchers(method, patterns).toArray(new RequestMatcher[0]));
dispatcherServlet = requireOnlyPathMappedDispatcherServlet(registrations);
if (dispatcherServlet != null) {
String mapping = dispatcherServlet.getMappings().iterator().next();
mvc.setServletPath(mapping.substring(0, mapping.length() - 2));
return mvc;
}
String errorMessage = computeErrorMessage(registrations.values());
throw new IllegalArgumentException(errorMessage);
}
private Map<String, ? extends ServletRegistration> mappableServletRegistrations(ServletContext servletContext) {
@@ -343,22 +392,66 @@ public abstract class AbstractRequestMatcherRegistry<C> {
if (registrations == null) {
return false;
}
Class<?> dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet",
null);
for (ServletRegistration registration : registrations.values()) {
try {
Class<?> clazz = Class.forName(registration.getClassName());
if (dispatcherServlet.isAssignableFrom(clazz)) {
return true;
}
}
catch (ClassNotFoundException ex) {
return false;
if (isDispatcherServlet(registration)) {
return true;
}
}
return false;
}
private ServletRegistration requireOneRootDispatcherServlet(
Map<String, ? extends ServletRegistration> registrations) {
ServletRegistration rootDispatcherServlet = null;
for (ServletRegistration registration : registrations.values()) {
if (!isDispatcherServlet(registration)) {
continue;
}
if (registration.getMappings().size() > 1) {
return null;
}
if (!"/".equals(registration.getMappings().iterator().next())) {
return null;
}
rootDispatcherServlet = registration;
}
return rootDispatcherServlet;
}
private ServletRegistration requireOnlyPathMappedDispatcherServlet(
Map<String, ? extends ServletRegistration> registrations) {
ServletRegistration pathDispatcherServlet = null;
for (ServletRegistration registration : registrations.values()) {
if (!isDispatcherServlet(registration)) {
return null;
}
if (registration.getMappings().size() > 1) {
return null;
}
String mapping = registration.getMappings().iterator().next();
if (!mapping.startsWith("/") || !mapping.endsWith("/*")) {
return null;
}
if (pathDispatcherServlet != null) {
return null;
}
pathDispatcherServlet = registration;
}
return pathDispatcherServlet;
}
private boolean isDispatcherServlet(ServletRegistration registration) {
Class<?> dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet",
null);
try {
Class<?> clazz = Class.forName(registration.getClassName());
return dispatcherServlet.isAssignableFrom(clazz);
}
catch (ClassNotFoundException ex) {
return false;
}
}
private String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
@@ -498,4 +591,99 @@ public abstract class AbstractRequestMatcherRegistry<C> {
}
static class DeferredRequestMatcher implements RequestMatcher {
final Function<HttpServletRequest, RequestMatcher> requestMatcherFactory;
final AtomicReference<String> description = new AtomicReference<>();
volatile RequestMatcher requestMatcher;
DeferredRequestMatcher(Function<HttpServletRequest, RequestMatcher> resolver, RequestMatcher... candidates) {
this.requestMatcherFactory = (request) -> {
if (this.requestMatcher == null) {
synchronized (this) {
if (this.requestMatcher == null) {
this.requestMatcher = resolver.apply(request);
}
}
}
return this.requestMatcher;
};
this.description.set("Deferred " + Arrays.toString(candidates));
}
@Override
public boolean matches(HttpServletRequest request) {
return this.requestMatcherFactory.apply(request).matches(request);
}
@Override
public MatchResult matcher(HttpServletRequest request) {
return this.requestMatcherFactory.apply(request).matcher(request);
}
@Override
public String toString() {
return this.description.get();
}
}
static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
private final AntPathRequestMatcher ant;
private final MvcRequestMatcher mvc;
private final ServletContext servletContext;
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
ServletContext servletContext) {
this.ant = ant;
this.mvc = mvc;
this.servletContext = servletContext;
}
@Override
public boolean matches(HttpServletRequest request) {
String name = request.getHttpServletMapping().getServletName();
ServletRegistration registration = this.servletContext.getServletRegistration(name);
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
if (isDispatcherServlet(registration)) {
return this.mvc.matches(request);
}
return this.ant.matches(request);
}
@Override
public MatchResult matcher(HttpServletRequest request) {
String name = request.getHttpServletMapping().getServletName();
ServletRegistration registration = this.servletContext.getServletRegistration(name);
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
if (isDispatcherServlet(registration)) {
return this.mvc.matcher(request);
}
return this.ant.matcher(request);
}
private boolean isDispatcherServlet(ServletRegistration registration) {
Class<?> dispatcherServlet = ClassUtils
.resolveClassName("org.springframework.web.servlet.DispatcherServlet", null);
try {
Class<?> clazz = Class.forName(registration.getClassName());
return dispatcherServlet.isAssignableFrom(clazz);
}
catch (ClassNotFoundException ex) {
return false;
}
}
@Override
public String toString() {
return "DispatcherServletDelegating [" + "ant = " + this.ant + ", mvc = " + this.mvc + "]";
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import org.springframework.security.openid.OpenIDAuthenticationFilter;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -38,7 +39,11 @@ import org.springframework.security.web.authentication.rememberme.RememberMeAuth
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
@@ -46,6 +51,7 @@ import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.DisableEncodeUrlFilter;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.filter.CorsFilter;
/**
* @param <H>
@@ -127,15 +133,24 @@ public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
* The ordering of the Filters is:
*
* <ul>
* <li>{@link ForceEagerSessionCreationFilter}</li>
* <li>{@link DisableEncodeUrlFilter}</li>
* <li>{@link ForceEagerSessionCreationFilter}</li>
* <li>{@link ChannelProcessingFilter}</li>
* <li>{@link WebAsyncManagerIntegrationFilter}</li>
* <li>{@link SecurityContextHolderFilter}</li>
* <li>{@link SecurityContextPersistenceFilter}</li>
* <li>{@link HeaderWriterFilter}</li>
* <li>{@link CorsFilter}</li>
* <li>{@link CsrfFilter}</li>
* <li>{@link LogoutFilter}</li>
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter}</li>
* <li>{@link org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter}</li>
* <li>{@link X509AuthenticationFilter}</li>
* <li>{@link AbstractPreAuthenticatedProcessingFilter}</li>
* <li><a href="
* {@docRoot}/org/springframework/security/cas/web/CasAuthenticationFilter.html">CasAuthenticationFilter</a></li>
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter}</li>
* <li>{@link org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter}</li>
* <li>{@link UsernamePasswordAuthenticationFilter}</li>
* <li>{@link OpenIDAuthenticationFilter}</li>
* <li>{@link org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter}</li>
@@ -149,9 +164,11 @@ public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
* <li>{@link JaasApiIntegrationFilter}</li>
* <li>{@link RememberMeAuthenticationFilter}</li>
* <li>{@link AnonymousAuthenticationFilter}</li>
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter}</li>
* <li>{@link SessionManagementFilter}</li>
* <li>{@link ExceptionTranslationFilter}</li>
* <li>{@link FilterSecurityInterceptor}</li>
* <li>{@link AuthorizationFilter}</li>
* <li>{@link SwitchUserFilter}</li>
* </ul>
* @param filter the {@link Filter} to add
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -801,7 +801,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
* replaced with "#". For example:
*
* <pre>
* X-XSS-Protection: 1 ; mode=block
* X-XSS-Protection: 1; mode=block
* </pre>
* @param headerValue the new header value
* @since 5.8
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -335,7 +335,6 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
result = postProcess(result);
return result;
}
@@ -268,12 +268,14 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
return postProcess(logoutResponseFilter);
}
private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationResolver registrations) {
private Saml2RelyingPartyInitiatedLogoutFilter createRelyingPartyLogoutFilter(
RelyingPartyRegistrationResolver registrations) {
LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler(
registrations);
logoutRequestSuccessHandler.setLogoutRequestRepository(this.logoutRequestConfigurer.logoutRequestRepository);
LogoutFilter logoutFilter = new LogoutFilter(logoutRequestSuccessHandler, logoutHandlers);
Saml2RelyingPartyInitiatedLogoutFilter logoutFilter = new Saml2RelyingPartyInitiatedLogoutFilter(
logoutRequestSuccessHandler, logoutHandlers);
logoutFilter.setLogoutRequestMatcher(createLogoutMatcher());
return postProcess(logoutFilter);
}
@@ -568,4 +570,12 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
}
private static class Saml2RelyingPartyInitiatedLogoutFilter extends LogoutFilter {
Saml2RelyingPartyInitiatedLogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
super(logoutSuccessHandler, handlers);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.security.config.http;
import org.opensaml.core.Version;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
@@ -27,6 +28,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -35,6 +37,8 @@ import org.springframework.util.StringUtils;
*/
final class Saml2LoginBeanDefinitionParserUtils {
private static final String OPEN_SAML_4_VERSION = "4";
private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
private static final String ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF = "authentication-request-repository-ref";
@@ -78,15 +82,27 @@ final class Saml2LoginBeanDefinitionParserUtils {
.rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
.addConstructorArgValue(relyingPartyRegistrationRepository)
.getBeanDefinition();
if (version().startsWith("4")) {
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver")
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver)
.getBeanDefinition();
}
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver")
"org.springframework.security.saml2.provider.service.web.authentication.OpenSamlAuthenticationRequestResolver")
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver)
.getBeanDefinition();
}
static BeanDefinition createAuthenticationProvider() {
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider")
if (version().startsWith("4")) {
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider")
.getBeanDefinition();
}
return BeanDefinitionBuilder
.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider")
.getBeanDefinition();
}
@@ -108,4 +124,17 @@ final class Saml2LoginBeanDefinitionParserUtils {
.getBeanDefinition();
}
static String version() {
String version = Version.getVersion();
if (StringUtils.hasText(version)) {
return version;
}
boolean openSaml4ClassPresent = ClassUtils
.isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
if (openSaml4ClassPresent) {
return OPEN_SAML_4_VERSION;
}
throw new IllegalStateException("cannot determine OpenSAML version");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.security.config.http;
import org.opensaml.core.Version;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
@@ -25,6 +26,7 @@ import org.springframework.security.saml2.provider.service.authentication.logout
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -33,6 +35,8 @@ import org.springframework.util.StringUtils;
*/
final class Saml2LogoutBeanDefinitionParserUtils {
private static final String OPEN_SAML_4_VERSION = "4";
private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
private static final String ATT_LOGOUT_REQUEST_VALIDATOR_REF = "logout-request-validator-ref";
@@ -62,8 +66,14 @@ final class Saml2LogoutBeanDefinitionParserUtils {
if (StringUtils.hasText(logoutResponseResolver)) {
return new RuntimeBeanReference(logoutResponseResolver);
}
if (version().startsWith("4")) {
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
.addConstructorArgValue(registrations)
.getBeanDefinition();
}
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseResolver")
.addConstructorArgValue(registrations)
.getBeanDefinition();
}
@@ -97,10 +107,29 @@ final class Saml2LogoutBeanDefinitionParserUtils {
if (StringUtils.hasText(logoutRequestResolver)) {
return new RuntimeBeanReference(logoutRequestResolver);
}
if (version().startsWith("4")) {
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
.addConstructorArgValue(registrations)
.getBeanDefinition();
}
return BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestResolver")
.addConstructorArgValue(registrations)
.getBeanDefinition();
}
static String version() {
String version = Version.getVersion();
if (StringUtils.hasText(version)) {
return version;
}
boolean openSaml4ClassPresent = ClassUtils
.isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
if (openSaml4ClassPresent) {
return OPEN_SAML_4_VERSION;
}
throw new IllegalStateException("cannot determine OpenSAML version");
}
}
@@ -55,6 +55,11 @@ public class MockServletContext extends org.springframework.mock.web.MockServlet
return this.registrations;
}
@Override
public ServletRegistration getServletRegistration(String servletName) {
return this.registrations.get(servletName);
}
private static class MockServletRegistration implements ServletRegistration.Dynamic {
private final String name;
@@ -0,0 +1,46 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.MappingMatch;
import org.springframework.mock.web.MockHttpServletMapping;
public final class TestMockHttpServletMappings {
private TestMockHttpServletMappings() {
}
public static MockHttpServletMapping extension(HttpServletRequest request, String extension) {
String uri = request.getRequestURI();
String matchValue = uri.substring(0, uri.lastIndexOf(extension));
return new MockHttpServletMapping(matchValue, "*" + extension, "extension", MappingMatch.EXTENSION);
}
public static MockHttpServletMapping path(HttpServletRequest request, String path) {
String uri = request.getRequestURI();
String matchValue = uri.substring(path.length());
return new MockHttpServletMapping(matchValue, path + "/*", "path", MappingMatch.PATH);
}
public static MockHttpServletMapping defaultMapping() {
return new MockHttpServletMapping("", "/", "default", MappingMatch.DEFAULT);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,10 +18,12 @@ package org.springframework.security.config.annotation.web;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServletMapping;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -29,8 +31,11 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.config.MockServletContext;
import org.springframework.security.config.TestMockHttpServletMappings;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
@@ -43,6 +48,9 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link AbstractRequestMatcherRegistry}.
@@ -195,8 +203,11 @@ public class AbstractRequestMatcherRegistryTests {
@Test
public void requestMatchersWhenNoDispatcherServletThenAntPathRequestMatcherType() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
@@ -211,10 +222,32 @@ public class AbstractRequestMatcherRegistryTests {
@Test
public void requestMatchersWhenAmbiguousServletsThenException() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/servlet/**");
servletContext.addServlet("servletTwo", DispatcherServlet.class).addMapping("/servlet/*");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
}
@Test
public void requestMatchersWhenMultipleDispatcherServletMappingsThenException() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/mvc/*");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
}
@Test
public void requestMatchersWhenPathDispatcherServletAndOtherServletsThenException() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
servletContext.addServlet("default", Servlet.class).addMapping("/");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
}
@@ -231,6 +264,87 @@ public class AbstractRequestMatcherRegistryTests {
assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
}
@Test
public void requestMatchersWhenOnlyDispatcherServletThenAllows() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
}
@Test
public void requestMatchersWhenImplicitServletsThenAllows() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("defaultServlet", Servlet.class);
servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
}
@Test
public void requestMatchersWhenPathBasedNonDispatcherServletThenAllows() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
servletContext.addServlet("default", DispatcherServlet.class).addMapping("/");
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/services/*");
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.defaultMapping();
}
};
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.path(this, "/services");
}
};
request.setServletPath("/services");
request.setPathInfo("/endpoint");
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
}
@Test
public void matchesWhenDispatcherServletThenMvc() {
MockServletContext servletContext = new MockServletContext();
servletContext.addServlet("default", DispatcherServlet.class).addMapping("/");
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
MvcRequestMatcher mvc = mock(MvcRequestMatcher.class);
AntPathRequestMatcher ant = mock(AntPathRequestMatcher.class);
DispatcherServletDelegatingRequestMatcher requestMatcher = new DispatcherServletDelegatingRequestMatcher(ant,
mvc, servletContext);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.defaultMapping();
}
};
assertThat(requestMatcher.matches(request)).isFalse();
verify(mvc).matches(request);
verifyNoInteractions(ant);
request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.path(this, "/services");
}
};
assertThat(requestMatcher.matches(request)).isFalse();
verify(ant).matches(request);
verifyNoMoreInteractions(mvc);
}
private void mockMvcIntrospector(boolean isPresent) {
ApplicationContext context = this.matcherRegistry.getApplicationContext();
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
@@ -257,11 +371,29 @@ public class AbstractRequestMatcherRegistryTests {
return null;
}
@Override
public List<RequestMatcher> requestMatchers(RequestMatcher... requestMatchers) {
return unwrap(super.requestMatchers(requestMatchers));
}
@Override
protected List<RequestMatcher> chainRequestMatchers(List<RequestMatcher> requestMatchers) {
return requestMatchers;
}
private static List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
List<RequestMatcher> requestMatchers = new ArrayList<>();
for (RequestMatcher requestMatcher : wrappedMatchers) {
if (requestMatcher instanceof AbstractRequestMatcherRegistry.DeferredRequestMatcher) {
requestMatchers.add(((DeferredRequestMatcher) requestMatcher).requestMatcher);
}
else {
requestMatchers.add(requestMatcher);
}
}
return requestMatchers;
}
}
}
@@ -29,7 +29,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -461,7 +460,7 @@ public class Saml2LoginConfigurerTests {
Authentication authentication = this.securityContextRepository
.loadContext(new HttpRequestResponseHolder(this.request, this.response))
.getAuthentication();
Assertions.assertNotNull(authentication, "Expected a valid authentication object.");
assertThat(authentication).as("Expected a valid authentication object.").isNotNull();
assertThat(authentication.getAuthorities()).hasSize(1);
assertThat(authentication.getAuthorities()).first()
.isInstanceOf(SimpleGrantedAuthority.class)
@@ -75,7 +75,7 @@ public class HeaderSpecTests {
this.expectedHeaders.add(HttpHeaders.EXPIRES, "0");
this.expectedHeaders.add(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff");
this.expectedHeaders.add(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, "DENY");
this.expectedHeaders.add(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block");
this.expectedHeaders.add(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block");
}
@Test
@@ -320,7 +320,7 @@ public class HeaderSpecTests {
@Test
public void headersWhenXssProtectionValueEnabledModeBlockThenXssProtectionWritten() {
this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block");
this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block");
// @formatter:off
this.http.headers()
.xssProtection()
@@ -70,7 +70,7 @@ class ServerHeadersDslTests {
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
}
@EnableWebFluxSecurity
@@ -123,7 +123,7 @@ class ServerHttpSecurityDslTests {
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
}
@EnableWebFluxSecurity
@@ -56,7 +56,7 @@ class ServerXssProtectionDslTests {
this.client.get()
.uri("/")
.exchange()
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
}
@EnableWebFluxSecurity
@@ -61,7 +61,7 @@ public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private boolean isFullyAuthenticated(Authentication authentication) {
return (!this.authenticationTrustResolver.isAnonymous(authentication)
return authentication != null && (!this.authenticationTrustResolver.isAnonymous(authentication)
&& !this.authenticationTrustResolver.isRememberMe(authentication));
}
@@ -95,17 +95,28 @@ final class AuthorizationAnnotationUtils {
private static <A extends Annotation> boolean hasDuplicate(MergedAnnotations mergedAnnotations,
Class<A> annotationType) {
boolean alreadyFound = false;
MergedAnnotation<Annotation> alreadyFound = null;
for (MergedAnnotation<Annotation> mergedAnnotation : mergedAnnotations) {
if (isSynthetic(mergedAnnotation.getSource())) {
continue;
}
if (mergedAnnotation.getType() == annotationType) {
if (alreadyFound) {
return true;
}
alreadyFound = true;
if (mergedAnnotation.getType() != annotationType) {
continue;
}
if (alreadyFound == null) {
alreadyFound = mergedAnnotation;
continue;
}
// https://github.com/spring-projects/spring-framework/issues/31803
if (!mergedAnnotation.getSource().equals(alreadyFound.getSource())) {
return true;
}
if (mergedAnnotation.getRoot().getType() != alreadyFound.getRoot().getType()) {
return true;
}
}
return false;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -51,8 +51,7 @@ import org.springframework.util.Assert;
public final class AuthorizationManagerAfterMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
private Supplier<Authentication> authentication = getAuthentication(
SecurityContextHolder.getContextHolderStrategy());
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
private final Log logger = LogFactory.getLog(this.getClass());
@@ -94,7 +93,7 @@ public final class AuthorizationManagerAfterMethodInterceptor
PostAuthorizeAuthorizationManager authorizationManager) {
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(
AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
interceptor.setOrder(500);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
return interceptor;
}
@@ -156,14 +155,14 @@ public final class AuthorizationManagerAfterMethodInterceptor
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) {
this.authentication = getAuthentication(strategy);
this.securityContextHolderStrategy = () -> strategy;
}
private void attemptAuthorization(MethodInvocation mi, Object result) {
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
MethodInvocationResult object = new MethodInvocationResult(mi, result);
AuthorizationDecision decision = this.authorizationManager.check(this.authentication, object);
this.eventPublisher.publishAuthorizationEvent(this.authentication, object, decision);
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
+ this.authorizationManager + " and decision " + decision));
@@ -172,15 +171,13 @@ public final class AuthorizationManagerAfterMethodInterceptor
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
}
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
return () -> {
Authentication authentication = strategy.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
};
private Authentication getAuthentication() {
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
}
private static <T> void noPublish(Supplier<Authentication> authentication, T object,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -56,8 +56,7 @@ import org.springframework.util.Assert;
public final class AuthorizationManagerBeforeMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
private Supplier<Authentication> authentication = getAuthentication(
SecurityContextHolder.getContextHolderStrategy());
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
private final Log logger = LogFactory.getLog(this.getClass());
@@ -202,13 +201,13 @@ public final class AuthorizationManagerBeforeMethodInterceptor
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
this.authentication = getAuthentication(securityContextHolderStrategy);
this.securityContextHolderStrategy = () -> securityContextHolderStrategy;
}
private void attemptAuthorization(MethodInvocation mi) {
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
AuthorizationDecision decision = this.authorizationManager.check(this.authentication, mi);
this.eventPublisher.publishAuthorizationEvent(this.authentication, mi, decision);
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
+ this.authorizationManager + " and decision " + decision));
@@ -217,15 +216,13 @@ public final class AuthorizationManagerBeforeMethodInterceptor
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
}
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
return () -> {
Authentication authentication = strategy.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
};
private Authentication getAuthentication() {
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
}
private static <T> void noPublish(Supplier<Authentication> authentication, T object,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -46,8 +46,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
public final class PostFilterAuthorizationMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
private Supplier<Authentication> authentication = getAuthentication(
SecurityContextHolder.getContextHolderStrategy());
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
private PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
@@ -108,7 +107,7 @@ public final class PostFilterAuthorizationMethodInterceptor
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) {
this.authentication = getAuthentication(strategy);
this.securityContextHolderStrategy = () -> strategy;
}
/**
@@ -125,19 +124,17 @@ public final class PostFilterAuthorizationMethodInterceptor
return returnedObject;
}
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
EvaluationContext ctx = expressionHandler.createEvaluationContext(this::getAuthentication, mi);
return expressionHandler.filter(returnedObject, attribute.getExpression(), ctx);
}
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
return () -> {
Authentication authentication = strategy.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
};
private Authentication getAuthentication() {
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -47,8 +47,7 @@ import org.springframework.util.StringUtils;
public final class PreFilterAuthorizationMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
private Supplier<Authentication> authentication = getAuthentication(
SecurityContextHolder.getContextHolderStrategy());
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
private PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
@@ -109,7 +108,7 @@ public final class PreFilterAuthorizationMethodInterceptor
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) {
this.authentication = getAuthentication(strategy);
this.securityContextHolderStrategy = () -> strategy;
}
/**
@@ -124,7 +123,7 @@ public final class PreFilterAuthorizationMethodInterceptor
return mi.proceed();
}
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
EvaluationContext ctx = expressionHandler.createEvaluationContext(this::getAuthentication, mi);
Object filterTarget = findFilterTarget(attribute.getFilterTarget(), ctx, mi);
expressionHandler.filter(filterTarget, attribute.getExpression(), ctx);
return mi.proceed();
@@ -150,15 +149,13 @@ public final class PreFilterAuthorizationMethodInterceptor
return filterTarget;
}
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
return () -> {
Authentication authentication = strategy.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
};
private Authentication getAuthentication() {
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
}
}
@@ -16,7 +16,6 @@
package org.springframework.security.access.intercept;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.BadCredentialsException;
@@ -50,7 +49,7 @@ public class RunAsImplAuthenticationProviderTests {
RunAsImplAuthenticationProvider provider = new RunAsImplAuthenticationProvider();
provider.setKey("my_password");
Authentication result = provider.authenticate(token);
Assertions.assertTrue(result instanceof RunAsUserToken, "Should have returned RunAsUserToken");
assertThat(result instanceof RunAsUserToken).as("Should have returned RunAsUserToken").isTrue();
RunAsUserToken resultCast = (RunAsUserToken) result;
assertThat(resultCast.getKeyHash()).isEqualTo("my_password".hashCode());
}
@@ -59,6 +59,7 @@ public class AuthenticatedVoterTests {
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createAnonymous(), null, def));
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
}
@Test
@@ -68,6 +69,7 @@ public class AuthenticatedVoterTests {
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createRememberMe(), null, def));
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
}
@Test
@@ -77,6 +79,7 @@ public class AuthenticatedVoterTests {
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
}
@Test
@@ -41,6 +41,13 @@ class AuthorizationAnnotationUtilsTests {
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class));
}
@Test // gh-13625
void annotationsFromSuperSuperInterfaceShouldNotTriggerAnnotationConfigurationException() throws Exception {
Method method = HelloImpl.class.getMethod("sayHello");
assertThatNoException()
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class));
}
private interface BaseRepository<T> {
Iterable<T> findAll();
@@ -55,4 +62,24 @@ class AuthorizationAnnotationUtilsTests {
}
private interface Hello {
@PreAuthorize("hasRole('someRole')")
String sayHello();
}
private interface SayHello extends Hello {
}
private static class HelloImpl implements SayHello {
@Override
public String sayHello() {
return "hello";
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -29,6 +29,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -91,6 +92,25 @@ public class AuthorizationManagerAfterMethodInterceptorTests {
verify(strategy).getContext();
}
// gh-12877
@Test
public void afterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
Authentication authentication = new TestingAuthenticationToken("john", "password",
AuthorityUtils.createAuthorityList("authority"));
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
MethodInvocation invocation = mock(MethodInvocation.class);
AuthorizationManager<MethodInvocationResult> authorizationManager = AuthenticatedAuthorizationManager
.authenticated();
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
Pointcut.TRUE, authorizationManager);
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
SecurityContextHolder.setContextHolderStrategy(strategy);
advice.invoke(invocation);
verify(strategy).getContext();
SecurityContextHolder.setContextHolderStrategy(saved);
}
@Test
public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() {
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -87,6 +87,24 @@ public class AuthorizationManagerBeforeMethodInterceptorTests {
verify(strategy).getContext();
}
// gh-12877
@Test
public void beforeWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
Authentication authentication = new TestingAuthenticationToken("john", "password",
AuthorityUtils.createAuthorityList("authority"));
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
MethodInvocation invocation = mock(MethodInvocation.class);
AuthorizationManager<MethodInvocation> authorizationManager = AuthenticatedAuthorizationManager.authenticated();
AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
Pointcut.TRUE, authorizationManager);
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
SecurityContextHolder.setContextHolderStrategy(strategy);
advice.invoke(invocation);
verify(strategy).getContext();
SecurityContextHolder.setContextHolderStrategy(saved);
}
@Test
public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() {
AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -147,6 +147,29 @@ public class PostFilterAuthorizationMethodInterceptorTests {
verify(strategy).getContext();
}
// gh-12877
@Test
public void postFilterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
Authentication authentication = new TestingAuthenticationToken("john", "password",
AuthorityUtils.createAuthorityList("authority"));
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
String[] array = { "john", "bob" };
MockMethodInvocation invocation = new MockMethodInvocation(new TestClass(), TestClass.class,
"doSomethingArrayAuthentication", new Class[] { String[].class }, new Object[] { array }) {
@Override
public Object proceed() {
return array;
}
};
PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
SecurityContextHolder.setContextHolderStrategy(strategy);
advice.invoke(invocation);
verify(strategy).getContext();
SecurityContextHolder.setContextHolderStrategy(saved);
}
@PostFilter("filterObject == 'john'")
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -204,6 +204,26 @@ public class PreFilterAuthorizationMethodInterceptorTests {
verify(strategy).getContext();
}
// gh-12877
@Test
public void preFilterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
Authentication authentication = new TestingAuthenticationToken("john", "password",
AuthorityUtils.createAuthorityList("authority"));
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
List<String> list = new ArrayList<>();
list.add("john");
list.add("bob");
MockMethodInvocation invocation = new MockMethodInvocation(new TestClass(), TestClass.class,
"doSomethingArrayFilterAuthentication", new Class[] { List.class }, new Object[] { list });
PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
SecurityContextHolder.setContextHolderStrategy(strategy);
advice.invoke(invocation);
verify(strategy).getContext();
SecurityContextHolder.setContextHolderStrategy(saved);
}
@PreFilter("filterObject == 'john'")
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
@@ -20,7 +20,6 @@ import java.security.SecureRandom;
import java.util.Random;
import java.util.UUID;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -29,6 +28,8 @@ import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgor
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import static org.assertj.core.api.Assertions.assertThat;
public class BouncyCastleAesBytesEncryptorEquivalencyTests {
private byte[] testData;
@@ -96,11 +97,11 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests {
// and can decrypt back to the original input
byte[] leftEncrypted = left.encrypt(this.testData);
byte[] rightEncrypted = right.encrypt(this.testData);
Assertions.assertArrayEquals(leftEncrypted, rightEncrypted);
assertThat(rightEncrypted).containsExactly(leftEncrypted);
byte[] leftDecrypted = left.decrypt(leftEncrypted);
byte[] rightDecrypted = right.decrypt(rightEncrypted);
Assertions.assertArrayEquals(this.testData, leftDecrypted);
Assertions.assertArrayEquals(this.testData, rightDecrypted);
assertThat(leftDecrypted).containsExactly(this.testData);
assertThat(rightDecrypted).containsExactly(this.testData);
}
}
@@ -114,8 +115,8 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests {
byte[] rightEncrypted = right.encrypt(this.testData);
byte[] leftDecrypted = left.decrypt(rightEncrypted);
byte[] rightDecrypted = right.decrypt(leftEncrypted);
Assertions.assertArrayEquals(this.testData, leftDecrypted);
Assertions.assertArrayEquals(this.testData, rightDecrypted);
assertThat(leftDecrypted).containsExactly(this.testData);
assertThat(rightDecrypted).containsExactly(this.testData);
}
}
@@ -20,13 +20,13 @@ import java.security.SecureRandom;
import java.util.UUID;
import org.bouncycastle.util.Arrays;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.KeyGenerators;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
public class BouncyCastleAesBytesEncryptorTests {
@@ -64,11 +64,11 @@ public class BouncyCastleAesBytesEncryptorTests {
private void generatesDifferentCipherTexts(BytesEncryptor bcEncryptor) {
byte[] encrypted1 = bcEncryptor.encrypt(this.testData);
byte[] encrypted2 = bcEncryptor.encrypt(this.testData);
Assertions.assertFalse(Arrays.areEqual(encrypted1, encrypted2));
assertThat(Arrays.areEqual(encrypted1, encrypted2)).isFalse();
byte[] decrypted1 = bcEncryptor.decrypt(encrypted1);
byte[] decrypted2 = bcEncryptor.decrypt(encrypted2);
Assertions.assertArrayEquals(this.testData, decrypted1);
Assertions.assertArrayEquals(this.testData, decrypted2);
assertThat(decrypted1).containsExactly(this.testData);
assertThat(decrypted2).containsExactly(this.testData);
}
@Test
+1
View File
@@ -17,6 +17,7 @@
* xref:features/index.adoc[Features]
** xref:features/authentication/index.adoc[Authentication]
*** xref:features/authentication/password-storage.adoc[Password Storage]
** xref:features/authorization/index.adoc[Authorization]
** xref:features/exploits/index.adoc[Protection Against Exploits]
*** xref:features/exploits/csrf.adoc[CSRF]
*** xref:features/exploits/headers.adoc[HTTP Headers]
@@ -8,4 +8,4 @@ Once authentication is performed we know the identity and can perform authorizat
Spring Security provides built in support for authenticating users.
This section is dedicated to generic authentication support that applies in both Servlet and WebFlux environments.
Refer to the sections on authentication for xref:servlet/authentication/index.adoc#servlet-authentication[Servlet] and WebFlux for details on what is supported for each stack.
Refer to the sections on authentication for xref:servlet/authentication/index.adoc#servlet-authentication[Servlet] and xref:servlet/authentication/index.adoc[WebFlux] for details on what is supported for each stack.
@@ -8,7 +8,9 @@ Spring Security provides https://en.wikipedia.org/wiki/Defense_in_depth_(computi
[[authorization-request]]
== Request Based Authorization
Spring Security provides authorization based upon the request for both xref:servlet/authorization/index.adoc[Servlet] and WebFlux environments.
Spring Security provides authorization based upon the request for both xref:servlet/authorization/authorize-http-requests.adoc[Servlet] and xref:reactive/authorization/authorize-http-requests.adoc[WebFlux] environments.
[[authorization-method]]
== Method Based Authorization
Spring Security provides authorization based on the method invocation for both xref:servlet/authorization/method-security.adoc[Servlet] and xref:reactive/authorization/method.adoc[WebFlux] environments.
@@ -97,13 +97,13 @@ Spring provides two mechanisms to protect against CSRF attacks:
[NOTE]
====
Both protections require that <<Safe Methods Must be Idempotent>>
Both protections require that <<Safe Methods Must be Read-only>>
====
[[csrf-protection-idempotent]]
=== Safe Methods Must be Idempotent
[[csrf-protection-read-only]]
=== Safe Methods Must be Read-only
In order for <<csrf-protection,either protection>> against CSRF to work, the application must ensure that https://tools.ietf.org/html/rfc7231#section-4.2.1["safe" HTTP methods are idempotent].
In order for <<csrf-protection,either protection>> against CSRF to work, the application must ensure that https://tools.ietf.org/html/rfc7231#section-4.2.1["safe" HTTP methods are read-only].
This means that requests with the HTTP method `GET`, `HEAD`, `OPTIONS`, and `TRACE` should not change the state of the application.
[[csrf-protection-stp]]
@@ -119,7 +119,7 @@ For example, requiring the actual CSRF token in an HTTP parameter or an HTTP hea
Requiring the actual CSRF token in a cookie does not work because cookies are automatically included in the HTTP request by the browser.
We can relax the expectations to only require the actual CSRF token for each HTTP request that updates state of the application.
For that to work, our application must ensure that <<csrf-protection-idempotent,safe HTTP methods are idempotent>>.
For that to work, our application must ensure that <<csrf-protection-read-only,safe HTTP methods are read-only>>.
This improves usability since we want to allow linking to our website using links from external sites.
Additionally, we do not want to include the random token in HTTP GET as this can cause the tokens to be leaked.
@@ -190,7 +190,7 @@ Valid values for the `SameSite` attribute are:
* `Strict` - when specified any request coming from the https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1[same-site] will include the cookie.
Otherwise, the cookie will not be included in the HTTP request.
* `Lax` - when specified cookies will be sent when coming from the https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1[same-site] or when the request comes from top-level navigations and the <<Safe Methods Must be Idempotent,method is idempotent>>.
* `Lax` - when specified cookies will be sent when coming from the https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1[same-site] or when the request comes from top-level navigations and the <<Safe Methods Must be Read-only,method is read-only>>.
Otherwise, the cookie will not be included in the HTTP request.
Let's take a look at how <<csrf-explained,our example>> could be protected using the `SameSite` attribute.
@@ -4,5 +4,4 @@
Spring Security provides integrations with numerous frameworks and APIs.
In this section, we discuss generic integrations that are not specific to Servlet or Reactive environments.
To see specific integrations, refer to the xref:servlet/integrations/index.adoc[Servlet] and xref:servlet/integrations/index.adoc[Reactive] Integrations sections.
// FIXME add link to reactive integrations
To see specific integrations, refer to the xref:servlet/integrations/index.adoc[Servlet] and xref:reactive/integrations/cors.adoc[Reactive] Integrations sections.
@@ -17,7 +17,7 @@ In Spring Security 6, the default is that the lookup of the `CsrfToken` will be
[NOTE]
====
The `CsrfToken` is needed whenever a request is made with an HTTP verb that would change the state of the application.
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-read-only[Safe Methods Must be Read-only].
Additionally, it is needed by any request that renders the token to the response, such as a web page with a `<form>` tag that includes a hidden `<input>` for the CSRF token.
====
@@ -20,7 +20,7 @@ Java::
@Bean
SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception {
DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler(
new WebSessionServerLogoutHandler(), new SecurityContextServerLogoutHandler()
new SecurityContextServerLogoutHandler(), new WebSessionServerLogoutHandler()
);
http
@@ -38,7 +38,7 @@ Kotlin::
@Bean
fun http(http: ServerHttpSecurity): SecurityWebFilterChain {
val customLogoutHandler = DelegatingServerLogoutHandler(
WebSessionServerLogoutHandler(), SecurityContextServerLogoutHandler()
SecurityContextServerLogoutHandler(), WebSessionServerLogoutHandler()
)
return http {
@@ -7,14 +7,14 @@ This section discusses Spring Security's xref:features/exploits/csrf.adoc#csrf[C
== Using Spring Security CSRF Protection
The steps to using Spring Security's CSRF protection are outlined below:
* <<webflux-csrf-idempotent,Use proper HTTP verbs>>
* <<webflux-csrf-read-only,Use proper HTTP verbs>>
* <<webflux-csrf-configure,Configure CSRF Protection>>
* <<webflux-csrf-include,Include the CSRF Token>>
[[webflux-csrf-idempotent]]
[[webflux-csrf-read-only]]
=== Use proper HTTP verbs
The first step to protecting against CSRF attacks is to ensure your website uses proper HTTP verbs.
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-read-only[Safe Methods Must be Read-only].
[[webflux-csrf-configure]]
=== Configure CSRF Protection
@@ -62,7 +62,10 @@ This configuration enables <<rsocket-authentication-simple,simple authentication
For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
Spring Boot registers it automatically in `RSocketSecurityAutoConfiguration` when you include {gh-samples-url}/reactive/rsocket/hello-security/build.gradle[the correct dependencies].
Or, if you are not using Boot's auto-configuration, you can register it manually in the following way:
[tabs]
======
@@ -91,6 +94,8 @@ fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor
----
======
To customize the interceptor itself, use `RSocketSecurity` to add <<rsocket-authentication,authentication>> and <<rsocket-authorization,authorization>>.
[[rsocket-authentication]]
== RSocket Authentication
@@ -370,7 +370,7 @@ spring:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
----
@@ -77,11 +77,11 @@ class SecurityConfig {
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
authorize(anyRequest, authenticated)
}
formLogin { }
httpBasic { }
}
}
return http.build()
}
@@ -105,14 +105,14 @@ The preceding configuration automatically registers an xref:servlet/authenticati
To learn more about username/password authentication, consider the following use cases:
* I want to <<publish-authentication-manager-bean,publish an `AuthenticationManager` bean>> for custom authentication
* I want to <<customize-global-authentication-manager,customize the global `AuthenticationManager`>>
* I want to xref:servlet/authentication/passwords/form.adoc[learn how Form Login works]
* I want to xref:servlet/authentication/passwords/basic.adoc[learn how HTTP Basic authentication works]
* I want to xref:servlet/authentication/passwords/basic.adoc[learn how `DaoAuthenticationProvider` works]
* I want to xref:servlet/authentication/passwords/dao-authentication-provider.adoc[learn how `DaoAuthenticationProvider` works]
* I want to xref:servlet/authentication/passwords/in-memory.adoc[manage users in memory]
* I want to xref:servlet/authentication/passwords/jdbc.adoc[manage users in a database]
* I want to xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap-authentication[manage users in LDAP]
* I want to <<publish-authentication-manager-bean,publish an `AuthenticationManager` bean>> for custom authentication
* I want to <<customize-global-authentication-manager,customize the global `AuthenticationManager`>>
[[publish-authentication-manager-bean]]
== Publish an `AuthenticationManager` bean
@@ -199,7 +199,7 @@ XML::
</user-service>
<bean id="passwordEncoder"
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
</http>
----
@@ -207,6 +207,8 @@ Kotlin::
+
[source,kotlin,role="secondary"]
----
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@@ -215,6 +217,7 @@ class SecurityConfig {
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/login", permitAll)
authorize(anyRequest, authenticated)
}
}
@@ -410,7 +413,7 @@ XML::
</user-service>
<bean id="passwordEncoder"
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
</http>
----
@@ -418,14 +421,17 @@ Kotlin::
+
[source,kotlin,role="secondary"]
----
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
public class SecurityConfig {
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/login", permitAll)
authorize(anyRequest, authenticated)
}
formLogin { }
@@ -483,22 +489,22 @@ Java::
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// ...
return http.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// ...
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
// Return a UserDetailsService that caches users
// ...
}
@Bean
public UserDetailsService userDetailsService() {
// Return a UserDetailsService that caches users
// ...
}
@Autowired
public void configure(AuthenticationManagerBuilder builder) {
builder.eraseCredentials(false);
}
@Autowired
public void configure(AuthenticationManagerBuilder builder) {
builder.eraseCredentials(false);
}
}
----
@@ -521,8 +527,8 @@ class SecurityConfig {
@Bean
fun userDetailsService(): UserDetailsService {
// Return a UserDetailsService that caches users
// ...
// Return a UserDetailsService that caches users
// ...
}
@Autowired
@@ -112,7 +112,7 @@ public class SecurityWebApplicationInitializer
This would simply only register the springSecurityFilterChain Filter for every URL in your application.
After that we would ensure that `WebSecurityConfig` was loaded in our existing ApplicationInitializer.
For example, if we were using Spring MVC it would be added in the `getRootConfigClasses()`
For example, if we were using Spring MVC it would be added in the `getServletConfigClasses()`
[[message-web-application-inititializer-java]]
[source,java]
@@ -121,14 +121,42 @@ public class MvcWebApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { WebSecurityConfig.class };
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
}
// ... other overrides ...
}
----
The reason for this is that Spring Security needs to be able to inspect some Spring MVC configuration in order to appropriately configure xref:servlet/authorization/authorize-http-requests.adoc#_request_matchers[underlying request matchers], so they need to be in the same application context.
Placing Spring Security in `getRootConfigClasses` places it into a parent application context that may not be able to find Spring MVC's `HandlerMappingIntrospector`.
==== Configuring for Multiple Spring MVC Dispatchers
If desired, any Spring Security configuration that is unrelated to Spring MVC may be placed in a different configuration class like so:
[source,java]
----
public class MvcWebApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { NonWebSecurityConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
}
// ... other overrides ...
}
----
This can be helpful if you have multiple instances of `AbstractAnnotationConfigDispatcherServletInitializer` and don't want to duplicate the general security configuration across both of them.
[[jc-httpsecurity]]
== HttpSecurity
@@ -7,14 +7,14 @@ This section discusses Spring Security's xref:features/exploits/csrf.adoc#csrf[C
== Using Spring Security CSRF Protection
The steps to using Spring Security's CSRF protection are outlined below:
* <<servlet-csrf-idempotent,Use proper HTTP verbs>>
* <<servlet-csrf-read-only,Use proper HTTP verbs>>
* <<servlet-csrf-configure,Configure CSRF Protection>>
* <<servlet-csrf-include,Include the CSRF Token>>
[[servlet-csrf-idempotent]]
[[servlet-csrf-read-only]]
=== Use proper HTTP verbs
The first step to protecting against CSRF attacks is to ensure your website uses proper HTTP verbs.
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-read-only[Safe Methods Must be Read-only].
[[servlet-csrf-configure]]
=== Configure CSRF Protection
@@ -198,6 +198,63 @@ fun index(): String {
======
<1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`.
The following code shows how to set an `Authentication` as a request attribute:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@GetMapping("/")
public String index() {
String resourceUri = ...
Authentication anonymousAuthentication = new AnonymousAuthenticationToken(
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
String body = webClient
.get()
.uri(resourceUri)
.attributes(authentication(anonymousAuthentication)) <1>
.retrieve()
.bodyToMono(String.class)
.block();
...
return "index";
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@GetMapping("/")
fun index(): String {
val resourceUri: String = ...
val anonymousAuthentication: Authentication = AnonymousAuthenticationToken(
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"))
val body: String = webClient
.get()
.uri(resourceUri)
.attributes(authentication(anonymousAuthentication)) <1>
.retrieve()
.bodyToMono()
.block()
...
return "index"
}
----
======
<1> `authentication()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`.
[WARNING]
It is recommended to be cautious with this feature since all HTTP requests will receive an access token bound to the provided principal.
=== Defaulting the Authorized Client
@@ -513,7 +513,7 @@ spring:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
----
@@ -374,29 +374,22 @@ Java::
----
@Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final TenantRepository tenants;
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
private final TenantRepository tenants;
public TenantJwtIssuerValidator(TenantRepository tenants) {
this.tenants = tenants;
}
private final OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The iss claim is not valid",
"https://tools.ietf.org/html/rfc6750#section-3.1");
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
.validate(token);
}
public TenantJwtIssuerValidator(TenantRepository tenants) {
this.tenants = tenants;
}
private String toTenant(Jwt jwt) {
return jwt.getIssuer();
}
private JwtIssuerValidator fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.findById(tenant))
.map(t -> t.getAttribute("issuer"))
.map(JwtIssuerValidator::new)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
if(this.tenants.findById(token.getIssuer()) != null) {
return OAuth2TokenValidatorResult.success();
}
return OAuth2TokenValidatorResult.failure(this.error);
}
}
----
@@ -405,32 +398,17 @@ Kotlin::
[source,kotlin,role="secondary"]
----
@Component
class TenantJwtIssuerValidator(tenants: TenantRepository) : OAuth2TokenValidator<Jwt> {
private val tenants: TenantRepository
private val validators: MutableMap<String, JwtIssuerValidator> = ConcurrentHashMap()
class TenantJwtIssuerValidator(private val tenants: TenantRepository) : OAuth2TokenValidator<Jwt> {
private val error: OAuth2Error = OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The iss claim is not valid",
"https://tools.ietf.org/html/rfc6750#section-3.1")
override fun validate(token: Jwt): OAuth2TokenValidatorResult {
return validators.computeIfAbsent(toTenant(token)) { tenant: String -> fromTenant(tenant) }
.validate(token)
}
private fun toTenant(jwt: Jwt): String {
return jwt.issuer.toString()
}
private fun fromTenant(tenant: String): JwtIssuerValidator {
return Optional.ofNullable(tenants.findById(tenant))
.map({ t -> t.getAttribute("issuer") })
.map({ JwtIssuerValidator() })
.orElseThrow({ IllegalArgumentException("unknown tenant") })
}
init {
this.tenants = tenants
return if (tenants.findById(token.issuer) != null)
OAuth2TokenValidatorResult.success() else OAuth2TokenValidatorResult.failure(error)
}
}
----
======
Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`]:
[tabs]
@@ -55,7 +55,42 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
== Minimal Dependencies
SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
It builds off of the OpenSAML library.
It builds off of the OpenSAML library, and, for that reason, you must also include the Shibboleth Maven repository in your build configuration.
Check https://shibboleth.atlassian.net/wiki/spaces/DEV/pages/1123844333/Use+of+Maven+Central#Publishing-to-Maven-Central[this link] for more details about why a separate repository is needed.
[tabs]
======
Maven::
+
[source,xml,role="primary"]
----
<repositories>
<!-- ... -->
<repository>
<id>shibboleth-releases</id>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>
</repositories>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
----
Gradle::
+
[source,groovy,role="secondary"]
----
repositories {
// ...
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
// ...
implementation 'org.springframework.security:spring-security-saml2-service-provider'
}
----
======
[[servlet-saml2login-minimalconfiguration]]
== Minimal Configuration
+1 -1
View File
@@ -1,5 +1,5 @@
springBootVersion=2.7.12
version=5.8.8
version=5.8.11
samplesBranch=5.8.x
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+7 -8
View File
@@ -2,12 +2,12 @@
com-squareup-okhttp3 = "3.14.9"
io-r2dbc = "0.9.1.RELEASE"
io-rsocket = "1.1.4"
io-spring-javaformat = "0.0.39"
io-spring-javaformat = "0.0.41"
io-spring-nohttp = "0.0.11"
org-apache-directory-server = "1.5.5"
org-aspectj = "1.9.20.1"
org-bouncycastle = "1.70"
org-eclipse-jetty = "9.4.53.v20231009"
org-eclipse-jetty = "9.4.54.v20240208"
org-jetbrains-kotlin = "1.7.22"
org-jetbrains-kotlinx = "1.6.4"
org-junit-jupiter = "5.9.3"
@@ -15,10 +15,10 @@ org-mockito = "4.8.1"
org-opensaml4 = "4.1.0"
org-opensaml3 = "3.4.6"
org-slf4j = "1.7.36"
org-springframework = "5.3.30"
org-springframework = "5.3.33"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.12"
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.13"
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.13.5"
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"
@@ -30,8 +30,8 @@ com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:4.0.14"
commons-collections = "commons-collections:commons-collections:3.2.2"
io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:6.5.1"
io-mockk = "io.mockk:mockk:1.13.3"
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.38"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.37"
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.43"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.42"
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
io-r2dbc-r2dbc-h2 = { module = "io.r2dbc:r2dbc-h2", version.ref = "io-r2dbc" }
io-r2dbc-r2dbc-spi-test = { module = "io.r2dbc:r2dbc-spi-test", version.ref = "io-r2dbc" }
@@ -90,7 +90,7 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1"
org-slf4j-log4j-over-slf4j = { module = "org.slf4j:log4j-over-slf4j", version.ref = "org-slf4j" }
org-slf4j-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "org-slf4j" }
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2021.2.17"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2021.2.18"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:2.4.1"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
@@ -102,7 +102,6 @@ org-yaml-snakeyaml = "org.yaml:snakeyaml:1.30"
org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0"
org-gretty-gretty = "org.gretty:gretty:3.0.9"
com-apollographql-apollo-apollo-runtime = "com.apollographql.apollo:apollo-runtime:2.4.5"
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.38.0"
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11"
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
@@ -22,7 +22,6 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -30,6 +29,8 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultMapOAuth2AccessTokenResponseConverter}.
*
@@ -56,24 +57,24 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
map.put("custom_parameter_2", "custom-value-2");
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
OAuth2AccessToken accessToken = converted.getAccessToken();
Assertions.assertNotNull(accessToken);
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
assertThat(accessToken).isNotNull();
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
Set<String> scopes = accessToken.getScopes();
Assertions.assertNotNull(scopes);
Assertions.assertEquals(2, scopes.size());
Assertions.assertTrue(scopes.contains("read"));
Assertions.assertTrue(scopes.contains("write"));
Assertions.assertEquals(3600,
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
assertThat(scopes).isNotNull();
assertThat(scopes.size()).isEqualTo(2);
assertThat(scopes.contains("read")).isTrue();
assertThat(scopes.contains("write")).isTrue();
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds())
.isEqualTo(3600);
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
Assertions.assertNotNull(refreshToken);
Assertions.assertEquals("refresh-token-1234", refreshToken.getTokenValue());
assertThat(refreshToken).isNotNull();
assertThat(refreshToken.getTokenValue()).isEqualTo("refresh-token-1234");
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
Assertions.assertNotNull(additionalParameters);
Assertions.assertEquals(2, additionalParameters.size());
Assertions.assertEquals("custom-value-1", additionalParameters.get("custom_parameter_1"));
Assertions.assertEquals("custom-value-2", additionalParameters.get("custom_parameter_2"));
assertThat(additionalParameters).isNotNull();
assertThat(additionalParameters.size()).isEqualTo(2);
assertThat(additionalParameters.get("custom_parameter_1")).isEqualTo("custom-value-1");
assertThat(additionalParameters.get("custom_parameter_2")).isEqualTo("custom-value-2");
}
@Test
@@ -83,19 +84,18 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
map.put("token_type", "bearer");
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
OAuth2AccessToken accessToken = converted.getAccessToken();
Assertions.assertNotNull(accessToken);
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
assertThat(accessToken).isNotNull();
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
Set<String> scopes = accessToken.getScopes();
Assertions.assertNotNull(scopes);
Assertions.assertEquals(0, scopes.size());
Assertions.assertEquals(1,
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
assertThat(scopes).isNotNull();
assertThat(scopes.size()).isEqualTo(0);
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()).isEqualTo(1);
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
Assertions.assertNull(refreshToken);
assertThat(refreshToken).isNull();
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
Assertions.assertNotNull(additionalParameters);
Assertions.assertEquals(0, additionalParameters.size());
assertThat(additionalParameters).isNotNull();
assertThat(additionalParameters.size()).isEqualTo(0);
}
@Test
@@ -106,19 +106,18 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
map.put("expires_in", "2100-01-01-abc");
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
OAuth2AccessToken accessToken = converted.getAccessToken();
Assertions.assertNotNull(accessToken);
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
assertThat(accessToken).isNotNull();
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
Set<String> scopes = accessToken.getScopes();
Assertions.assertNotNull(scopes);
Assertions.assertEquals(0, scopes.size());
Assertions.assertEquals(1,
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
assertThat(scopes).isNotNull();
assertThat(scopes.size()).isEqualTo(0);
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()).isEqualTo(1);
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
Assertions.assertNull(refreshToken);
assertThat(refreshToken).isNull();
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
Assertions.assertNotNull(additionalParameters);
Assertions.assertEquals(0, additionalParameters.size());
assertThat(additionalParameters).isNotNull();
assertThat(additionalParameters.size()).isEqualTo(0);
}
// gh-9685
@@ -130,11 +129,11 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
map.put("expires_in", 3600);
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
OAuth2AccessToken accessToken = converted.getAccessToken();
Assertions.assertNotNull(accessToken);
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
Assertions.assertEquals(3600,
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
assertThat(accessToken).isNotNull();
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds())
.isEqualTo(3600);
}
// gh-9685
@@ -153,24 +152,24 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
map.put("custom_parameter_2", "custom-value-2");
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
OAuth2AccessToken accessToken = converted.getAccessToken();
Assertions.assertNotNull(accessToken);
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
assertThat(accessToken).isNotNull();
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
Set<String> scopes = accessToken.getScopes();
Assertions.assertNotNull(scopes);
Assertions.assertEquals(2, scopes.size());
Assertions.assertTrue(scopes.contains("read"));
Assertions.assertTrue(scopes.contains("write"));
Assertions.assertEquals(3600,
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
assertThat(scopes).isNotNull();
assertThat(scopes.size()).isEqualTo(2);
assertThat(scopes.contains("read")).isTrue();
assertThat(scopes.contains("write")).isTrue();
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds())
.isEqualTo(3600);
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
Assertions.assertNotNull(refreshToken);
Assertions.assertEquals("refresh-token-1234", refreshToken.getTokenValue());
assertThat(refreshToken).isNotNull();
assertThat(refreshToken.getTokenValue()).isEqualTo("refresh-token-1234");
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
Assertions.assertNotNull(additionalParameters);
Assertions.assertEquals(2, additionalParameters.size());
Assertions.assertEquals(nestedObject, additionalParameters.get("custom_parameter_1"));
Assertions.assertEquals("custom-value-2", additionalParameters.get("custom_parameter_2"));
assertThat(additionalParameters).isNotNull();
assertThat(additionalParameters.size()).isEqualTo(2);
assertThat(additionalParameters.get("custom_parameter_1")).isEqualTo(nestedObject);
assertThat(additionalParameters.get("custom_parameter_2")).isEqualTo("custom-value-2");
}
}
@@ -22,13 +22,14 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultOAuth2AccessTokenResponseMapConverter}.
*
@@ -61,14 +62,14 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests {
.build();
// @formatter:on
Map<String, Object> result = this.messageConverter.convert(build);
Assertions.assertEquals(7, result.size());
Assertions.assertEquals("access-token-value-1234", result.get("access_token"));
Assertions.assertEquals("refresh-token-value-1234", result.get("refresh_token"));
Assertions.assertEquals("read write", result.get("scope"));
Assertions.assertEquals("Bearer", result.get("token_type"));
Assertions.assertNotNull(result.get("expires_in"));
Assertions.assertEquals("custom-value-1", result.get("custom_parameter_1"));
Assertions.assertEquals("custom-value-2", result.get("custom_parameter_2"));
assertThat(result.size()).isEqualTo(7);
assertThat(result.get("access_token")).isEqualTo("access-token-value-1234");
assertThat(result.get("refresh_token")).isEqualTo("refresh-token-value-1234");
assertThat(result.get("scope")).isEqualTo("read write");
assertThat(result.get("token_type")).isEqualTo("Bearer");
assertThat(result.get("expires_in")).isNotNull();
assertThat(result.get("custom_parameter_1")).isEqualTo("custom-value-1");
assertThat(result.get("custom_parameter_2")).isEqualTo("custom-value-2");
}
@Test
@@ -79,10 +80,10 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests {
.build();
// @formatter:on
Map<String, Object> result = this.messageConverter.convert(build);
Assertions.assertEquals(3, result.size());
Assertions.assertEquals("access-token-value-1234", result.get("access_token"));
Assertions.assertEquals("Bearer", result.get("token_type"));
Assertions.assertNotNull(result.get("expires_in"));
assertThat(result.size()).isEqualTo(3);
assertThat(result.get("access_token")).isEqualTo("access-token-value-1234");
assertThat(result.get("token_type")).isEqualTo("Bearer");
assertThat(result.get("expires_in")).isNotNull();
}
// gh-9685
@@ -107,14 +108,14 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests {
.build();
// @formatter:on
Map<String, Object> result = this.messageConverter.convert(build);
Assertions.assertEquals(7, result.size());
Assertions.assertEquals("access-token-value-1234", result.get("access_token"));
Assertions.assertEquals("refresh-token-value-1234", result.get("refresh_token"));
Assertions.assertEquals("read write", result.get("scope"));
Assertions.assertEquals("Bearer", result.get("token_type"));
Assertions.assertNotNull(result.get("expires_in"));
Assertions.assertEquals(nestedObject, result.get("custom_parameter_1"));
Assertions.assertEquals("custom-value-2", result.get("custom_parameter_2"));
assertThat(result.size()).isEqualTo(7);
assertThat(result.get("access_token")).isEqualTo("access-token-value-1234");
assertThat(result.get("refresh_token")).isEqualTo("refresh-token-value-1234");
assertThat(result.get("scope")).isEqualTo("read write");
assertThat(result.get("token_type")).isEqualTo("Bearer");
assertThat(result.get("expires_in")).isNotNull();
assertThat(result.get("custom_parameter_1")).isEqualTo(nestedObject);
assertThat(result.get("custom_parameter_2")).isEqualTo("custom-value-2");
}
}
@@ -144,6 +144,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
}
catch (Exception ex) {
this.logger.trace("Failed to parse token", ex);
if (ex instanceof ParseException) {
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed token"), ex);
}
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -145,20 +145,17 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
}
@Override
public Mono<Jwt> decode(String token) throws JwtException {
JWT jwt = parse(token);
if (jwt instanceof PlainJWT) {
throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm());
}
return this.decode(jwt);
}
private JWT parse(String token) {
public Mono<Jwt> decode(String token) {
try {
return JWTParser.parse(token);
JWT jwt = JWTParser.parse(token);
if (jwt instanceof PlainJWT) {
return Mono.error(new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()));
}
return this.decode(jwt);
}
catch (Exception ex) {
throw new BadJwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex);
return Mono.error(new BadJwtException(
"An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -43,23 +43,34 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
*/
private final AtomicReference<Mono<JWKSet>> cachedJWKSet = new AtomicReference<>(Mono.empty());
/**
* The cached JWK set URL.
*/
private final AtomicReference<String> cachedJwkSetUrl = new AtomicReference<>();
private WebClient webClient = WebClient.create();
private final String jwkSetURL;
private final Mono<String> jwkSetUrlProvider;
ReactiveRemoteJWKSource(String jwkSetURL) {
Assert.hasText(jwkSetURL, "jwkSetURL cannot be empty");
this.jwkSetURL = jwkSetURL;
this.jwkSetUrlProvider = Mono.just(jwkSetURL);
}
ReactiveRemoteJWKSource(Mono<String> jwkSetUrlProvider) {
Assert.notNull(jwkSetUrlProvider, "jwkSetUrlProvider cannot be null");
this.jwkSetUrlProvider = Mono.fromCallable(this.cachedJwkSetUrl::get)
.switchIfEmpty(Mono.defer(() -> jwkSetUrlProvider.doOnNext(this.cachedJwkSetUrl::set)));
}
@Override
public Mono<List<JWK>> get(JWKSelector jwkSelector) {
// @formatter:off
return this.cachedJWKSet.get()
.switchIfEmpty(Mono.defer(() -> getJWKSet()))
.switchIfEmpty(Mono.defer(this::getJWKSet))
.flatMap((jwkSet) -> get(jwkSelector, jwkSet))
.switchIfEmpty(Mono.defer(() -> getJWKSet()
.map((jwkSet) -> jwkSelector.select(jwkSet)))
.map(jwkSelector::select))
);
// @formatter:on
}
@@ -95,13 +106,15 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
*/
private Mono<JWKSet> getJWKSet() {
// @formatter:off
return this.webClient.get()
.uri(this.jwkSetURL)
.retrieve()
.bodyToMono(String.class)
return this.jwkSetUrlProvider
.flatMap((jwkSetURL) -> this.webClient.get()
.uri(jwkSetURL)
.retrieve()
.bodyToMono(String.class)
)
.map(this::parse)
.doOnNext((jwkSet) -> this.cachedJWKSet
.set(Mono.just(jwkSet))
.set(Mono.just(jwkSet))
)
.cache();
// @formatter:on
@@ -97,6 +97,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*/
public class NimbusJwtDecoderTests {
private static final String MALFORMED_TOKEN = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9";
private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}";
@@ -194,6 +196,12 @@ public class NimbusJwtDecoderTests {
// @formatter:on
}
@Test
public void decodeWhenTokenMalformedThenReturnsMalformedTokenMessage() {
assertThatExceptionOfType(BadJwtException.class).isThrownBy(() -> this.jwtDecoder.decode(MALFORMED_TOKEN))
.withMessage("An error occurred while attempting to decode the Jwt: Malformed token");
}
@Test
public void decodeWhenJwtFailsValidationThenReturnsCorrespondingErrorMessage() {
OAuth2Error failure = new OAuth2Error("mock-error", "mock-description", "mock-uri");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.jwt;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
@@ -31,10 +32,16 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.BDDMockito.willThrow;
/**
* @author Rob Winch
@@ -52,6 +59,9 @@ public class ReactiveRemoteJWKSourceTests {
private MockWebServer server;
@Mock
private Supplier<String> mockStringSupplier;
// @formatter:off
private String keys = "{\n"
+ " \"keys\": [\n"
@@ -156,4 +166,18 @@ public class ReactiveRemoteJWKSourceTests {
assertThat(this.source.get(this.selector).block()).isEmpty();
}
@Test
public void getShouldRecoverAndReturnKeysAfterErrorCase() {
given(this.matcher.matches(any())).willReturn(true);
this.source = new ReactiveRemoteJWKSource(Mono.fromSupplier(this.mockStringSupplier));
willThrow(WebClientResponseException.ServiceUnavailable.class).given(this.mockStringSupplier).get();
// first case: id provider has error state
assertThatExceptionOfType(WebClientResponseException.ServiceUnavailable.class)
.isThrownBy(() -> this.source.get(this.selector).block());
// second case: id provider is healthy again
willReturn(this.server.url("/").toString()).given(this.mockStringSupplier).get();
List<JWK> actual = this.source.get(this.selector).block();
assertThat(actual).isNotEmpty();
}
}
@@ -167,10 +167,12 @@ class OpenSamlAuthenticationRequestResolver {
.samlRequest(deflatedAndEncoded)
.relayState(relayState);
if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) {
Map<String, String> parameters = OpenSamlSigningUtils.sign(registration)
.param(Saml2ParameterNames.SAML_REQUEST, deflatedAndEncoded)
.param(Saml2ParameterNames.RELAY_STATE, relayState)
.parameters();
OpenSamlSigningUtils.QueryParametersPartial parametersPartial = OpenSamlSigningUtils.sign(registration)
.param(Saml2ParameterNames.SAML_REQUEST, deflatedAndEncoded);
if (relayState != null) {
parametersPartial = parametersPartial.param(Saml2ParameterNames.RELAY_STATE, relayState);
}
Map<String, String> parameters = parametersPartial.parameters();
builder.sigAlg(parameters.get(Saml2ParameterNames.SIG_ALG))
.signature(parameters.get(Saml2ParameterNames.SIGNATURE));
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
@@ -190,14 +189,14 @@ public class OpenSamlAuthenticationRequestFactoryTests {
@Test
public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() {
AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST);
Assertions.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding());
assertThat(SAMLConstants.SAML2_POST_BINDING_URI).isEqualTo(authn.getProtocolBinding());
}
@Test
public void createAuthenticationRequestWhenSetUriThenReturnsCorrectBinding() {
this.factory.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST);
Assertions.assertEquals(SAMLConstants.SAML2_REDIRECT_BINDING_URI, authn.getProtocolBinding());
assertThat(SAMLConstants.SAML2_REDIRECT_BINDING_URI).isEqualTo(authn.getProtocolBinding());
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
@@ -190,7 +189,7 @@ public class OpenSaml4AuthenticationRequestFactoryTests {
@Test
public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() {
AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST);
Assertions.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding());
assertThat(SAMLConstants.SAML2_POST_BINDING_URI).isEqualTo(authn.getProtocolBinding());
}
@Test
@@ -16,12 +16,15 @@
package org.springframework.security.saml2.provider.service.web.authentication;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.mockito.MockedStatic;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
@@ -32,6 +35,12 @@ import org.springframework.security.saml2.provider.service.registration.TestRely
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OpenSamlAuthenticationRequestResolver}
@@ -40,7 +49,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
private RelyingPartyRegistration.Builder relyingPartyRegistrationBuilder;
@Before
@BeforeEach
public void setUp() {
this.relyingPartyRegistrationBuilder = TestRelyingPartyRegistrations.relyingPartyRegistration();
}
@@ -102,7 +111,9 @@ public class OpenSamlAuthenticationRequestResolverTests {
.assertingPartyDetails((party) -> party.verificationX509Credentials((c) -> c.add(credential)))
.build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> resolver.resolve(request, null));
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> resolver.resolve(request, (r, authnRequest) -> {
}));
}
@Test
@@ -161,7 +172,8 @@ public class OpenSamlAuthenticationRequestResolverTests {
(party) -> party.signingAlgorithms((algs) -> algs.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1)))
.build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, null);
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
});
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isNotNull();
assertThat(result.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
@@ -169,6 +181,58 @@ public class OpenSamlAuthenticationRequestResolverTests {
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
}
@Test
public void resolveAuthenticationRequestWhenSignedAndRelayStateIsNullThenSignsWithoutRelayState() {
try (MockedStatic<OpenSamlSigningUtils> openSamlSigningUtilsMockedStatic = mockStatic(
OpenSamlSigningUtils.class, Answers.CALLS_REAL_METHODS)) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setPathInfo("/saml2/authenticate/registration-id");
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(true))
.build();
OpenSamlSigningUtils.QueryParametersPartial queryParametersPartialSpy = spy(
new OpenSamlSigningUtils.QueryParametersPartial(registration));
openSamlSigningUtilsMockedStatic.when(() -> OpenSamlSigningUtils.sign(any()))
.thenReturn(queryParametersPartialSpy);
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
resolver.setRelayStateResolver((source) -> null);
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
});
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isNull();
assertThat(result.getSigAlg()).isNotNull();
assertThat(result.getSignature()).isNotNull();
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
verify(queryParametersPartialSpy, never()).param(eq(Saml2ParameterNames.RELAY_STATE), any());
}
}
@Test
public void resolveAuthenticationRequestWhenSignedAndRelayStateIsEmptyThenSignsWithEmptyRelayState() {
try (MockedStatic<OpenSamlSigningUtils> openSamlSigningUtilsMockedStatic = mockStatic(
OpenSamlSigningUtils.class, Answers.CALLS_REAL_METHODS)) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setPathInfo("/saml2/authenticate/registration-id");
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(true))
.build();
OpenSamlSigningUtils.QueryParametersPartial queryParametersPartialSpy = spy(
new OpenSamlSigningUtils.QueryParametersPartial(registration));
openSamlSigningUtilsMockedStatic.when(() -> OpenSamlSigningUtils.sign(any()))
.thenReturn(queryParametersPartialSpy);
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
resolver.setRelayStateResolver((source) -> "");
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
});
assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isEmpty();
assertThat(result.getSigAlg()).isNotNull();
assertThat(result.getSignature()).isNotNull();
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
verify(queryParametersPartialSpy).param(eq(Saml2ParameterNames.RELAY_STATE), eq(""));
}
}
private OpenSamlAuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistration registration) {
return new OpenSamlAuthenticationRequestResolver((request, id) -> registration);
}
@@ -18,7 +18,6 @@ package org.springframework.security.saml2.provider.service.web.authentication;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -46,6 +45,7 @@ import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
@@ -94,7 +94,7 @@ public class Saml2WebSsoAuthenticationFilterTests {
@Test
public void requiresAuthenticationWhenHappyPathThenReturnsTrue() {
Assertions.assertTrue(this.filter.requiresAuthentication(this.request, this.response));
assertThat(this.filter.requiresAuthentication(this.request, this.response)).isTrue();
}
@Test
@@ -102,7 +102,7 @@ public class Saml2WebSsoAuthenticationFilterTests {
this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/some/other/path/{registrationId}");
this.request.setPathInfo("/some/other/path/idp-registration-id");
this.request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "xml-data-goes-here");
Assertions.assertTrue(this.filter.requiresAuthentication(this.request, this.response));
assertThat(this.filter.requiresAuthentication(this.request, this.response)).isTrue();
}
@Test
@@ -143,7 +143,7 @@ public class Saml2WebSsoAuthenticationFilterTests {
this.filter.setAuthenticationDetailsSource(authenticationDetailsSource);
this.request.setPathInfo("/some/other/path/idp-registration-id");
this.filter.attemptAuthentication(this.request, this.response);
Assertions.assertEquals(details, token.getDetails());
assertThat(token.getDetails()).isEqualTo(details);
}
@Test
+1 -1
View File
@@ -6,7 +6,7 @@ pluginManagement {
plugins {
id "com.gradle.enterprise" version "3.11.4"
id "io.spring.ge.conventions" version "0.0.14"
id "io.spring.ge.conventions" version "0.0.15"
}
dependencyResolutionManagement {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -36,6 +36,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import org.springframework.web.util.HtmlUtils;
@@ -244,7 +245,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
if (session != null) {
AuthenticationException ex = (AuthenticationException) session
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials";
errorMsg = (ex != null && StringUtils.hasLength(ex.getMessage())) ? ex.getMessage()
: "Invalid credentials";
}
}
String contextPath = request.getContextPath();
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -130,9 +130,13 @@ public class StrictHttpFirewall implements HttpFirewall {
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
private static final Pattern HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[[^\\p{IsControl}]||\\t]]*");
private static final Predicate<String> HEADER_VALUE_PREDICATE = (s) -> HEADER_VALUE_PATTERN.matcher(s).matches();
private Predicate<String> allowedHeaderNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
private Predicate<String> allowedHeaderValues = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
private Predicate<String> allowedHeaderValues = HEADER_VALUE_PREDICATE;
private Predicate<String> allowedParameterNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
@@ -122,7 +122,7 @@ public final class XXssProtectionHeaderWriter implements HeaderWriter {
* specify mode as blocked. The content will be replaced with "#". For example:
*
* <pre>
* X-XSS-Protection: 1 ; mode=block
* X-XSS-Protection: 1; mode=block
* </pre>
* @param headerValue the new header value
* @throws IllegalArgumentException when headerValue is null
@@ -134,7 +134,7 @@ public final class XXssProtectionHeaderWriter implements HeaderWriter {
}
/**
* The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block"
* The value of the x-xss-protection header. One of: "0", "1", "1; mode=block"
*
* @author Daniel Garnier-Moiroux
* @since 5.8
@@ -122,7 +122,7 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW
* specify mode as blocked. The content will be replaced with "#". For example:
*
* <pre>
* X-XSS-Protection: 1 ; mode=block
* X-XSS-Protection: 1; mode=block
* </pre>
* @param headerValue the new headerValue
* @throws IllegalArgumentException if headerValue is null
@@ -135,14 +135,14 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW
}
/**
* The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block"
* The value of the x-xss-protection header. One of: "0", "1", "1; mode=block"
*
* @author Daniel Garnier-Moiroux
* @since 5.8
*/
public enum HeaderValue {
DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1 ; mode=block");
DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1; mode=block");
private final String value;
@@ -182,6 +182,20 @@ public class DefaultLoginPageGeneratingFilterTests {
.contains("<a href=\"/saml/sso/google\">Google &lt; &gt; &quot; &#39; &amp;</a>");
} // Fake OpenID filter (since it's not in this module
// gh-13768
@Test
public void generatesWhenExceptionWithEmptyMessageThenInvalidCredentials() throws Exception {
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(
new UsernamePasswordAuthenticationFilter());
filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/login");
request.setQueryString("error");
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new BadCredentialsException(null));
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, this.chain);
assertThat(response.getContentAsString()).contains("Invalid credentials");
}
@SuppressWarnings("unused")
private static class MockProcessingFilter extends AbstractAuthenticationProcessingFilter {
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -782,6 +782,13 @@ public class StrictHttpFirewallTests {
assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Something"));
}
@Test
public void getFirewalledRequestGetHeaderWhenHorizontalTabInHeaderValueThenNoException() {
this.request.addHeader("Something", "tab\tvalue");
HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
assertThat(request.getHeader("Something")).isEqualTo("tab\tvalue");
}
@Test
public void getFirewalledRequestGetHeaderWhenUndefinedCharacterInHeaderValueThenException() {
this.request.addHeader("Something", "bad\uFFFEvalue");
@@ -49,7 +49,7 @@ public class XXssProtectionServerHttpHeadersWriterTests {
this.writer.writeHttpHeaders(this.exchange);
assertThat(this.headers).hasSize(1);
assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION))
.containsOnly("1 ; mode=block");
.containsOnly("1; mode=block");
}
@Test
@@ -99,7 +99,7 @@ public class XXssProtectionServerHttpHeadersWriterTests {
this.writer.writeHttpHeaders(this.exchange);
assertThat(this.headers).hasSize(1);
assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION))
.containsOnly("1 ; mode=block");
.containsOnly("1; mode=block");
}
}