1
0
mirror of synced 2026-07-08 12:20:02 +00:00

Compare commits

...

90 Commits

Author SHA1 Message Date
github-actions[bot] 5d3c0621d1 Release 6.3.0 2024-05-20 15:21:36 +00:00
Marcus Hert Da Coregio a5976b120d Use new version of update-antora-ui-spring action
Issue gh-15106
2024-05-20 11:51:02 -03:00
Marcus Hert Da Coregio 5d6ba3015a Merge branch '6.2.x'
Closes gh-15113
2024-05-20 11:11:38 -03:00
Marcus Hert Da Coregio f39de00179 Polish workflow
Issue gh-15106
2024-05-20 09:36:59 -03:00
Marcus Hert Da Coregio 9d831fe14e Fix action repository name
Issue gh-15106
2024-05-20 09:32:47 -03:00
Marcus Hert Da Coregio 4541e2f7ad Use update-antora-ui-spring action from spring-doc-actions
Closes gh-15106
2024-05-20 09:31:08 -03:00
dependabot[bot] a2a87a1271 Bump com.gradle.develocity from 3.17.3 to 3.17.4
Bumps com.gradle.develocity from 3.17.3 to 3.17.4.

---
updated-dependencies:
- dependency-name: com.gradle.develocity
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-19 21:07:54 -07:00
github-actions[bot] 40a0e8d47c Merge branch '6.2.x' 2024-05-20 04:07:30 +00:00
dependabot[bot] 8317db8525 Bump org.springframework.data:spring-data-bom
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.0.0-RC1 to 2024.0.0.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.0.0-RC1...2024.0.0)

---
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>
2024-05-19 21:04:29 -07:00
github-actions[bot] c0b3d24f95 Merge branch '6.2.x' 2024-05-20 04:03:21 +00:00
dependabot[bot] 6bb17fa917 Bump org.springframework:spring-framework-bom from 6.1.6 to 6.1.7
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.6 to 6.1.7.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.6...v6.1.7)

---
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-05-16 20:49:51 -07:00
dependabot[bot] b5b105070d Bump io.mockk:mockk from 1.13.10 to 1.13.11
Bumps [io.mockk:mockk](https://github.com/mockk/mockk) from 1.13.10 to 1.13.11.
- [Release notes](https://github.com/mockk/mockk/releases)
- [Commits](https://github.com/mockk/mockk/compare/1.13.10...1.13.11)

---
updated-dependencies:
- dependency-name: io.mockk:mockk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-16 20:44:38 -07:00
dependabot[bot] 76f962e0e6 Bump org-eclipse-jetty from 11.0.20 to 11.0.21
Bumps `org-eclipse-jetty` from 11.0.20 to 11.0.21.

Updates `org.eclipse.jetty:jetty-server` from 11.0.20 to 11.0.21

Updates `org.eclipse.jetty:jetty-servlet` from 11.0.20 to 11.0.21

---
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-05-16 20:39:36 -07:00
github-actions[bot] 0010d560e4 Merge branch '6.2.x' 2024-05-17 03:27:03 +00:00
github-actions[bot] 51b02c273f Merge branch '6.2.x' 2024-05-17 03:25:48 +00:00
vvaadd 0a6e50d892 Minor code fix 2024-05-15 13:36:54 -03:00
Hyeon Sung 742c95b1fc Use instanceof Pattern Matching 2024-05-15 08:32:25 -03:00
dependabot[bot] 6ec81b1aec Bump io.projectreactor:reactor-bom from 2023.0.5 to 2023.0.6
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.5 to 2023.0.6.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2023.0.5...2023.0.6)

---
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-05-14 20:51:52 -07:00
github-actions[bot] a757fe88e5 Merge branch '6.2.x' 2024-05-15 03:32:57 +00:00
github-actions[bot] 398a39649f Merge branch '6.2.x' 2024-05-15 03:25:23 +00:00
Kohei Tamura e0d6699f30 Fix incorrect links 2024-05-14 10:13:43 -03:00
Joaquin Santana 927840fe88 Do Not Invalidate Current Session When It Is Registered
Closes gh-15066
2024-05-14 10:01:54 -03:00
github-actions[bot] e321e5de7c Merge branch '6.2.x' 2024-05-14 04:12:02 +00:00
dependabot[bot] 0502078174 Bump io.micrometer:micrometer-observation from 1.12.5 to 1.12.6
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.12.5 to 1.12.6.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.5...v1.12.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 20:29:56 -07:00
dependabot[bot] a17a599bd5 Bump org.jfrog.buildinfo:build-info-extractor-gradle
Bumps [org.jfrog.buildinfo:build-info-extractor-gradle](https://github.com/jfrog/build-info) from 4.33.13 to 4.33.15.
- [Release notes](https://github.com/jfrog/build-info/releases)
- [Changelog](https://github.com/jfrog/build-info/blob/master/RELEASE.md)
- [Commits](https://github.com/jfrog/build-info/compare/build-info-gradle-extractor-4.33.13...build-info-gradle-extractor-4.33.15)

---
updated-dependencies:
- dependency-name: org.jfrog.buildinfo:build-info-extractor-gradle
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 10:32:52 -07:00
Marcus Hert Da Coregio 76ca0ee52f Merge branch '6.2.x'
Closes gh-15065
2024-05-13 11:23:34 -03:00
MrJovanovic13 e932387714 fix docs error
Closes gh-14978
2024-05-13 09:28:27 -03:00
Marcus Hert Da Coregio 28f2f895f1 Merge branch '6.2.x'
Closes gh-15061
2024-05-13 08:53:19 -03:00
github-actions[bot] d47a7bd536 Merge branch '6.2.x' 2024-05-13 04:01:33 +00:00
github-actions[bot] 1c663466d0 Merge branch '6.2.x' 2024-05-13 04:00:41 +00:00
dependabot[bot] 76a4a03a63 Bump com.gradle.develocity from 3.17.2 to 3.17.3
Bumps com.gradle.develocity from 3.17.2 to 3.17.3.

---
updated-dependencies:
- dependency-name: com.gradle.develocity
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-12 20:29:23 -07:00
github-actions[bot] c132e7cd7e Merge branch '6.2.x' 2024-05-13 03:28:51 +00:00
dependabot[bot] 19f2775d00 Bump org-aspectj from 1.9.22 to 1.9.22.1
Bumps `org-aspectj` from 1.9.22 to 1.9.22.1.

Updates `org.aspectj:aspectjrt` from 1.9.22 to 1.9.22.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.22 to 1.9.22.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-05-12 20:28:11 -07:00
dependabot[bot] 126ef5208e Bump org.jetbrains.kotlinx:kotlinx-coroutines-bom from 1.8.0 to 1.8.1
Bumps [org.jetbrains.kotlinx:kotlinx-coroutines-bom](https://github.com/Kotlin/kotlinx.coroutines) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md)
- [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.8.0...1.8.1)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-12 20:27:27 -07:00
Steve Riesenberg 746ee27dff Add labels for dependabot updates to GitHub Actions
Issue gh-14485
2024-05-09 13:44:56 -05:00
Steve Riesenberg 495403b1b6 Merge branch '6.2.x'
Closes gh-15039
2024-05-09 13:42:51 -05:00
github-actions[bot] 03607f59f3 Merge branch '6.2.x' 2024-05-09 03:46:05 +00:00
github-actions[bot] 2b1cc2ab9e Merge branch '6.2.x' 2024-05-09 03:41:09 +00:00
Steve Riesenberg db8e563b24 Merge branch '6.2.x' 2024-05-08 16:54:35 -05:00
Steve Riesenberg ad00852975 Merge branch '6.2.x' 2024-05-08 14:55:48 -05:00
Marcus Hert Da Coregio e6782d4448 Merge branch '6.2.x' 2024-05-08 13:51:18 -03:00
dependabot[bot] 1ea0e6cb48 Bump org.jetbrains.kotlin:kotlin-gradle-plugin from 1.9.23 to 1.9.24
Bumps [org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) from 1.9.23 to 1.9.24.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.24/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.23...v1.9.24)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-07 20:37:54 -07:00
dependabot[bot] 771096d07f Bump org.jetbrains.kotlin:kotlin-bom from 1.9.23 to 1.9.24
Bumps [org.jetbrains.kotlin:kotlin-bom](https://github.com/JetBrains/kotlin) from 1.9.23 to 1.9.24.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.24/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.23...v1.9.24)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-07 20:35:50 -07:00
github-actions[bot] ed0d46aba5 Merge branch '6.2.x' 2024-05-08 03:32:02 +00:00
github-actions[bot] f92abf0220 Merge branch '6.2.x' 2024-05-08 03:31:07 +00:00
Steve Riesenberg aaf7044721 Merge branch '6.2.x' 2024-05-06 16:05:01 -05:00
github-actions[bot] e54175511c Merge branch '6.2.x' 2024-05-06 04:04:51 +00:00
dependabot[bot] 9c8c9b87b4 Bump com.fasterxml.jackson:jackson-bom from 2.17.0 to 2.17.1
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.17.0 to 2.17.1.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.17.0...jackson-bom-2.17.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-05 20:25:13 -07:00
Steve Riesenberg a6abe5d96e Merge branch '6.2.x' 2024-05-03 17:04:35 -05:00
Marcus Hert Da Coregio 8024b573fd Try using default token with custom permissions
Issue gh-14993
2024-05-03 13:32:42 -03:00
Marcus Hert Da Coregio 62b3fe2e38 Run Update Antora UI Spring Workflow Every Day
Closes gh-14993
2024-05-03 12:01:31 -03:00
Marcus Hert Da Coregio 3bdb18468f Update workflow to remove additional arguments causing problems
Issue gh-14993
2024-05-03 11:57:48 -03:00
Marcus Hert Da Coregio fa282f3452 Update workflow to use fetch-depth = 1
Issue gh-14993
2024-05-03 11:46:13 -03:00
Marcus Hert Da Coregio 2462fb98f4 Use new update-antora-ui-spring workflow version
Issue gh-14993
2024-05-03 11:42:27 -03:00
Marcus Hert Da Coregio e225a9df14 Add update-antora-ui-spring.yml workflow
Issue gh-14993
2024-05-03 11:21:47 -03:00
dependabot[bot] e92dd02c57 Bump com.github.spullara.mustache.java:compiler from 0.9.12 to 0.9.13
Bumps [com.github.spullara.mustache.java:compiler](https://github.com/spullara/mustache.java) from 0.9.12 to 0.9.13.
- [Commits](https://github.com/spullara/mustache.java/compare/mustache.java-0.9.12...mustache.java-0.9.13)

---
updated-dependencies:
- dependency-name: com.github.spullara.mustache.java:compiler
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 20:42:38 -07:00
github-actions[bot] 095403fc03 Merge branch '6.2.x' 2024-05-02 03:27:17 +00:00
github-actions[bot] 21bfc6c958 Merge branch '6.2.x' 2024-05-02 03:21:42 +00:00
Rishi Raj 0202409dae Proofread documentation 2024-04-30 09:14:54 -03:00
Marcus Hert Da Coregio a48b17d016 Merge branch '6.2.x'
Closes gh-14996
2024-04-30 08:59:19 -03:00
madorb 1ae99be1cf Clarify Package Import
Provide more explict instructions regarding the necessary import
to make the Kotlin DSL work. For some reason it took me 10 minutes
to figure this out based on the existing doc.
2024-04-30 08:48:57 -03:00
Marcus Hert Da Coregio b3c7f3ff19 Rename CompromisedPasswordCheckResult to CompromisedPasswordDecision
Issue gh-7395
2024-04-30 08:38:03 -03:00
dependabot[bot] 1ca381c364 Bump com.github.spullara.mustache.java:compiler from 0.9.11 to 0.9.12
Bumps [com.github.spullara.mustache.java:compiler](https://github.com/spullara/mustache.java) from 0.9.11 to 0.9.12.
- [Commits](https://github.com/spullara/mustache.java/compare/mustache.java-0.9.11...mustache.java-0.9.12)

---
updated-dependencies:
- dependency-name: com.github.spullara.mustache.java:compiler
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 20:55:34 -07:00
dependabot[bot] 3c31d72e22 Bump org-apache-maven-resolver from 1.9.19 to 1.9.20
Bumps `org-apache-maven-resolver` from 1.9.19 to 1.9.20.

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

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

Updates `org.apache.maven.resolver:maven-resolver-transport-http` from 1.9.19 to 1.9.20

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 20:54:54 -07:00
github-actions[bot] 9d769e28a2 Merge branch '6.2.x' 2024-04-30 03:37:21 +00:00
github-actions[bot] 1e2d329d6e Merge branch '6.2.x' 2024-04-30 03:35:56 +00:00
github-actions[bot] 0a868a22a7 Merge branch '6.2.x' 2024-04-30 03:35:07 +00:00
Steve Riesenberg d8fe5e7b88 Update What's New for 6.3 2024-04-29 15:26:46 -05:00
Steve Riesenberg 2dd908dff8 Document additional client authenticating methods
Issue gh-11440
Closes gh-14982
2024-04-29 15:07:46 -05:00
Steve Riesenberg 2598bf8c37 Polish gh-14859 2024-04-29 15:07:45 -05:00
Crain-32 d0adb2aa70 Simplify Disabling Encoding Client ID and Secret
Closes gh-11440
2024-04-29 14:46:12 -05:00
github-actions[bot] f7dd76df64 Merge branch '6.2.x' 2024-04-29 03:35:07 +00:00
dependabot[bot] f6ef34a535 Bump org.hibernate.orm:hibernate-core from 6.4.7.Final to 6.4.8.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.4.7.Final to 6.4.8.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.8/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.7...6.4.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-28 20:31:09 -07:00
Josh Cummings 1825dcbe33 Merge branch '6.2.x' 2024-04-26 17:21:25 -06:00
Josh Cummings 9f125afc81 Merge branch '6.2.x' 2024-04-26 17:14:00 -06:00
Josh Cummings 47775f5167 Merge branch '6.2.x' 2024-04-26 17:09:29 -06:00
Josh Cummings dde3200677 Merge branch '6.2.x' 2024-04-26 15:38:27 -06:00
dependabot[bot] 9813ad4002 Bump org.hibernate.orm:hibernate-core from 6.4.6.Final to 6.4.7.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.4.6.Final to 6.4.7.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.7/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.6...6.4.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-25 20:39:02 -07:00
github-actions[bot] 19aac66f21 Merge branch '6.2.x' 2024-04-26 03:25:21 +00:00
Josh Cummings 89d935f1ca Merge branch '6.2.x' 2024-04-25 16:12:29 -06:00
Josh Cummings 11421c6385 Merge branch '6.2.x' 2024-04-25 14:03:27 -06:00
DingHao 2a6f0cac5a Fix not exist class in java doc
Closes gh-14954
2024-04-25 11:37:23 -06:00
Daniel Garnier-Moiroux 7ddc00521e Improve logging for Global Authentication
Closes gh-14663
2024-04-25 11:35:59 -06:00
github-actions[bot] 23dc0edb97 Merge branch '6.2.x' 2024-04-25 03:48:31 +00:00
dependabot[bot] 97b4b75d6d Bump org.hibernate.orm:hibernate-core from 6.4.5.Final to 6.4.6.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.4.5.Final to 6.4.6.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.6/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.5...6.4.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-24 20:29:14 -07:00
Josh Cummings 7e9c21286d Merge branch '6.2.x' 2024-04-24 11:11:40 -06:00
github-actions[bot] 3f3051ef95 Merge branch '6.2.x' 2024-04-24 04:07:05 +00:00
github-actions[bot] bcc552ec48 Merge branch '6.2.x' 2024-04-24 04:01:57 +00:00
dependabot[bot] 1121a40a3b Bump org.hibernate.orm:hibernate-core from 6.4.4.Final to 6.4.5.Final
Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.4.4.Final to 6.4.5.Final.
- [Release notes](https://github.com/hibernate/hibernate-orm/releases)
- [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.5/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.4...6.4.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-23 20:45:05 -07:00
Josh Cummings d6085feab8 Merge branch '6.2.x' 2024-04-23 08:32:33 -06:00
329 changed files with 31168 additions and 1417 deletions
+41
View File
@@ -0,0 +1,41 @@
version: 2
registries:
spring-milestones:
type: maven-repository
url: https://repo.spring.io/milestone
updates:
- package-ecosystem: "gradle"
target-branch: "main"
directory: "/"
schedule:
interval: "daily"
time: "03:00"
timezone: "Etc/UTC"
labels: [ "type: dependency-upgrade" ]
registries:
- "spring-milestones"
ignore:
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
- dependency-name: "org.python:jython" # jython updates break integration tests
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
- dependency-name: "org.junit:junit-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "org.mockito:mockito-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "*"
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
# GitHub Actions
- package-ecosystem: github-actions
target-branch: "main"
directory: "/"
schedule:
interval: weekly
ignore:
- dependency-name: "sjohnr/*"
- dependency-name: "spring-io/*"
- dependency-name: "spring-security-release-tools/*"
+163 -59
View File
@@ -1,72 +1,176 @@
version: 2
registries:
spring-milestones:
type: maven-repository
url: https://repo.spring.io/milestone
updates:
- package-ecosystem: "gradle"
target-branch: "main"
milestone: 319 # 6.2.x
directory: "/"
- package-ecosystem: gradle
target-branch: 5.8.x
directory: /
schedule:
interval: "daily"
time: "03:00"
timezone: "Etc/UTC"
labels: [ "type: dependency-upgrade" ]
interval: daily
time: '03:00'
timezone: Etc/UTC
labels:
- 'type: dependency-upgrade'
registries:
- "spring-milestones"
- spring-milestones
ignore:
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
- dependency-name: "org.python:jython" # jython updates break integration tests
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
- dependency-name: "org.junit:junit-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "org.mockito:mockito-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "*"
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
- package-ecosystem: "gradle"
target-branch: "6.1.x"
milestone: 318 # 6.1.x
directory: "/"
- dependency-name: com.nimbusds:nimbus-jose-jwt
- dependency-name: org.python:jython
- dependency-name: org.apache.directory.server:*
- dependency-name: org.junit:junit-bom
update-types:
- version-update:semver-major
- dependency-name: org.mockito:mockito-bom
update-types:
- version-update:semver-major
- dependency-name: '*'
update-types:
- version-update:semver-major
- version-update:semver-minor
- package-ecosystem: gradle
target-branch: 6.1.x
directory: /
schedule:
interval: "daily"
time: "03:00"
timezone: "Etc/UTC"
labels: [ "type: dependency-upgrade" ]
interval: daily
time: '03:00'
timezone: Etc/UTC
labels:
- 'type: dependency-upgrade'
registries:
- spring-milestones
ignore:
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
- dependency-name: "org.python:jython" # jython updates break integration tests
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
- dependency-name: "org.junit:junit-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "org.mockito:mockito-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "*"
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
- package-ecosystem: "gradle"
target-branch: "5.8.x"
milestone: 246 # 5.8.x
directory: "/"
- dependency-name: com.nimbusds:nimbus-jose-jwt
- dependency-name: org.python:jython
- dependency-name: org.apache.directory.server:*
- dependency-name: org.junit:junit-bom
update-types:
- version-update:semver-major
- dependency-name: org.mockito:mockito-bom
update-types:
- version-update:semver-major
- dependency-name: '*'
update-types:
- version-update:semver-major
- version-update:semver-minor
- package-ecosystem: gradle
target-branch: 6.2.x
directory: /
schedule:
interval: "daily"
time: "03:00"
timezone: "Etc/UTC"
labels: [ "type: dependency-upgrade" ]
interval: daily
time: '03:00'
timezone: Etc/UTC
labels:
- 'type: dependency-upgrade'
registries:
- spring-milestones
ignore:
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
- dependency-name: "org.python:jython" # jython updates break integration tests
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
- dependency-name: "io.mockk:mockk" # mockk updates break tests
- dependency-name: "org.opensaml:*" # org.opensaml maintains two different versions, so it must be updated manually
- dependency-name: "org.junit:junit-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "org.mockito:mockito-bom"
update-types: [ "version-update:semver-major" ]
- dependency-name: "*"
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
- dependency-name: com.nimbusds:nimbus-jose-jwt
- dependency-name: org.python:jython
- dependency-name: org.apache.directory.server:*
- dependency-name: org.junit:junit-bom
update-types:
- version-update:semver-major
- dependency-name: org.mockito:mockito-bom
update-types:
- version-update:semver-major
- dependency-name: '*'
update-types:
- version-update:semver-major
- version-update:semver-minor
- package-ecosystem: gradle
target-branch: main
directory: /
schedule:
interval: daily
time: '03:00'
timezone: Etc/UTC
labels:
- 'type: dependency-upgrade'
registries:
- spring-milestones
ignore:
- dependency-name: com.nimbusds:nimbus-jose-jwt
- dependency-name: org.python:jython
- dependency-name: org.apache.directory.server:*
- dependency-name: org.junit:junit-bom
update-types:
- version-update:semver-major
- dependency-name: org.mockito:mockito-bom
update-types:
- version-update:semver-major
- dependency-name: com.gradle.enterprise
update-types:
- version-update:semver-major
- version-update:semver-minor
- dependency-name: '*'
update-types:
- version-update:semver-major
- version-update:semver-minor
- package-ecosystem: github-actions
target-branch: 5.8.x
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
ignore:
- dependency-name: sjohnr/*
- dependency-name: spring-io/*
- dependency-name: spring-security-release-tools/*
- package-ecosystem: github-actions
target-branch: 6.1.x
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
ignore:
- dependency-name: sjohnr/*
- dependency-name: spring-io/*
- dependency-name: spring-security-release-tools/*
- package-ecosystem: github-actions
target-branch: 6.2.x
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
ignore:
- dependency-name: sjohnr/*
- dependency-name: spring-io/*
- dependency-name: spring-security-release-tools/*
- package-ecosystem: github-actions
target-branch: main
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
ignore:
- dependency-name: sjohnr/*
- dependency-name: spring-io/*
- dependency-name: spring-security-release-tools/*
- package-ecosystem: github-actions
target-branch: docs-build
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
ignore:
- dependency-name: sjohnr/*
- dependency-name: spring-io/*
- dependency-name: spring-security-release-tools/*
- package-ecosystem: npm
target-branch: docs-build
directory: /
schedule:
interval: weekly
@@ -0,0 +1,57 @@
name: Auto Merge Forward Dependabot Commits
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: dependabot-auto-merge-forward
jobs:
get-supported-branches:
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
with:
project: spring-security
type: oss
repository_name: spring-projects/spring-security
auto-merge-forward-dependabot:
name: Auto Merge Forward Dependabot Commits
runs-on: ubuntu-latest
needs: [get-supported-branches]
permissions:
contents: write
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Setup GitHub User
id: setup-gh-user
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Run Auto Merge Forward
id: run-auto-merge-forward
uses: spring-io/spring-security-release-tools/.github/actions/auto-merge-forward@actions-v1
with:
branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
from-author: dependabot[bot]
notify_result:
name: Check for failures
needs: [ auto-merge-forward-dependabot ]
if: failure()
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Send Slack message
uses: Gamesight/slack-workflow-status@v1.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
# List of active maintenance branches.
branch: [ main, 6.1.x, 5.8.x ]
branch: [ main, 6.2.x, 6.1.x, 5.8.x ]
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -0,0 +1,29 @@
name: Update Antora UI Spring
on:
workflow_dispatch:
permissions:
pull-requests: write
issues: write
contents: write
jobs:
update-antora-ui-spring:
runs-on: ubuntu-latest
name: Update
strategy:
matrix:
branch: [ '5.8.x', '6.1.x', '6.2.x', 'main' ]
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@17ed79ea5fbd65813c69ef1062a024d4a37ff0ca
name: Update on Supported Branches
with:
docs-branch: ${{ matrix.branch }}
token: ${{ secrets.GITHUB_TOKEN }}
antora-file-path: 'docs/antora-playbook.yml'
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@17ed79ea5fbd65813c69ef1062a024d4a37ff0ca
name: Update on docs-build
with:
docs-branch: 'docs-build'
token: ${{ secrets.GITHUB_TOKEN }}
+36
View File
@@ -0,0 +1,36 @@
name: Update dependabot.yml
on:
workflow_dispatch:
permissions:
contents: read
jobs:
get-supported-branches:
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
with:
project: spring-security
type: oss
repository_name: spring-projects/spring-security
main:
runs-on: ubuntu-latest
needs: [get-supported-branches]
if: ${{ (github.repository == 'spring-projects/spring-security') && (github.ref == 'refs/heads/main') }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: spring-io/spring-security-release-tools/.github/actions/generate-dependabot-yml@actions-v1
name: Update dependabot.yml
with:
gradle-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
github-actions-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main,docs-build
gh-token: ${{ secrets.GITHUB_TOKEN }}
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Update dependabot.yml
+1 -1
View File
@@ -105,7 +105,7 @@ Once merged, the fix will be forwarded-ported to applicable branches including `
1. Create a local branch
If this is for an issue, consider a branch name with the issue number, like `gh-22276`.
[[write-tests]]
1. Add JUnit Tests for your changes
1. Add documentation and JUnit Tests for your changes.
[[update-copyright]]
1. In all files you edited, if the copyright header is of the form 2002-20xx, update the final copyright year to the current year.
[[add-since]]
+4 -4
View File
@@ -1,8 +1,8 @@
image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
image:https://github.com/spring-projects/spring-security/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions?query=workflow%3ACI"]
image:https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml"]
image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
= Spring Security
@@ -15,14 +15,14 @@ For a detailed list of features and access to the latest release, please visit h
Please see our https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[code of conduct]
== Downloading Artifacts
See https://docs.spring.io/spring-security/site/docs/current/reference/html5/#getting[Getting Spring Security] for how to obtain Spring Security.
See https://docs.spring.io/spring-security/reference/getting-spring-security.html[Getting Spring Security] for how to obtain Spring Security.
== Documentation
Be sure to read the https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/[Spring Security Reference].
Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation].
== Quick Start
See https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-hello[Hello Spring Security] to get started with a "Hello, World" application.
See https://docs.spring.io/spring-security/reference/servlet/getting-started.html[Hello Spring Security] to get started with a "Hello, World" application.
== Building from Source
Spring Security uses a https://gradle.org[Gradle]-based build system.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -103,6 +103,13 @@ public class PostAuthorizeAspectTests {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
}
@Test
public void nestedDenyAllPostAuthorizeDeniesAccess() {
SecurityContextHolder.getContext().setAuthentication(this.anne);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
}
interface SecuredInterface {
@PostAuthorize("hasRole('X')")
@@ -134,6 +141,10 @@ public class PostAuthorizeAspectTests {
privateMethod();
}
NestedObject myObject() {
return new NestedObject();
}
}
static class SecuredImplSubclass extends SecuredImpl {
@@ -157,4 +168,13 @@ public class PostAuthorizeAspectTests {
}
static class NestedObject {
@PostAuthorize("denyAll")
void denyAllMethod() {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -54,6 +54,11 @@ public class PostFilterAspectTests {
assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine");
}
@Test
public void nestedDenyAllPostFilterDeniesAccess() {
assertThat(this.prePostSecured.myObject().denyAllMethod()).isEmpty();
}
static class PrePostSecured {
@PostFilter("filterObject.startsWith('a')")
@@ -61,6 +66,19 @@ public class PostFilterAspectTests {
return objects;
}
NestedObject myObject() {
return new NestedObject();
}
}
static class NestedObject {
@PostFilter("filterObject == null")
List<String> denyAllMethod() {
return new ArrayList<>(List.of("deny"));
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -103,6 +103,13 @@ public class PreAuthorizeAspectTests {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
}
@Test
public void nestedDenyAllPreAuthorizeDeniesAccess() {
SecurityContextHolder.getContext().setAuthentication(this.anne);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
}
interface SecuredInterface {
@PreAuthorize("hasRole('X')")
@@ -134,6 +141,10 @@ public class PreAuthorizeAspectTests {
privateMethod();
}
NestedObject myObject() {
return new NestedObject();
}
}
static class SecuredImplSubclass extends SecuredImpl {
@@ -157,4 +168,13 @@ public class PreAuthorizeAspectTests {
}
static class NestedObject {
@PreAuthorize("denyAll")
void denyAllMethod() {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -54,6 +54,11 @@ public class PreFilterAspectTests {
assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine");
}
@Test
public void nestedDenyAllPreFilterDeniesAccess() {
assertThat(this.prePostSecured.myObject().denyAllMethod(new ArrayList<>(List.of("deny")))).isEmpty();
}
static class PrePostSecured {
@PreFilter("filterObject.startsWith('a')")
@@ -61,6 +66,19 @@ public class PreFilterAspectTests {
return objects;
}
NestedObject myObject() {
return new NestedObject();
}
}
static class NestedObject {
@PreFilter("filterObject == null")
List<String> denyAllMethod(List<String> list) {
return list;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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-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.
@@ -28,6 +28,7 @@ import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.util.Assert;
/**
@@ -61,6 +62,8 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
*/
private boolean encodeServiceUrlWithSessionId = true;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void afterPropertiesSet() {
Assert.hasLength(this.loginUrl, "loginUrl must be specified");
@@ -74,8 +77,7 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
String urlEncodedService = createServiceUrl(servletRequest, response);
String redirectUrl = createRedirectUrl(urlEncodedService);
preCommence(servletRequest, response);
new DefaultRedirectStrategy().sendRedirect(servletRequest, response, redirectUrl);
// response.sendRedirect(redirectUrl);
this.redirectStrategy.sendRedirect(servletRequest, response, redirectUrl);
}
/**
@@ -149,4 +151,14 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
return this.encodeServiceUrlWithSessionId;
}
/**
* Sets the {@link RedirectStrategy} to use
* @param redirectStrategy the {@link RedirectStrategy} to use
* @since 6.3
*/
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
this.redirectStrategy = redirectStrategy;
}
}
@@ -22,6 +22,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
import org.apereo.cas.client.util.WebUtils;
import org.apereo.cas.client.validation.TicketValidator;
@@ -40,14 +41,20 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy
@@ -55,14 +62,14 @@ import org.springframework.util.Assert;
* <h2>Service Tickets</h2>
* <p>
* A service ticket consists of an opaque ticket string. It arrives at this filter by the
* user's browser successfully authenticating using CAS, and then receiving a HTTP
* user's browser successfully authenticating using CAS, and then receiving an HTTP
* redirect to a <code>service</code>. The opaque ticket string is presented in the
* <code>ticket</code> request parameter.
* <p>
* This filter monitors the <code>service</code> URL so it can receive the service ticket
* and process it. By default this filter processes the URL <tt>/login/cas</tt>. When
* processing this URL, the value of {@link ServiceProperties#getService()} is used as the
* <tt>service</tt> when validating the <code>ticket</code>. This means that it is
* This filter monitors the <code>service</code> URL so that it can receive the service
* ticket and process it. By default, this filter processes the URL <tt>/login/cas</tt>.
* When processing this URL, the value of {@link ServiceProperties#getService()} is used
* as the <tt>service</tt> when validating the <code>ticket</code>. This means that it is
* important that {@link ServiceProperties#getService()} specifies the same value as the
* <tt>filterProcessesUrl</tt>.
* <p>
@@ -73,22 +80,22 @@ import org.springframework.util.Assert;
* <h2>Obtaining Proxy Granting Tickets</h2>
* <p>
* If specified, the filter can also monitor the <code>proxyReceptorUrl</code>. The filter
* will respond to requests matching this url so that the CAS Server can provide a PGT to
* the filter. Note that in addition to the <code>proxyReceptorUrl</code> a non-null
* will respond to the requests matching this url so that the CAS Server can provide a PGT
* to the filter. Note that in addition to the <code>proxyReceptorUrl</code> a non-null
* <code>proxyGrantingTicketStorage</code> must be provided in order for the filter to
* respond to proxy receptor requests. By configuring a shared
* {@link ProxyGrantingTicketStorage} between the {@link TicketValidator} and the
* CasAuthenticationFilter one can have the CasAuthenticationFilter handle the proxying
* requirements for CAS.
* <code>CasAuthenticationFilter</code>, one can have the
* <code>CasAuthenticationFilter</code> handling the proxying requirements for CAS.
* <h2>Proxy Tickets</h2>
* <p>
* The filter can process tickets present on any url. This is useful when wanting to
* process proxy tickets. In order for proxy tickets to get processed
* The filter can process tickets present on any url. This is useful when one wants to
* process proxy tickets. In order for proxy tickets to get processed,
* {@link ServiceProperties#isAuthenticateAllArtifacts()} must return <code>true</code>.
* Additionally, if the request is already authenticated, authentication will <b>not</b>
* occur. Last, {@link AuthenticationDetailsSource#buildDetails(Object)} must return a
* {@link ServiceAuthenticationDetails}. This can be accomplished using the
* {@link ServiceAuthenticationDetailsSource}. In this case
* {@link ServiceAuthenticationDetailsSource}. In this case,
* {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url.
* <p>
* Processing the proxy ticket involves creating a
@@ -200,6 +207,10 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private RequestCache requestCache = new HttpSessionRequestCache();
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
public CasAuthenticationFilter() {
@@ -240,7 +251,22 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
return null;
}
String serviceTicket = obtainArtifact(request);
if (serviceTicket == null) {
if (!StringUtils.hasText(serviceTicket)) {
HttpSession session = request.getSession(false);
if (session != null && session
.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR) != null) {
this.logger.debug("Failed authentication response from CAS gateway request");
session.removeAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR);
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest != null) {
String redirectUrl = savedRequest.getRedirectUrl();
this.logger.debug(LogMessage.format("Redirecting to: %s", redirectUrl));
this.requestCache.removeRequest(request, response);
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
return null;
}
}
this.logger.debug("Failed to obtain an artifact (cas ticket)");
serviceTicket = "";
}
@@ -318,6 +344,28 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
/**
* Set the {@link RedirectStrategy} used to redirect to the saved request if there is
* one saved. Defaults to {@link DefaultRedirectStrategy}.
* @param redirectStrategy the redirect strategy to use
* @since 6.3
*/
public final void setRedirectStrategy(RedirectStrategy redirectStrategy) {
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
this.redirectStrategy = redirectStrategy;
}
/**
* The {@link RequestCache} used to retrieve the saved request in failed gateway
* authentication scenarios.
* @param requestCache the request cache to use
* @since 6.3
*/
public final void setRequestCache(RequestCache requestCache) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
}
/**
* Indicates if the request is elgible to process a service ticket. This method exists
* for readability.
@@ -0,0 +1,126 @@
/*
* 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.cas.web;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.apereo.cas.client.util.CommonUtils;
import org.apereo.cas.client.util.WebUtils;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
/**
* Redirects the request to the CAS server appending {@code gateway=true} to the URL. Upon
* redirection, the {@link ServiceProperties#isSendRenew()} is ignored and considered as
* {@code false} to align with the specification says that the {@code sendRenew} parameter
* is not compatible with the {@code gateway} parameter. See the <a href=
* "https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html#:~:text=This%20parameter%20is%20not%20compatible%20with%20the%20%E2%80%9Crenew%E2%80%9D%20parameter.%20Behavior%20is%20undefined%20if%20both%20are%20set.">CAS
* Protocol Specification</a> for more details. To allow other filters to know if the
* request is a gateway request, this filter creates a session and add an attribute with
* name {@link #CAS_GATEWAY_AUTHENTICATION_ATTR} which can be checked by other filters if
* needed. It is recommended that this filter is placed after
* {@link CasAuthenticationFilter} if it is defined.
*
* @author Michael Remond
* @author Jerome LELEU
* @author Marcus da Coregio
* @since 6.3
*/
public final class CasGatewayAuthenticationRedirectFilter extends GenericFilterBean {
public static final String CAS_GATEWAY_AUTHENTICATION_ATTR = "CAS_GATEWAY_AUTHENTICATION";
private final String casLoginUrl;
private final ServiceProperties serviceProperties;
private RequestMatcher requestMatcher;
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* Constructs a new instance of this class
* @param serviceProperties the {@link ServiceProperties}
*/
public CasGatewayAuthenticationRedirectFilter(String casLoginUrl, ServiceProperties serviceProperties) {
Assert.hasText(casLoginUrl, "casLoginUrl cannot be null or empty");
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
this.casLoginUrl = casLoginUrl;
this.serviceProperties = serviceProperties;
this.requestMatcher = new CasGatewayResolverRequestMatcher(this.serviceProperties);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!this.requestMatcher.matches(request)) {
chain.doFilter(request, response);
return;
}
this.requestCache.saveRequest(request, response);
HttpSession session = request.getSession(true);
session.setAttribute(CAS_GATEWAY_AUTHENTICATION_ATTR, true);
String urlEncodedService = WebUtils.constructServiceUrl(request, response, this.serviceProperties.getService(),
null, this.serviceProperties.getServiceParameter(), this.serviceProperties.getArtifactParameter(),
true);
String redirectUrl = CommonUtils.constructRedirectUrl(this.casLoginUrl,
this.serviceProperties.getServiceParameter(), urlEncodedService, false, true);
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
}
/**
* Sets the {@link RequestMatcher} used to trigger this filter. Defaults to
* {@link CasGatewayResolverRequestMatcher}.
* @param requestMatcher the {@link RequestMatcher} to use
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
/**
* Sets the {@link RequestCache} used to store the current request to be replayed
* after redirect from the CAS server. Defaults to {@link HttpSessionRequestCache}.
* @param requestCache the {@link RequestCache} to use
*/
public void setRequestCache(RequestCache requestCache) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
}
}
@@ -0,0 +1,67 @@
/*
* 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.cas.web;
import jakarta.servlet.http.HttpServletRequest;
import org.apereo.cas.client.authentication.DefaultGatewayResolverImpl;
import org.apereo.cas.client.authentication.GatewayResolver;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* A {@link RequestMatcher} implementation that delegates the check to an instance of
* {@link GatewayResolver}. The request is marked as "gatewayed" using the configured
* {@link GatewayResolver} to avoid infinite loop.
*
* @author Michael Remond
* @author Marcus da Coregio
* @since 6.3
*/
public final class CasGatewayResolverRequestMatcher implements RequestMatcher {
private final ServiceProperties serviceProperties;
private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
public CasGatewayResolverRequestMatcher(ServiceProperties serviceProperties) {
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
this.serviceProperties = serviceProperties;
}
@Override
public boolean matches(HttpServletRequest request) {
boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, this.serviceProperties.getService());
if (!wasGatewayed) {
this.gatewayStorage.storeGatewayInformation(request, this.serviceProperties.getService());
return true;
}
return false;
}
/**
* Sets the {@link GatewayResolver} to check if the request was already gatewayed.
* Defaults to {@link DefaultGatewayResolverImpl}
* @param gatewayStorage the {@link GatewayResolver} to use. Cannot be null.
*/
public void setGatewayStorage(GatewayResolver gatewayStorage) {
Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
this.gatewayStorage = gatewayStorage;
}
}
@@ -16,16 +16,22 @@
package org.springframework.security.cas.web;
import java.io.IOException;
import java.net.URLEncoder;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.web.RedirectStrategy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests {@link CasAuthenticationEntryPoint}.
@@ -95,4 +101,25 @@ public class CasAuthenticationEntryPointTests {
.isEqualTo(response.getRedirectedUrl());
}
@Test
void setRedirectStrategyThenUses() throws IOException {
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint();
ServiceProperties sp = new ServiceProperties();
sp.setService("https://mycompany.com/login/cas");
ep.setServiceProperties(sp);
ep.setLoginUrl("https://cas/login");
RedirectStrategy redirectStrategy = mock();
ep.setRedirectStrategy(redirectStrategy);
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
ep.commence(req, res, new BadCredentialsException("bad credentials"));
verify(redirectStrategy).sendRedirect(eq(req), eq(res),
eq("https://cas/login?service=https%3A%2F%2Fmycompany.com%2Flogin%2Fcas"));
}
}
@@ -20,6 +20,7 @@ import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpSession;
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -41,6 +42,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -224,6 +226,25 @@ public class CasAuthenticationFilterTests {
verify(securityContextRepository).saveContext(any(SecurityContext.class), eq(request), eq(response));
}
@Test
public void attemptAuthenticationWhenNoServiceTicketAndIsGatewayRequestThenRedirectToSavedRequestAndClearAttribute()
throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HttpSession session = request.getSession(true);
session.setAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR, true);
new HttpSessionRequestCache().saveRequest(request, response);
Authentication authn = filter.attemptAuthentication(request, response);
assertThat(authn).isNull();
assertThat(response.getStatus()).isEqualTo(302);
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue");
assertThat(session.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR))
.isNull();
}
@Test
void successfulAuthenticationWhenSecurityContextRepositorySetThenUses() throws ServletException, IOException {
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
@@ -0,0 +1,95 @@
/*
* 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.cas.web;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.web.savedrequest.RequestCache;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link CasGatewayAuthenticationRedirectFilter}.
*
* @author Jerome LELEU
* @author Marcus da Coregio
*/
public class CasGatewayAuthenticationRedirectFilterTests {
private static final String CAS_LOGIN_URL = "http://mycasserver/login";
CasGatewayAuthenticationRedirectFilter filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL,
serviceProperties());
@Test
void doFilterWhenMatchesThenSavesRequestAndSavesAttributeAndSendRedirect() throws IOException, ServletException {
RequestCache requestCache = mock();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
this.filter.setRequestMatcher((req) -> true);
this.filter.setRequestCache(requestCache);
this.filter.doFilter(request, response, new MockFilterChain());
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getHeader("Location"))
.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
verify(requestCache).saveRequest(request, response);
}
@Test
void doFilterWhenNotMatchThenContinueFilter() throws ServletException, IOException {
this.filter.setRequestMatcher((req) -> false);
FilterChain chain = mock();
MockHttpServletResponse response = mock();
this.filter.doFilter(new MockHttpServletRequest(), response, chain);
verify(chain).doFilter(any(), any());
verifyNoInteractions(response);
}
@Test
void doFilterWhenSendRenewTrueThenIgnores() throws ServletException, IOException {
ServiceProperties serviceProperties = serviceProperties();
serviceProperties.setSendRenew(true);
this.filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL, serviceProperties);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
this.filter.setRequestMatcher((req) -> true);
this.filter.doFilter(request, response, new MockFilterChain());
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getHeader("Location"))
.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
}
private static ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("http://localhost/login/cas");
return serviceProperties;
}
}
@@ -0,0 +1,74 @@
/*
* 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.cas.web;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.cas.ServiceProperties;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests {@link CasGatewayResolverRequestMatcher}.
*
* @author Marcus da Coregio
*/
class CasGatewayResolverRequestMatcherTests {
CasGatewayResolverRequestMatcher matcher = new CasGatewayResolverRequestMatcher(new ServiceProperties());
@Test
void constructorWhenServicePropertiesNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> new CasGatewayResolverRequestMatcher(null))
.withMessage("serviceProperties cannot be null");
}
@Test
void matchesWhenAlreadyGatewayedThenReturnsFalse() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession().setAttribute("_const_cas_gateway_", "yes");
boolean matches = this.matcher.matches(request);
assertThat(matches).isFalse();
}
@Test
void matchesWhenNotGatewayedThenReturnsTrue() {
MockHttpServletRequest request = new MockHttpServletRequest();
boolean matches = this.matcher.matches(request);
assertThat(matches).isTrue();
}
@Test
void matchesWhenNoSessionThenReturnsTrue() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setSession(null);
boolean matches = this.matcher.matches(request);
assertThat(matches).isTrue();
}
@Test
void matchesWhenNotGatewayedAndCheckedAgainThenSavesAsGatewayedAndReturnsFalse() {
MockHttpServletRequest request = new MockHttpServletRequest();
boolean matches = this.matcher.matches(request);
boolean secondMatch = this.matcher.matches(request);
assertThat(matches).isTrue();
assertThat(secondMatch).isFalse();
}
}
@@ -96,7 +96,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
pc.getReaderContext()
.fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or "
+ "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema "
+ "with Spring Security 6.2. Please update your schema declarations to the 6.2 schema.",
+ "with Spring Security 6.3. Please update your schema declarations to the 6.3 schema.",
element);
}
String name = pc.getDelegate().getLocalName(element);
@@ -221,7 +221,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
private boolean matchesVersionInternal(Element element) {
String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation");
return schemaLocation.matches("(?m).*spring-security-6\\.2.*.xsd.*")
return schemaLocation.matches("(?m).*spring-security-6\\.3.*.xsd.*")
|| schemaLocation.matches("(?m).*spring-security.xsd.*")
|| !schemaLocation.matches("(?m).*spring-security.*");
}
@@ -16,8 +16,15 @@
package org.springframework.security.config.annotation.authentication.configuration;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@@ -49,16 +56,33 @@ class InitializeAuthenticationProviderBeanManagerConfigurer extends GlobalAuthen
class InitializeAuthenticationProviderManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {
private final Log logger = LogFactory.getLog(getClass());
@Override
public void configure(AuthenticationManagerBuilder auth) {
if (auth.isConfigured()) {
return;
}
AuthenticationProvider authenticationProvider = getBeanOrNull(AuthenticationProvider.class);
if (authenticationProvider == null) {
List<BeanWithName<AuthenticationProvider>> authenticationProviders = getBeansWithName(
AuthenticationProvider.class);
if (authenticationProviders.isEmpty()) {
return;
}
else if (authenticationProviders.size() > 1) {
List<String> beanNames = authenticationProviders.stream().map(BeanWithName::getName).toList();
this.logger.info(LogMessage.format("Found %s AuthenticationProvider beans, with names %s. "
+ "Global Authentication Manager will not be configured with AuthenticationProviders. "
+ "Consider publishing a single AuthenticationProvider bean, or wiring your Providers directly "
+ "using the DSL.", authenticationProviders.size(), beanNames));
return;
}
var authenticationProvider = authenticationProviders.get(0).getBean();
var authenticationProviderBeanName = authenticationProviders.get(0).getName();
auth.authenticationProvider(authenticationProvider);
this.logger.info(LogMessage.format(
"Global AuthenticationManager configured with AuthenticationProvider bean with name %s",
authenticationProviderBeanName));
}
/**
@@ -74,6 +98,42 @@ class InitializeAuthenticationProviderBeanManagerConfigurer extends GlobalAuthen
return InitializeAuthenticationProviderBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
}
/**
* @return a list of beans of the requested class, along with their names. If
* there are no registered beans of that type, the list is empty.
*/
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
String[] beanNames = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
.getBeanNamesForType(type);
for (String beanName : beanNames) {
T bean = InitializeAuthenticationProviderBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
beanWithNames.add(new BeanWithName<T>(bean, beanName));
}
return beanWithNames;
}
static class BeanWithName<T> {
private final T bean;
private final String name;
BeanWithName(T bean, String name) {
this.bean = bean;
this.name = name;
}
T getBean() {
return this.bean;
}
String getName() {
return this.name;
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -16,10 +16,18 @@
package org.springframework.security.config.annotation.authentication.configuration;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -54,27 +62,57 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {
private final Log logger = LogFactory.getLog(getClass());
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
List<BeanWithName<UserDetailsService>> userDetailsServices = getBeansWithName(UserDetailsService.class);
if (auth.isConfigured()) {
if (!userDetailsServices.isEmpty()) {
this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. "
+ "UserDetailsService beans will not be used for username/password login. "
+ "Consider removing the AuthenticationProvider bean. "
+ "Alternatively, consider using the UserDetailsService in a manually instantiated "
+ "DaoAuthenticationProvider.");
}
return;
}
UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
if (userDetailsService == null) {
if (userDetailsServices.isEmpty()) {
return;
}
else if (userDetailsServices.size() > 1) {
List<String> beanNames = userDetailsServices.stream().map(BeanWithName::getName).toList();
this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. "
+ "Global Authentication Manager will not use a UserDetailsService for username/password login. "
+ "Consider publishing a single UserDetailsService bean.", userDetailsServices.size(),
beanNames));
return;
}
var userDetailsService = userDetailsServices.get(0).getBean();
var userDetailsServiceBeanName = userDetailsServices.get(0).getName();
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class);
DaoAuthenticationProvider provider;
if (passwordEncoder != null) {
provider.setPasswordEncoder(passwordEncoder);
provider = new DaoAuthenticationProvider(passwordEncoder);
}
else {
provider = new DaoAuthenticationProvider();
}
provider.setUserDetailsService(userDetailsService);
if (passwordManager != null) {
provider.setUserDetailsPasswordService(passwordManager);
}
if (passwordChecker != null) {
provider.setCompromisedPasswordChecker(passwordChecker);
}
provider.afterPropertiesSet();
auth.authenticationProvider(provider);
this.logger.info(LogMessage.format(
"Global AuthenticationManager configured with UserDetailsService bean with name %s",
userDetailsServiceBeanName));
}
/**
@@ -89,6 +127,41 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
}
/**
* @return a list of beans of the requested class, along with their names. If
* there are no registered beans of that type, the list is empty.
*/
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type);
for (String beanName : beanNames) {
T bean = InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
beanWithNames.add(new BeanWithName<T>(bean, beanName));
}
return beanWithNames;
}
static class BeanWithName<T> {
private final T bean;
private final String name;
BeanWithName(T bean, String name) {
this.bean = bean;
this.name = name;
}
T getBean() {
return this.bean;
}
String getName() {
return this.name;
}
}
}
}
@@ -0,0 +1,63 @@
/*
* 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.
* 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.annotation.method.configuration;
import java.util.ArrayList;
import java.util.List;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
import org.springframework.security.config.Customizer;
@Configuration(proxyBeanMethods = false)
final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider,
ObjectProvider<Customizer<AuthorizationAdvisorProxyFactory>> customizers) {
List<AuthorizationAdvisor> advisors = new ArrayList<>();
provider.forEach(advisors::add);
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults();
customizers.forEach((c) -> c.customize(factory));
factory.setAdvisors(advisors);
return factory;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
AuthorizationAdvisorProxyFactory authorizationProxyFactory) {
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
authorizationProxyFactory);
List<AuthorizationAdvisor> advisors = new ArrayList<>();
provider.forEach(advisors::add);
advisors.add(interceptor);
authorizationProxyFactory.setAdvisors(advisors);
return interceptor;
}
}
@@ -19,18 +19,26 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationAuthorizationManager;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier;
final class DeferringObservationAuthorizationManager<T> implements AuthorizationManager<T> {
final class DeferringObservationAuthorizationManager<T>
implements AuthorizationManager<T>, MethodAuthorizationDeniedHandler {
private final Supplier<AuthorizationManager<T>> delegate;
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
AuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> {
@@ -40,6 +48,9 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
}
return new ObservationAuthorizationManager<>(registry, delegate);
});
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
}
@Override
@@ -47,4 +58,15 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
return this.delegate.get().check(authentication, object);
}
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handleDeniedInvocation(methodInvocation, authorizationResult);
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.handler.handleDeniedInvocationResult(methodInvocationResult, authorizationResult);
}
}
@@ -19,19 +19,27 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier;
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
final class DeferringObservationReactiveAuthorizationManager<T>
implements ReactiveAuthorizationManager<T>, MethodAuthorizationDeniedHandler {
private final Supplier<ReactiveAuthorizationManager<T>> delegate;
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
ReactiveAuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> {
@@ -41,6 +49,9 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
}
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
});
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
}
@Override
@@ -48,4 +59,15 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
return this.delegate.get().check(authentication, object);
}
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handleDeniedInvocation(methodInvocation, authorizationResult);
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.handler.handleDeniedInvocationResult(methodInvocationResult, authorizationResult);
}
}
@@ -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.
@@ -87,4 +87,14 @@ public @interface EnableMethodSecurity {
*/
AdviceMode mode() default AdviceMode.PROXY;
/**
* Indicate additional offset in the ordering of the execution of the security
* interceptors when multiple advices are applied at a specific joinpoint. I.e.,
* precedence of each security interceptor enabled by this annotation will be
* calculated as sum of its default precedence and offset. The default is 0.
* @return the offset in the order the security advisor should be applied
* @since 6.3
*/
int offset() default 0;
}
@@ -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.
@@ -20,11 +20,18 @@ import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
@@ -42,15 +49,23 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class Jsr250MethodSecurityConfiguration {
final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
private int interceptorOrderOffset;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor jsr250AuthorizationMethodInterceptor(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
Jsr250MethodSecurityConfiguration configuration) {
Jsr250AuthorizationManager jsr250 = new Jsr250AuthorizationManager();
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
authoritiesAuthorizationManager.setRoleHierarchy(roleHierarchy);
jsr250.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager);
defaultsProvider.ifAvailable((d) -> jsr250.setRolePrefix(d.getRolePrefix()));
SecurityContextHolderStrategy strategy = strategyProvider
.getIfAvailable(SecurityContextHolder::getContextHolderStrategy);
@@ -58,8 +73,16 @@ final class Jsr250MethodSecurityConfiguration {
registryProvider, jsr250);
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
.jsr250(manager);
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
interceptor.setSecurityContextHolderStrategy(strategy);
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
return interceptor;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
this.interceptorOrderOffset = annotation.offset();
}
}
@@ -33,6 +33,7 @@ class MethodSecurityAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
registerAsAdvisor("postAuthorizeAuthorization", registry);
registerAsAdvisor("securedAuthorization", registry);
registerAsAdvisor("jsr250Authorization", registry);
registerAsAdvisor("authorizeReturnObject", registry);
}
private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {
@@ -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.
@@ -56,6 +56,7 @@ final class MethodSecuritySelector implements ImportSelector {
if (annotation.jsr250Enabled()) {
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
}
imports.add(AuthorizationProxyConfiguration.class.getName());
return imports.toArray(new String[0]);
}
@@ -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.
@@ -16,33 +16,41 @@
package org.springframework.security.config.annotation.method.configuration;
import java.util.function.Consumer;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.util.function.SingletonSupplier;
@@ -56,73 +64,102 @@ import org.springframework.util.function.SingletonSupplier;
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class PrePostMethodSecurityConfiguration {
final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
private int interceptorOrderOffset;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor preFilterAuthorizationMethodInterceptor(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
ApplicationContext context) {
PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor();
strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy);
preFilter.setExpressionHandler(
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
return preFilter;
preFilter.setOrder(preFilter.getOrder() + configuration.interceptorOrderOffset);
return new DeferringMethodInterceptor<>(preFilter, (f) -> {
methodSecurityDefaultsProvider.ifAvailable(f::setTemplateDefaults);
f.setExpressionHandler(expressionHandlerProvider
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
});
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
manager.setExpressionHandler(
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
manager.setApplicationContext(context);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(manager(manager, registryProvider));
strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher);
return preAuthorize;
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
return new DeferringMethodInterceptor<>(preAuthorize, (f) -> {
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
manager.setExpressionHandler(expressionHandlerProvider
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
eventPublisherProvider.ifAvailable(f::setAuthorizationEventPublisher);
});
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
manager.setExpressionHandler(
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
manager.setApplicationContext(context);
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
.postAuthorize(manager(manager, registryProvider));
strategyProvider.ifAvailable(postAuthorize::setSecurityContextHolderStrategy);
eventPublisherProvider.ifAvailable(postAuthorize::setAuthorizationEventPublisher);
return postAuthorize;
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
return new DeferringMethodInterceptor<>(postAuthorize, (f) -> {
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
manager.setExpressionHandler(expressionHandlerProvider
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
eventPublisherProvider.ifAvailable(f::setAuthorizationEventPublisher);
});
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor postFilterAuthorizationMethodInterceptor(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<PrePostTemplateDefaults> methodSecurityDefaultsProvider,
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider, ApplicationContext context) {
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<RoleHierarchy> roleHierarchyProvider, PrePostMethodSecurityConfiguration configuration,
ApplicationContext context) {
PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor();
strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy);
postFilter.setExpressionHandler(
new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider, defaultsProvider, context));
return postFilter;
postFilter.setOrder(postFilter.getOrder() + configuration.interceptorOrderOffset);
return new DeferringMethodInterceptor<>(postFilter, (f) -> {
methodSecurityDefaultsProvider.ifAvailable(f::setTemplateDefaults);
f.setExpressionHandler(expressionHandlerProvider
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context)));
strategyProvider.ifAvailable(f::setSecurityContextHolderStrategy);
});
}
private static MethodSecurityExpressionHandler defaultExpressionHandler(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext context) {
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<RoleHierarchy> roleHierarchyProvider, ApplicationContext context) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
handler.setRoleHierarchy(roleHierarchy);
defaultsProvider.ifAvailable((d) -> handler.setDefaultRolePrefix(d.getRolePrefix()));
handler.setApplicationContext(context);
return handler;
@@ -133,41 +170,54 @@ final class PrePostMethodSecurityConfiguration {
return new DeferringObservationAuthorizationManager<>(registryProvider, delegate);
}
private static final class DeferringMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
this.interceptorOrderOffset = annotation.offset();
}
private final Supplier<MethodSecurityExpressionHandler> expressionHandler;
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
implements AuthorizationAdvisor {
private DeferringMethodSecurityExpressionHandler(
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext applicationContext) {
this.expressionHandler = SingletonSupplier.of(() -> expressionHandlerProvider
.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, applicationContext)));
private final Pointcut pointcut;
private final int order;
private final Supplier<M> delegate;
DeferringMethodInterceptor(M delegate, Consumer<M> supplier) {
this.pointcut = delegate.getPointcut();
this.order = delegate.getOrder();
this.delegate = SingletonSupplier.of(() -> {
supplier.accept(delegate);
return delegate;
});
}
@Nullable
@Override
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
return this.delegate.get().invoke(invocation);
}
@Override
public ExpressionParser getExpressionParser() {
return this.expressionHandler.get().getExpressionParser();
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public EvaluationContext createEvaluationContext(Authentication authentication, MethodInvocation invocation) {
return this.expressionHandler.get().createEvaluationContext(authentication, invocation);
public Advice getAdvice() {
return this;
}
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication,
MethodInvocation invocation) {
return this.expressionHandler.get().createEvaluationContext(authentication, invocation);
public int getOrder() {
return this.order;
}
@Override
public Object filter(Object filterTarget, Expression filterExpression, EvaluationContext ctx) {
return this.expressionHandler.get().filter(filterTarget, filterExpression, ctx);
}
@Override
public void setReturnObject(Object returnObject, EvaluationContext ctx) {
this.expressionHandler.get().setReturnObject(returnObject, ctx);
public boolean isPerInstance() {
return true;
}
}
@@ -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.
@@ -16,12 +16,22 @@
package org.springframework.security.config.annotation.method.configuration;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import java.util.function.Consumer;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
@@ -29,6 +39,7 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
import org.springframework.security.authorization.method.MethodInvocationResult;
@@ -36,7 +47,9 @@ import org.springframework.security.authorization.method.PostAuthorizeReactiveAu
import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor;
import org.springframework.security.authorization.method.PreAuthorizeReactiveAuthorizationManager;
import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.util.function.SingletonSupplier;
/**
* Configuration for a {@link ReactiveAuthenticationManager} based Method Security.
@@ -45,38 +58,58 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults;
* @since 5.8
*/
@Configuration(proxyBeanMethods = false)
final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements AopInfrastructureBean {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
MethodSecurityExpressionHandler expressionHandler) {
return new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(
expressionHandler);
return new DeferringMethodInterceptor<>(interceptor,
(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(
new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
.preAuthorize(authorizationManager);
return new DeferringMethodInterceptor<>(interceptor,
(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
MethodSecurityExpressionHandler expressionHandler) {
return new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(
expressionHandler);
return new DeferringMethodInterceptor<>(interceptor,
(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(
new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
.postAuthorize(authorizationManager);
return new DeferringMethodInterceptor<>(interceptor,
(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
}
@Bean
@@ -95,4 +128,50 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
}
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
implements AuthorizationAdvisor {
private final Pointcut pointcut;
private final int order;
private final Supplier<M> delegate;
DeferringMethodInterceptor(M delegate, Consumer<M> supplier) {
this.pointcut = delegate.getPointcut();
this.order = delegate.getOrder();
this.delegate = SingletonSupplier.of(() -> {
supplier.accept(delegate);
return delegate;
});
}
@Nullable
@Override
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
return this.delegate.get().invoke(invocation);
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public boolean isPerInstance() {
return true;
}
}
}
@@ -0,0 +1,63 @@
/*
* 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.
* 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.annotation.method.configuration;
import java.util.ArrayList;
import java.util.List;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
import org.springframework.security.config.Customizer;
@Configuration(proxyBeanMethods = false)
final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider,
ObjectProvider<Customizer<AuthorizationAdvisorProxyFactory>> customizers) {
List<AuthorizationAdvisor> advisors = new ArrayList<>();
provider.forEach(advisors::add);
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withReactiveDefaults();
customizers.forEach((c) -> c.customize(factory));
factory.setAdvisors(advisors);
return factory;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
AuthorizationAdvisorProxyFactory authorizationProxyFactory) {
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
authorizationProxyFactory);
List<AuthorizationAdvisor> advisors = new ArrayList<>();
provider.forEach(advisors::add);
advisors.add(interceptor);
authorizationProxyFactory.setAdvisors(advisors);
return interceptor;
}
}
@@ -51,13 +51,15 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
else {
imports.add(ReactiveMethodSecurityConfiguration.class.getName());
}
imports.add(ReactiveAuthorizationProxyConfiguration.class.getName());
return imports.toArray(new String[0]);
}
private static final class AutoProxyRegistrarSelector
extends AdviceModeImportSelector<EnableReactiveMethodSecurity> {
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() };
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName(),
MethodSecurityAdvisorRegistrar.class.getName() };
@Override
protected String[] selectImports(@NonNull AdviceMode adviceMode) {
@@ -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.
@@ -20,12 +20,19 @@ import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.SecuredAuthorizationManager;
@@ -42,22 +49,38 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class SecuredMethodSecurityConfiguration {
final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
private int interceptorOrderOffset;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor securedAuthorizationMethodInterceptor(
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
SecuredMethodSecurityConfiguration configuration) {
SecuredAuthorizationManager secured = new SecuredAuthorizationManager();
AuthoritiesAuthorizationManager authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
RoleHierarchy roleHierarchy = roleHierarchyProvider.getIfAvailable(NullRoleHierarchy::new);
authoritiesAuthorizationManager.setRoleHierarchy(roleHierarchy);
secured.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager);
SecurityContextHolderStrategy strategy = strategyProvider
.getIfAvailable(SecurityContextHolder::getContextHolderStrategy);
AuthorizationManager<MethodInvocation> manager = new DeferringObservationAuthorizationManager<>(
registryProvider, secured);
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
.secured(manager);
interceptor.setOrder(interceptor.getOrder() + configuration.interceptorOrderOffset);
interceptor.setSecurityContextHolderStrategy(strategy);
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
return interceptor;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
this.interceptorOrderOffset = annotation.offset();
}
}
@@ -51,11 +51,13 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
@@ -183,7 +185,8 @@ final class OAuth2ClientConfiguration {
RefreshTokenOAuth2AuthorizedClientProvider.class,
ClientCredentialsOAuth2AuthorizedClientProvider.class,
PasswordOAuth2AuthorizedClientProvider.class,
JwtBearerOAuth2AuthorizedClientProvider.class
JwtBearerOAuth2AuthorizedClientProvider.class,
TokenExchangeOAuth2AuthorizedClientProvider.class
);
// @formatter:on
@@ -255,6 +258,12 @@ final class OAuth2ClientConfiguration {
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
}
OAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = getTokenExchangeAuthorizedClientProvider(
authorizedClientProviderBeans);
if (tokenExchangeAuthorizedClientProvider != null) {
authorizedClientProviders.add(tokenExchangeAuthorizedClientProvider);
}
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
}
@@ -364,6 +373,25 @@ final class OAuth2ClientConfiguration {
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getTokenExchangeAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, TokenExchangeOAuth2AuthorizedClientProvider.class);
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
TokenExchangeGrantRequest.class));
if (accessTokenResponseClient != null) {
if (authorizedClientProvider == null) {
authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
}
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
@@ -17,6 +17,7 @@
package org.springframework.security.config.annotation.web.configurers;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
@@ -30,12 +31,14 @@ import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationManagers;
import org.springframework.security.authorization.ObservationAuthorizationManager;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
@@ -244,11 +247,14 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* {@link RequestMatcher}s.
*
* @author Evgeniy Cheban
* @author Josh Cummings
*/
public class AuthorizedUrl {
private final List<? extends RequestMatcher> matchers;
private boolean not;
/**
* Creates an instance.
* @param matchers the {@link RequestMatcher} instances to map
@@ -261,6 +267,16 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
return this.matchers;
}
/**
* Negates the following authorization rule.
* @return the {@link AuthorizedUrl} for further customization
* @since 6.3
*/
public AuthorizedUrl not() {
this.not = true;
return this;
}
/**
* Specify that URLs are allowed by anyone.
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
@@ -373,6 +389,21 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
return access(AuthenticatedAuthorizationManager.anonymous());
}
/**
* Specify that a path variable in URL to be compared.
*
* <p>
* For example, <pre>
* requestMatchers("/user/{username}").hasVariable("username").equalTo(Authentication::getName)
* </pre>
* @param variable the variable in URL template to compare.
* @return {@link AuthorizedUrlVariable} for further customization.
* @since 6.3
*/
public AuthorizedUrlVariable hasVariable(String variable) {
return new AuthorizedUrlVariable(variable);
}
/**
* Allows specifying a custom {@link AuthorizationManager}.
* @param manager the {@link AuthorizationManager} to use
@@ -382,7 +413,44 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
public AuthorizationManagerRequestMatcherRegistry access(
AuthorizationManager<RequestAuthorizationContext> manager) {
Assert.notNull(manager, "manager cannot be null");
return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
return (this.not)
? AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, AuthorizationManagers.not(manager))
: AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
}
/**
* An object that allows configuring {@link RequestMatcher}s with URI path
* variables
*
* @author Taehong Kim
* @since 6.3
*/
public final class AuthorizedUrlVariable {
private final String variable;
private AuthorizedUrlVariable(String variable) {
this.variable = variable;
}
/**
* Compares the value of a path variable in the URI with an `Authentication`
* attribute
* <p>
* For example, <pre>
* requestMatchers("/user/{username}").hasVariable("username").equalTo(Authentication::getName));
* </pre>
* @param function a function to get value from {@link Authentication}.
* @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
* customization.
*/
public AuthorizationManagerRequestMatcherRegistry equalTo(Function<Authentication, String> function) {
return access((auth, requestContext) -> {
String value = requestContext.getVariables().get(this.variable);
return new AuthorizationDecision(function.apply(auth.get()).equals(value));
});
}
}
}
@@ -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.
@@ -19,17 +19,15 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
import java.util.function.Function;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.JwtValidators;
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new OidcBackChannelLogoutTokenValidator(clientRegistration));
return JwtValidators.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration));
}
}
@@ -0,0 +1,413 @@
/*
* 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.
* 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.annotation.web.reactive;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.ResolvableType;
import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.DelegatingReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.JwtBearerReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.TokenExchangeReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
/**
* {@link Configuration} for OAuth 2.0 Client support.
*
* <p>
* This {@code Configuration} is conditionally imported by
* {@link ReactiveOAuth2ClientImportSelector} when the
* {@code spring-security-oauth2-client} module is present on the classpath.
*
* @author Steve Riesenberg
* @since 6.3
* @see ReactiveOAuth2ClientImportSelector
*/
@Import({ ReactiveOAuth2ClientConfiguration.ReactiveOAuth2AuthorizedClientManagerConfiguration.class,
ReactiveOAuth2ClientConfiguration.OAuth2ClientWebFluxSecurityConfiguration.class })
final class ReactiveOAuth2ClientConfiguration {
@Configuration(proxyBeanMethods = false)
static class ReactiveOAuth2AuthorizedClientManagerConfiguration {
@Bean(name = ReactiveOAuth2AuthorizedClientManagerRegistrar.BEAN_NAME)
ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() {
return new ReactiveOAuth2AuthorizedClientManagerRegistrar();
}
}
@Configuration(proxyBeanMethods = false)
static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer {
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
private ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar;
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
if (authorizedClientManager != null) {
configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager));
}
}
@Autowired(required = false)
void setAuthorizedClientManager(List<ReactiveOAuth2AuthorizedClientManager> authorizedClientManager) {
if (authorizedClientManager.size() == 1) {
this.authorizedClientManager = authorizedClientManager.get(0);
}
}
@Autowired
void setAuthorizedClientManagerRegistrar(
ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) {
this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar;
}
private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() {
if (this.authorizedClientManager != null) {
return this.authorizedClientManager;
}
return this.authorizedClientManagerRegistrar.getAuthorizedClientManagerIfAvailable();
}
}
/**
* A registrar for registering the default
* {@link ReactiveOAuth2AuthorizedClientManager} bean definition, if not already
* present.
*/
static final class ReactiveOAuth2AuthorizedClientManagerRegistrar
implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
static final String BEAN_NAME = "authorizedClientManagerRegistrar";
static final String FACTORY_METHOD_NAME = "getAuthorizedClientManager";
// @formatter:off
private static final Set<Class<?>> KNOWN_AUTHORIZED_CLIENT_PROVIDERS = Set.of(
AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class,
RefreshTokenReactiveOAuth2AuthorizedClientProvider.class,
ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class,
PasswordReactiveOAuth2AuthorizedClientProvider.class,
JwtBearerReactiveOAuth2AuthorizedClientProvider.class,
TokenExchangeReactiveOAuth2AuthorizedClientProvider.class
);
// @formatter:on
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
private ListableBeanFactory beanFactory;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (getBeanNamesForType(ReactiveOAuth2AuthorizedClientManager.class).length != 0
|| getBeanNamesForType(ReactiveClientRegistrationRepository.class).length != 1
|| getBeanNamesForType(ServerOAuth2AuthorizedClientRepository.class).length != 1
&& getBeanNamesForType(ReactiveOAuth2AuthorizedClientService.class).length != 1) {
return;
}
BeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(ReactiveOAuth2AuthorizedClientManager.class)
.setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME)
.getBeanDefinition();
registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry),
beanDefinition);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ListableBeanFactory) beanFactory;
}
ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManagerIfAvailable() {
if (getBeanNamesForType(ReactiveClientRegistrationRepository.class).length != 1
|| getBeanNamesForType(ServerOAuth2AuthorizedClientRepository.class).length != 1
&& getBeanNamesForType(ReactiveOAuth2AuthorizedClientService.class).length != 1) {
return null;
}
return getAuthorizedClientManager();
}
ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() {
ReactiveClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils
.beanOfTypeIncludingAncestors(this.beanFactory, ReactiveClientRegistrationRepository.class, true, true);
ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
try {
authorizedClientRepository = BeanFactoryUtils.beanOfTypeIncludingAncestors(this.beanFactory,
ServerOAuth2AuthorizedClientRepository.class, true, true);
}
catch (NoSuchBeanDefinitionException ex) {
ReactiveOAuth2AuthorizedClientService authorizedClientService = BeanFactoryUtils
.beanOfTypeIncludingAncestors(this.beanFactory, ReactiveOAuth2AuthorizedClientService.class, true,
true);
authorizedClientRepository = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(
authorizedClientService);
}
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviderBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(this.beanFactory, ReactiveOAuth2AuthorizedClientProvider.class, true,
true)
.values();
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) {
authorizedClientProvider = authorizedClientProviderBeans.iterator().next();
}
else {
List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
authorizedClientProviders
.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders
.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans));
ReactiveOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider(
authorizedClientProviderBeans);
if (jwtBearerAuthorizedClientProvider != null) {
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
}
ReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = getTokenExchangeAuthorizedClientProvider(
authorizedClientProviderBeans);
if (tokenExchangeAuthorizedClientProvider != null) {
authorizedClientProviders.add(tokenExchangeAuthorizedClientProvider);
}
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
authorizedClientProvider = new DelegatingReactiveOAuth2AuthorizedClientProvider(
authorizedClientProviders);
}
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
Consumer<DefaultReactiveOAuth2AuthorizedClientManager> authorizedClientManagerConsumer = getBeanOfType(
ResolvableType.forClassWithGenerics(Consumer.class,
DefaultReactiveOAuth2AuthorizedClientManager.class));
if (authorizedClientManagerConsumer != null) {
authorizedClientManagerConsumer.accept(authorizedClientManager);
}
return authorizedClientManager;
}
private boolean hasDelegatingAuthorizedClientProvider(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
if (authorizedClientProviders.size() != 1) {
return false;
}
return authorizedClientProviders.iterator()
.next() instanceof DelegatingReactiveOAuth2AuthorizedClientProvider;
}
private ReactiveOAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider();
}
return authorizedClientProvider;
}
private ReactiveOAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, RefreshTokenReactiveOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider();
}
ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
OAuth2RefreshTokenGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private ReactiveOAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
}
ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
OAuth2ClientCredentialsGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private ReactiveOAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, PasswordReactiveOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
}
ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
OAuth2PasswordGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private ReactiveOAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
JwtBearerReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, JwtBearerReactiveOAuth2AuthorizedClientProvider.class);
ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
JwtBearerGrantRequest.class));
if (accessTokenResponseClient != null) {
if (authorizedClientProvider == null) {
authorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider();
}
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private ReactiveOAuth2AuthorizedClientProvider getTokenExchangeAuthorizedClientProvider(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
TokenExchangeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, TokenExchangeReactiveOAuth2AuthorizedClientProvider.class);
ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
TokenExchangeGrantRequest.class));
if (accessTokenResponseClient != null) {
if (authorizedClientProvider == null) {
authorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
}
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private List<ReactiveOAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
List<ReactiveOAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
authorizedClientProviders);
additionalAuthorizedClientProviders
.removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass()));
return additionalAuthorizedClientProviders;
}
private <T extends ReactiveOAuth2AuthorizedClientProvider> T getAuthorizedClientProviderByType(
Collection<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders, Class<T> providerClass) {
T authorizedClientProvider = null;
for (ReactiveOAuth2AuthorizedClientProvider current : authorizedClientProviders) {
if (providerClass.isInstance(current)) {
assertAuthorizedClientProviderIsNull(authorizedClientProvider);
authorizedClientProvider = providerClass.cast(current);
}
}
return authorizedClientProvider;
}
private static void assertAuthorizedClientProviderIsNull(
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
if (authorizedClientProvider != null) {
// @formatter:off
throw new BeanInitializationException(String.format(
"Unable to create a %s bean. Expected one bean of type %s, but found multiple. " +
"Please consider defining only a single bean of this type, or define a %s bean yourself.",
ReactiveOAuth2AuthorizedClientManager.class.getName(),
authorizedClientProvider.getClass().getName(),
ReactiveOAuth2AuthorizedClientManager.class.getName()));
// @formatter:on
}
}
private <T> String[] getBeanNamesForType(Class<T> beanClass) {
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, true, true);
}
private <T> T getBeanOfType(ResolvableType resolvableType) {
ObjectProvider<T> objectProvider = this.beanFactory.getBeanProvider(resolvableType, true);
return objectProvider.getIfAvailable();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -16,27 +16,13 @@
package org.springframework.security.config.annotation.web.reactive;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.util.ClassUtils;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
/**
* {@link Configuration} for OAuth 2.0 Client support.
* Used by {@link EnableWebFluxSecurity} to conditionally import
* {@link ReactiveOAuth2ClientConfiguration}.
*
* <p>
* This {@code Configuration} is imported by {@link EnableWebFluxSecurity}
@@ -60,85 +46,8 @@ final class ReactiveOAuth2ClientImportSelector implements ImportSelector {
if (!oauth2ClientPresent) {
return new String[0];
}
return new String[] { "org.springframework.security.config.annotation.web.reactive."
+ "ReactiveOAuth2ClientImportSelector$OAuth2ClientWebFluxSecurityConfiguration" };
}
@Configuration(proxyBeanMethods = false)
static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
if (authorizedClientManager != null) {
configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager));
}
}
@Autowired(required = false)
void setClientRegistrationRepository(ReactiveClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Autowired(required = false)
void setAuthorizedClientRepository(ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
this.authorizedClientRepository = authorizedClientRepository;
}
@Autowired(required = false)
void setAuthorizedClientService(List<ReactiveOAuth2AuthorizedClientService> authorizedClientService) {
if (authorizedClientService.size() == 1) {
this.authorizedClientService = authorizedClientService.get(0);
}
}
@Autowired(required = false)
void setAuthorizedClientManager(List<ReactiveOAuth2AuthorizedClientManager> authorizedClientManager) {
if (authorizedClientManager.size() == 1) {
this.authorizedClientManager = authorizedClientManager.get(0);
}
}
private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
if (this.authorizedClientRepository != null) {
return this.authorizedClientRepository;
}
if (this.authorizedClientService != null) {
return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(this.authorizedClientService);
}
return null;
}
private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() {
if (this.authorizedClientManager != null) {
return this.authorizedClientManager;
}
ReactiveOAuth2AuthorizedClientManager authorizedClientManager = null;
if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, getAuthorizedClientRepository());
defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager = defaultReactiveOAuth2AuthorizedClientManager;
}
return authorizedClientManager;
}
return new String[] {
"org.springframework.security.config.annotation.web.reactive.ReactiveOAuth2ClientConfiguration" };
}
}
@@ -32,6 +32,7 @@ import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.authentication.ObservationReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
@@ -63,6 +64,8 @@ class ServerHttpSecurityConfiguration {
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
private ReactiveCompromisedPasswordChecker compromisedPasswordChecker;
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
@Autowired(required = false)
@@ -98,6 +101,11 @@ class ServerHttpSecurityConfiguration {
this.observationRegistry = observationRegistry;
}
@Autowired(required = false)
void setCompromisedPasswordChecker(ReactiveCompromisedPasswordChecker compromisedPasswordChecker) {
this.compromisedPasswordChecker = compromisedPasswordChecker;
}
@Bean
static WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer(
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver) {
@@ -153,6 +161,7 @@ class ServerHttpSecurityConfiguration {
manager.setPasswordEncoder(this.passwordEncoder);
}
manager.setUserDetailsPasswordService(this.userDetailsPasswordService);
manager.setCompromisedPasswordChecker(this.compromisedPasswordChecker);
if (!this.observationRegistry.isNoop()) {
return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager);
}
@@ -44,11 +44,13 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
@@ -76,7 +78,8 @@ final class OAuth2AuthorizedClientManagerRegistrar implements BeanDefinitionRegi
RefreshTokenOAuth2AuthorizedClientProvider.class,
ClientCredentialsOAuth2AuthorizedClientProvider.class,
PasswordOAuth2AuthorizedClientProvider.class,
JwtBearerOAuth2AuthorizedClientProvider.class
JwtBearerOAuth2AuthorizedClientProvider.class,
TokenExchangeOAuth2AuthorizedClientProvider.class
);
// @formatter:on
@@ -137,6 +140,12 @@ final class OAuth2AuthorizedClientManagerRegistrar implements BeanDefinitionRegi
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
}
OAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = getTokenExchangeAuthorizedClientProvider(
authorizedClientProviderBeans);
if (tokenExchangeAuthorizedClientProvider != null) {
authorizedClientProviders.add(tokenExchangeAuthorizedClientProvider);
}
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
}
@@ -245,6 +254,25 @@ final class OAuth2AuthorizedClientManagerRegistrar implements BeanDefinitionRegi
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getTokenExchangeAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, TokenExchangeOAuth2AuthorizedClientProvider.class);
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
TokenExchangeGrantRequest.class));
if (accessTokenResponseClient != null) {
if (authorizedClientProvider == null) {
authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
}
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
@@ -64,6 +64,8 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
private static final String ELT_ENCRYPTION_CREDENTIAL = "encryption-credential";
private static final String ATT_ID = "id";
private static final String ATT_REGISTRATION_ID = "registration-id";
private static final String ATT_ASSERTING_PARTY_ID = "asserting-party-id";
@@ -108,8 +110,11 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
.rootBeanDefinition(InMemoryRelyingPartyRegistrationRepository.class)
.addConstructorArgValue(relyingPartyRegistrations)
.getBeanDefinition();
String relyingPartyRegistrationRepositoryId = parserContext.getReaderContext()
.generateBeanName(relyingPartyRegistrationRepositoryBean);
String relyingPartyRegistrationRepositoryId = element.getAttribute(ATT_ID);
if (!StringUtils.hasText(relyingPartyRegistrationRepositoryId)) {
relyingPartyRegistrationRepositoryId = parserContext.getReaderContext()
.generateBeanName(relyingPartyRegistrationRepositoryBean);
}
parserContext.registerBeanComponent(new BeanComponentDefinition(relyingPartyRegistrationRepositoryBean,
relyingPartyRegistrationRepositoryId));
parserContext.popAndRegisterContainingComponent();
@@ -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.
@@ -19,17 +19,15 @@ package org.springframework.security.config.web.server;
import java.util.function.Function;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.JwtValidators;
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new OidcBackChannelLogoutTokenValidator(clientRegistration));
return JwtValidators.createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration));
}
}
@@ -63,6 +63,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
@@ -126,19 +127,26 @@ import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.ConcurrentSessionControlServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.RegisterSessionServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerHttpBasicAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
import org.springframework.security.web.server.authentication.SessionLimit;
import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
@@ -208,6 +216,8 @@ import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.util.pattern.PathPatternParser;
/**
@@ -314,6 +324,8 @@ public class ServerHttpSecurity {
private LoginPageSpec loginPage = new LoginPageSpec();
private SessionManagementSpec sessionManagement;
private ReactiveAuthenticationManager authenticationManager;
private ServerSecurityContextRepository securityContextRepository;
@@ -362,6 +374,7 @@ public class ServerHttpSecurity {
}
/**
*
* Adds a {@link WebFilter} before specific position.
* @param webFilter the {@link WebFilter} to add
* @param order the place before which to insert the {@link WebFilter}
@@ -745,6 +758,36 @@ public class ServerHttpSecurity {
return this;
}
/**
* Configures Session Management. An example configuration is provided below:
* <pre class="code">
* &#064;Bean
* SecurityWebFilterChain filterChain(ServerHttpSecurity http, ReactiveSessionRegistry sessionRegistry) {
* http
* // ...
* .sessionManagement((sessionManagement) -> sessionManagement
* .concurrentSessions((concurrentSessions) -> concurrentSessions
* .maxSessions(1)
* .maxSessionsPreventsLogin(true)
* .sessionRegistry(sessionRegistry)
* )
* );
* return http.build();
* }
* </pre>
* @param customizer the {@link Customizer} to provide more options for the
* {@link SessionManagementSpec}
* @return the {@link ServerHttpSecurity} to continue configuring
* @since 6.3
*/
public ServerHttpSecurity sessionManagement(Customizer<SessionManagementSpec> customizer) {
if (this.sessionManagement == null) {
this.sessionManagement = new SessionManagementSpec();
}
customizer.customize(this.sessionManagement);
return this;
}
/**
* Configures password management. An example configuration is provided below:
*
@@ -1519,6 +1562,9 @@ public class ServerHttpSecurity {
}
WebFilter securityContextRepositoryWebFilter = securityContextRepositoryWebFilter();
this.webFilters.add(securityContextRepositoryWebFilter);
if (this.sessionManagement != null) {
this.sessionManagement.configure(this);
}
if (this.httpsRedirectSpec != null) {
this.httpsRedirectSpec.configure(this);
}
@@ -1909,6 +1955,270 @@ public class ServerHttpSecurity {
}
/**
* Configures how sessions are managed.
*/
public class SessionManagementSpec {
private ConcurrentSessionsSpec concurrentSessions;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private ReactiveSessionRegistry sessionRegistry;
private SessionLimit sessionLimit = SessionLimit.UNLIMITED;
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler;
/**
* Configures how many sessions are allowed for a given user.
* @param customizer the customizer to provide more options
* @return the {@link SessionManagementSpec} to customize
*/
public SessionManagementSpec concurrentSessions(Customizer<ConcurrentSessionsSpec> customizer) {
if (this.concurrentSessions == null) {
this.concurrentSessions = new ConcurrentSessionsSpec();
}
customizer.customize(this.concurrentSessions);
return this;
}
void configure(ServerHttpSecurity http) {
if (this.concurrentSessions != null) {
ReactiveSessionRegistry reactiveSessionRegistry = getSessionRegistry();
ConcurrentSessionControlServerAuthenticationSuccessHandler concurrentSessionControlStrategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(
reactiveSessionRegistry, getMaximumSessionsExceededHandler());
concurrentSessionControlStrategy.setSessionLimit(this.sessionLimit);
RegisterSessionServerAuthenticationSuccessHandler registerSessionAuthenticationStrategy = new RegisterSessionServerAuthenticationSuccessHandler(
reactiveSessionRegistry);
this.authenticationSuccessHandler = new DelegatingServerAuthenticationSuccessHandler(
concurrentSessionControlStrategy, registerSessionAuthenticationStrategy);
SessionRegistryWebFilter sessionRegistryWebFilter = new SessionRegistryWebFilter(
reactiveSessionRegistry);
configureSuccessHandlerOnAuthenticationFilters();
http.addFilterAfter(sessionRegistryWebFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
}
}
private ServerMaximumSessionsExceededHandler getMaximumSessionsExceededHandler() {
if (this.maximumSessionsExceededHandler != null) {
return this.maximumSessionsExceededHandler;
}
DefaultWebSessionManager webSessionManager = getBeanOrNull(
WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, DefaultWebSessionManager.class);
if (webSessionManager != null) {
this.maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler(
webSessionManager.getSessionStore());
}
if (this.maximumSessionsExceededHandler == null) {
throw new IllegalStateException(
"Could not create a default ServerMaximumSessionsExceededHandler. Please provide "
+ "a ServerMaximumSessionsExceededHandler via DSL");
}
return this.maximumSessionsExceededHandler;
}
private void configureSuccessHandlerOnAuthenticationFilters() {
if (ServerHttpSecurity.this.formLogin != null) {
ServerHttpSecurity.this.formLogin.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
}
if (ServerHttpSecurity.this.oauth2Login != null) {
ServerHttpSecurity.this.oauth2Login.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
}
if (ServerHttpSecurity.this.httpBasic != null) {
ServerHttpSecurity.this.httpBasic.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
}
}
private ReactiveSessionRegistry getSessionRegistry() {
if (this.sessionRegistry == null) {
this.sessionRegistry = getBeanOrNull(ReactiveSessionRegistry.class);
}
if (this.sessionRegistry == null) {
throw new IllegalStateException(
"A ReactiveSessionRegistry is needed for concurrent session management");
}
return this.sessionRegistry;
}
/**
* Configures how many sessions are allowed for a given user.
*/
public class ConcurrentSessionsSpec {
/**
* Sets the {@link ReactiveSessionRegistry} to use.
* @param reactiveSessionRegistry the {@link ReactiveSessionRegistry} to use
* @return the {@link ConcurrentSessionsSpec} to continue customizing
*/
public ConcurrentSessionsSpec sessionRegistry(ReactiveSessionRegistry reactiveSessionRegistry) {
SessionManagementSpec.this.sessionRegistry = reactiveSessionRegistry;
return this;
}
/**
* Sets the maximum number of sessions allowed for any user. You can use
* {@link SessionLimit#of(int)} to specify a positive integer or
* {@link SessionLimit#UNLIMITED} to allow unlimited sessions. To customize
* the maximum number of sessions on a per-user basis, you can provide a
* custom {@link SessionLimit} implementation, like so: <pre>
* http
* .sessionManagement((sessions) -> sessions
* .concurrentSessions((concurrency) -> concurrency
* .maximumSessions((authentication) -> {
* if (authentication.getName().equals("admin")) {
* return Mono.empty() // unlimited sessions for admin
* }
* return Mono.just(1); // one session for every other user
* })
* )
* )
* </pre>
* @param sessionLimit the maximum number of sessions allowed for any user
* @return the {@link ConcurrentSessionsSpec} to continue customizing
*/
public ConcurrentSessionsSpec maximumSessions(SessionLimit sessionLimit) {
Assert.notNull(sessionLimit, "sessionLimit cannot be null");
SessionManagementSpec.this.sessionLimit = sessionLimit;
return this;
}
/**
* Sets the {@link ServerMaximumSessionsExceededHandler} to use when the
* maximum number of sessions is exceeded.
* @param maximumSessionsExceededHandler the
* {@link ServerMaximumSessionsExceededHandler} to use
* @return the {@link ConcurrentSessionsSpec} to continue customizing
*/
public ConcurrentSessionsSpec maximumSessionsExceededHandler(
ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
SessionManagementSpec.this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
return this;
}
}
private static final class SessionRegistryWebFilter implements WebFilter {
private final ReactiveSessionRegistry sessionRegistry;
private SessionRegistryWebFilter(ReactiveSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(new SessionRegistryWebExchange(exchange));
}
private final class SessionRegistryWebExchange extends ServerWebExchangeDecorator {
private final Mono<WebSession> sessionMono;
private SessionRegistryWebExchange(ServerWebExchange delegate) {
super(delegate);
this.sessionMono = delegate.getSession()
.flatMap((session) -> SessionRegistryWebFilter.this.sessionRegistry
.updateLastAccessTime(session.getId())
.thenReturn(session))
.map(SessionRegistryWebSession::new);
}
@Override
public Mono<WebSession> getSession() {
return this.sessionMono;
}
}
private final class SessionRegistryWebSession implements WebSession {
private final WebSession session;
private SessionRegistryWebSession(WebSession session) {
this.session = session;
}
@Override
public String getId() {
return this.session.getId();
}
@Override
public Map<String, Object> getAttributes() {
return this.session.getAttributes();
}
@Override
public void start() {
this.session.start();
}
@Override
public boolean isStarted() {
return this.session.isStarted();
}
@Override
public Mono<Void> changeSessionId() {
String currentId = this.session.getId();
return this.session.changeSessionId()
.then(Mono.defer(
() -> SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
.flatMap((information) -> {
information = information.withSessionId(this.session.getId());
return SessionRegistryWebFilter.this.sessionRegistry
.saveSessionInformation(information);
})));
}
@Override
public Mono<Void> invalidate() {
String currentId = this.session.getId();
return this.session.invalidate()
.then(Mono.defer(() -> SessionRegistryWebFilter.this.sessionRegistry
.removeSessionInformation(currentId)))
.then();
}
@Override
public Mono<Void> save() {
return this.session.save();
}
@Override
public boolean isExpired() {
return this.session.isExpired();
}
@Override
public Instant getCreationTime() {
return this.session.getCreationTime();
}
@Override
public Instant getLastAccessTime() {
return this.session.getLastAccessTime();
}
@Override
public void setMaxIdleTime(Duration maxIdleTime) {
this.session.setMaxIdleTime(maxIdleTime);
}
@Override
public Duration getMaxIdleTime() {
return this.session.getMaxIdleTime();
}
}
}
}
/**
* Configures HTTPS redirection rules
*
@@ -2213,6 +2523,11 @@ public class ServerHttpSecurity {
private ServerAuthenticationFailureHandler authenticationFailureHandler;
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
List.of(new WebFilterChainServerAuthenticationSuccessHandler()));
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
private HttpBasicSpec() {
List<DelegateEntry> entryPoints = new ArrayList<>();
entryPoints
@@ -2223,6 +2538,40 @@ public class ServerHttpSecurity {
this.entryPoint = defaultEntryPoint;
}
/**
* The {@link ServerAuthenticationSuccessHandler} used after authentication
* success. Defaults to {@link WebFilterChainServerAuthenticationSuccessHandler}.
* Note that this method clears previously added success handlers via
* {@link #authenticationSuccessHandler(Consumer)}
* @param authenticationSuccessHandler the success handler to use
* @return the {@link HttpBasicSpec} to continue configuring
* @since 6.3
*/
public HttpBasicSpec authenticationSuccessHandler(
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
authenticationSuccessHandler((handlers) -> {
handlers.clear();
handlers.add(authenticationSuccessHandler);
});
return this;
}
/**
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
* default list contains a
* {@link WebFilterChainServerAuthenticationSuccessHandler}.
* @param handlersConsumer the handlers consumer
* @return the {@link HttpBasicSpec} to continue configuring
* @since 6.3
*/
public HttpBasicSpec authenticationSuccessHandler(
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
handlersConsumer.accept(this.authenticationSuccessHandlers);
return this;
}
/**
* The {@link ReactiveAuthenticationManager} used to authenticate. Defaults to
* {@link ServerHttpSecurity#authenticationManager(ReactiveAuthenticationManager)}.
@@ -2308,9 +2657,17 @@ public class ServerHttpSecurity {
authenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
authenticationFilter.setAuthenticationConverter(new ServerHttpBasicAuthenticationConverter());
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC);
}
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
if (this.authenticationSuccessHandlers.isEmpty()) {
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
}
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
}
private ServerAuthenticationFailureHandler authenticationFailureHandler() {
if (this.authenticationFailureHandler != null) {
return this.authenticationFailureHandler;
@@ -2382,6 +2739,9 @@ public class ServerHttpSecurity {
private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler(
"/");
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
List.of(this.defaultSuccessHandler));
private RedirectServerAuthenticationEntryPoint defaultEntryPoint;
private ReactiveAuthenticationManager authenticationManager;
@@ -2396,7 +2756,7 @@ public class ServerHttpSecurity {
private ServerAuthenticationFailureHandler authenticationFailureHandler;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = this.defaultSuccessHandler;
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
private FormLoginSpec() {
}
@@ -2414,14 +2774,34 @@ public class ServerHttpSecurity {
/**
* The {@link ServerAuthenticationSuccessHandler} used after authentication
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}.
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}. Note
* that this method clears previously added success handlers via
* {@link #authenticationSuccessHandler(Consumer)}
* @param authenticationSuccessHandler the success handler to use
* @return the {@link FormLoginSpec} to continue configuring
*/
public FormLoginSpec authenticationSuccessHandler(
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
authenticationSuccessHandler((handlers) -> {
handlers.clear();
handlers.add(authenticationSuccessHandler);
});
return this;
}
/**
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
* default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
* redirects to "/".
* @param handlersConsumer the handlers consumer
* @return the {@link FormLoginSpec} to continue configuring
* @since 6.3
*/
public FormLoginSpec authenticationSuccessHandler(
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
handlersConsumer.accept(this.authenticationSuccessHandlers);
return this;
}
@@ -2554,11 +2934,18 @@ public class ServerHttpSecurity {
authenticationFilter.setRequiresAuthenticationMatcher(this.requiresAuthenticationMatcher);
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
authenticationFilter.setAuthenticationConverter(new ServerFormLoginAuthenticationConverter());
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN);
}
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
if (this.authenticationSuccessHandlers.isEmpty()) {
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
}
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
}
}
private final class LoginPageSpec {
@@ -3737,7 +4124,12 @@ public class ServerHttpSecurity {
private ReactiveOidcSessionRegistry oidcSessionRegistry;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private final RedirectServerAuthenticationSuccessHandler defaultAuthenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
List.of(this.defaultAuthenticationSuccessHandler));
private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();
private ServerAuthenticationFailureHandler authenticationFailureHandler;
@@ -3785,7 +4177,8 @@ public class ServerHttpSecurity {
/**
* The {@link ServerAuthenticationSuccessHandler} used after authentication
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}
* redirecting to "/".
* redirecting to "/". Note that this method clears previously added success
* handlers via {@link #authenticationSuccessHandler(Consumer)}
* @param authenticationSuccessHandler the success handler to use
* @return the {@link OAuth2LoginSpec} to customize
* @since 5.2
@@ -3793,7 +4186,25 @@ public class ServerHttpSecurity {
public OAuth2LoginSpec authenticationSuccessHandler(
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
authenticationSuccessHandler((handlers) -> {
handlers.clear();
handlers.add(authenticationSuccessHandler);
});
return this;
}
/**
* Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
* default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
* redirects to "/".
* @param handlersConsumer the handlers consumer
* @return the {@link OAuth2LoginSpec} to continue configuring
* @since 6.3
*/
public OAuth2LoginSpec authenticationSuccessHandler(
Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
handlersConsumer.accept(this.authenticationSuccessHandlers);
return this;
}
@@ -4050,12 +4461,11 @@ public class ServerHttpSecurity {
}
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
if (this.authenticationSuccessHandler == null) {
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
handler.setRequestCache(http.requestCache.requestCache);
this.authenticationSuccessHandler = handler;
this.defaultAuthenticationSuccessHandler.setRequestCache(http.requestCache.requestCache);
if (this.authenticationSuccessHandlers.isEmpty()) {
return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
}
return this.authenticationSuccessHandler;
return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
}
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
@@ -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.
@@ -26,6 +26,7 @@ import org.springframework.security.config.annotation.web.configurers.AuthorizeH
import org.springframework.security.core.Authentication
import org.springframework.security.web.access.intercept.AuthorizationFilter
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
import org.springframework.security.web.access.IpAddressAuthorizationManager
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
import org.springframework.security.web.util.matcher.AnyRequestMatcher
import org.springframework.security.web.util.matcher.RequestMatcher
@@ -222,6 +223,13 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl() {
return AuthorityAuthorizationManager.hasAnyRole(*roles)
}
/**
* Require a specific IP or range of IP addresses.
* @since 6.3
*/
fun hasIpAddress(ipAddress: String): AuthorizationManager<RequestAuthorizationContext> =
IpAddressAuthorizationManager.hasIpAddress(ipAddress)
/**
* Specify that URLs are allowed by anyone.
*/
@@ -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.
@@ -38,6 +38,8 @@ import jakarta.servlet.http.HttpServletRequest
* @property loginProcessingUrl the URL to validate the credentials
* @property permitAll whether to grant access to the urls for [failureUrl] as well as
* for the [HttpSecurityBuilder], the [loginPage] and [loginProcessingUrl] for every user
* @property usernameParameter the HTTP parameter to look for the username when performing authentication
* @property passwordParameter the HTTP parameter to look for the password when performing authentication
*/
@SecurityMarker
class FormLoginDsl {
@@ -48,6 +50,8 @@ class FormLoginDsl {
var loginProcessingUrl: String? = null
var permitAll: Boolean? = null
var authenticationDetailsSource: AuthenticationDetailsSource<HttpServletRequest, *>? = null
var usernameParameter: String? = null
var passwordParameter: String? = null
private var defaultSuccessUrlOption: Pair<String, Boolean>? = null
@@ -95,6 +99,8 @@ class FormLoginDsl {
authenticationSuccessHandler?.also { login.successHandler(authenticationSuccessHandler) }
authenticationFailureHandler?.also { login.failureHandler(authenticationFailureHandler) }
authenticationDetailsSource?.also { login.authenticationDetailsSource(authenticationDetailsSource) }
usernameParameter?.also { login.usernameParameter(usernameParameter) }
passwordParameter?.also { login.passwordParameter(passwordParameter) }
if (disabled) {
login.disable()
}
@@ -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.
@@ -707,6 +707,69 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
this.http.saml2Login(saml2LoginCustomizer)
}
/**
* Configures logout support for a SAML 2.0 Service Provider. <br>
* <br>
*
* Implements the <b>Single Logout Profile, using POST and REDIRECT bindings</b>, as
* documented in the
* <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0
* Core, Profiles and Bindings</a> specifications. <br>
* <br>
*
* As a prerequisite to using this feature, is that you have a SAML v2.0 Asserting
* Party to send a logout request to. The representation of the relying party and the
* asserting party is contained within [RelyingPartyRegistration]. <br>
* <br>
*
* [RelyingPartyRegistration] (s) are composed within a
* [RelyingPartyRegistrationRepository], which is <b>required</b> and must be
* registered with the [ApplicationContext] or configured via
* [HttpSecurityDsl.saml2Login].<br>
* <br>
*
* The default configuration provides an auto-generated logout endpoint at
* `/logout` and redirects to `/login?logout` when
* logout completes. <br>
* <br>
*
* <p>
* <h2>Example Configuration</h2>
*
* The following example shows the minimal configuration required, using a
* hypothetical asserting party.
*
* Example:
*
* ```
* @Configuration
* @EnableWebSecurity
* class SecurityConfig {
*
* @Bean
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* saml2Login {
* relyingPartyRegistration = getSaml2RelyingPartyRegistration()
* }
* saml2Logout { }
* }
* return http.build()
* }
* }
* ```
*
* <p>
* @param saml2LogoutConfiguration custom configuration to configure the
* SAML 2.0 service provider
* @since 6.3
* @see [Saml2LogoutDsl]
*/
fun saml2Logout(saml2LogoutConfiguration: Saml2LogoutDsl.() -> Unit) {
val saml2LogoutCustomizer = Saml2LogoutDsl().apply(saml2LogoutConfiguration).get()
this.http.saml2Logout(saml2LogoutCustomizer)
}
/**
* Configures a SAML 2.0 relying party metadata endpoint.
*
@@ -0,0 +1,69 @@
/*
* Copyright 2002-2021 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.annotation.web
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
import org.springframework.security.config.annotation.web.saml2.LogoutRequestDsl
import org.springframework.security.config.annotation.web.saml2.LogoutResponseDsl
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
/**
* A Kotlin DSL to configure [HttpSecurity] SAML2 logout using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.3
* @property relyingPartyRegistrationRepository the [RelyingPartyRegistrationRepository] of relying parties,
* each party representing a service provider, SP and this host, and identity provider, IDP pair that
* communicate with each other.
* @property logoutUrl the logout page to begin the SLO redirect flow
*/
@SecurityMarker
class Saml2LogoutDsl {
var relyingPartyRegistrationRepository: RelyingPartyRegistrationRepository? = null
var logoutUrl: String? = null
private var logoutRequest: ((Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer) -> Unit)? = null
private var logoutResponse: ((Saml2LogoutConfigurer<HttpSecurity>.LogoutResponseConfigurer) -> Unit)? = null
/**
* Configures SAML 2.0 Logout Request components
* @param logoutRequestConfig the {@link Customizer} to provide more
* options for the {@link LogoutRequestConfigurer}
*/
fun logoutRequest(logoutRequestConfig: LogoutRequestDsl.() -> Unit) {
this.logoutRequest = LogoutRequestDsl().apply(logoutRequestConfig).get()
}
/**
* Configures SAML 2.0 Logout Response components
* @param logoutResponseConfig the {@link Customizer} to provide more
* options for the {@link LogoutResponseConfigurer}
*/
fun logoutResponse(logoutResponseConfig: LogoutResponseDsl.() -> Unit) {
this.logoutResponse = LogoutResponseDsl().apply(logoutResponseConfig).get()
}
internal fun get(): (Saml2LogoutConfigurer<HttpSecurity>) -> Unit {
return { saml2Logout ->
relyingPartyRegistrationRepository?.also { saml2Logout.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository) }
logoutUrl?.also { saml2Logout.logoutUrl(logoutUrl) }
logoutRequest?.also { saml2Logout.logoutRequest(logoutRequest) }
logoutResponse?.also { saml2Logout.logoutResponse(logoutResponse) }
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.
* 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.annotation.web.saml2
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver
/**
* A Kotlin DSL to configure SAML 2.0 Logout Request components using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.3
* @property logoutUrl The URL by which the asserting party can send a SAML 2.0 Logout Request.
* The Asserting Party should use whatever HTTP method specified in {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @property logoutRequestValidator the [Saml2LogoutRequestValidator] to use for validating incoming {@code LogoutRequest}s.
* @property logoutRequestResolver the [Saml2LogoutRequestResolver] to use for generating outgoing {@code LogoutRequest}s.
* @property logoutRequestRepository the [Saml2LogoutRequestRepository] to use for storing outgoing {@code LogoutRequest}s for
* linking to the corresponding {@code LogoutResponse} from the asserting party
*/
@Saml2SecurityMarker
class LogoutRequestDsl {
var logoutUrl = "/logout/saml2/slo"
var logoutRequestValidator: Saml2LogoutRequestValidator? = null
var logoutRequestResolver: Saml2LogoutRequestResolver? = null
var logoutRequestRepository: Saml2LogoutRequestRepository = HttpSessionLogoutRequestRepository()
internal fun get(): (Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer) -> Unit {
return { logoutRequest ->
logoutUrl.also { logoutRequest.logoutUrl(logoutUrl) }
logoutRequestValidator?.also { logoutRequest.logoutRequestValidator(logoutRequestValidator) }
logoutRequestResolver?.also { logoutRequest.logoutRequestResolver(logoutRequestResolver) }
logoutRequestRepository.also { logoutRequest.logoutRequestRepository(logoutRequestRepository) }
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.
* 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.annotation.web.saml2
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver
/**
* A Kotlin DSL to configure SAML 2.0 Logout Response components using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.3
* @property logoutUrl The URL by which the asserting party can send a SAML 2.0 Logout Response.
* The Asserting Party should use whatever HTTP method specified in {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @property logoutResponseValidator the [Saml2LogoutResponseValidator] to use for validating incoming {@code LogoutResponse}s.
* @property logoutResponseResolver the [Saml2LogoutResponseResolver] to use for generating outgoing {@code LogoutResponse}s.
*/
@Saml2SecurityMarker
class LogoutResponseDsl {
var logoutUrl = "/logout/saml2/slo"
var logoutResponseValidator: Saml2LogoutResponseValidator? = null
var logoutResponseResolver: Saml2LogoutResponseResolver? = null
internal fun get(): (Saml2LogoutConfigurer<HttpSecurity>.LogoutResponseConfigurer) -> Unit {
return { logoutResponse ->
logoutUrl.also { logoutResponse.logoutUrl(logoutUrl) }
logoutResponseValidator?.also { logoutResponse.logoutResponseValidator(logoutResponseValidator) }
logoutResponseResolver?.also { logoutResponse.logoutResponseResolver(logoutResponseResolver) }
}
}
}
@@ -0,0 +1,26 @@
/*
* 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.
* 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.annotation.web.saml2
/**
* Marker annotation indicating that the annotated class is part of the SAML 2.0 logout security DSL.
*
* @author Josh Cummings
* @since 6.3
*/
@DslMarker
annotation class Saml2SecurityMarker
@@ -682,6 +682,36 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
this.http.oidcLogout(oidcLogoutCustomizer)
}
/**
* Configures Session Management support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebFluxSecurity
* open class SecurityConfig {
*
* @Bean
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
* return http {
* sessionManagement {
* sessionConcurrency { }
* }
* }
* }
* }
* ```
*
* @param sessionManagementConfig custom configuration to configure the Session Management
* @since 6.3
* @see [ServerSessionManagementDsl]
*/
fun sessionManagement(sessionManagementConfig: ServerSessionManagementDsl.() -> Unit) {
val sessionManagementCustomizer = ServerSessionManagementDsl().apply(sessionManagementConfig).get()
this.http.sessionManagement(sessionManagementCustomizer)
}
/**
* Apply all configurations to the provided [ServerHttpSecurity]
*/
@@ -0,0 +1,48 @@
/*
* 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.web.server
import org.springframework.security.core.session.ReactiveSessionRegistry
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler
import org.springframework.security.web.server.authentication.SessionLimit
/**
* A Kotlin DSL to configure [ServerHttpSecurity] Session Concurrency support using idiomatic Kotlin code.
*
* @author Marcus da Coregio
* @since 6.3
*/
@ServerSecurityMarker
class ServerSessionConcurrencyDsl {
var maximumSessions: SessionLimit? = null
var maximumSessionsExceededHandler: ServerMaximumSessionsExceededHandler? = null
var sessionRegistry: ReactiveSessionRegistry? = null
internal fun get(): (ServerHttpSecurity.SessionManagementSpec.ConcurrentSessionsSpec) -> Unit {
return { sessionConcurrency ->
maximumSessions?.also {
sessionConcurrency.maximumSessions(maximumSessions!!)
}
maximumSessionsExceededHandler?.also {
sessionConcurrency.maximumSessionsExceededHandler(maximumSessionsExceededHandler!!)
}
sessionRegistry?.also {
sessionConcurrency.sessionRegistry(sessionRegistry!!)
}
}
}
}
@@ -0,0 +1,64 @@
/*
* 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.web.server
/**
* A Kotlin DSL to configure [ServerHttpSecurity] Session Management using idiomatic Kotlin code.
*
* @author Marcus da Coregio
* @since 6.3
*/
@ServerSecurityMarker
class ServerSessionManagementDsl {
private var sessionConcurrency: ((ServerHttpSecurity.SessionManagementSpec.ConcurrentSessionsSpec) -> Unit)? = null
/**
* Enables Session Management support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebFluxSecurity
* open class SecurityConfig {
*
* @Bean
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
* return http {
* sessionManagement {
* sessionConcurrency {
* maximumSessions = { authentication -> Mono.just(1) }
* }
* }
* }
* }
* }
* ```
*
* @param backChannelConfig custom configurations to configure OIDC 1.0 Back-Channel Logout support
* @see [ServerOidcBackChannelLogoutDsl]
*/
fun sessionConcurrency(sessionConcurrencyConfig: ServerSessionConcurrencyDsl.() -> Unit) {
this.sessionConcurrency = ServerSessionConcurrencyDsl().apply(sessionConcurrencyConfig).get()
}
internal fun get(): (ServerHttpSecurity.SessionManagementSpec) -> Unit {
return { sessionManagement ->
sessionConcurrency?.also { sessionManagement.concurrentSessions(sessionConcurrency) }
}
}
}
@@ -1,4 +1,5 @@
http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd
http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd
http\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd
http\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd
http\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd
http\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd
@@ -22,7 +23,8 @@ http\://www.springframework.org/schema/security/spring-security-2.0.xsd=org/spri
http\://www.springframework.org/schema/security/spring-security-2.0.1.xsd=org/springframework/security/config/spring-security-2.0.1.xsd
http\://www.springframework.org/schema/security/spring-security-2.0.2.xsd=org/springframework/security/config/spring-security-2.0.2.xsd
http\://www.springframework.org/schema/security/spring-security-2.0.4.xsd=org/springframework/security/config/spring-security-2.0.4.xsd
https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd
https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.3.xsd
https\://www.springframework.org/schema/security/spring-security-6.3.xsd=org/springframework/security/config/spring-security-6.3.xsd
https\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd
https\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd
https\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd
@@ -248,7 +248,6 @@ class SpringSecurityCoreVersionSerializableTests {
@ParameterizedTest
@MethodSource("getFilesToDeserialize")
@Disabled("The feature is only supported for versions >= 6.3")
void shouldBeAbleToDeserializeClassFromPreviousVersion(Path filePath) {
try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile());
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
@@ -0,0 +1,137 @@
/*
* 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.
* 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.annotation.method.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authorization.AuthorizationProxyFactory;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link PrePostMethodSecurityConfiguration}.
*
* @author Evgeniy Cheban
* @author Josh Cummings
*/
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
@SecurityTestExecutionListeners
public class AuthorizationProxyConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
AuthorizationProxyFactory proxyFactory;
@WithMockUser
@Test
public void proxyWhenNotPreAuthorizedThenDenies() {
this.spring.register(DefaultsConfig.class).autowire();
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::makeToast)
.withMessage("Access Denied");
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::extractBread)
.withMessage("Access Denied");
}
@WithMockUser(roles = "ADMIN")
@Test
public void proxyWhenPreAuthorizedThenAllows() {
this.spring.register(DefaultsConfig.class).autowire();
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
toaster.makeToast();
assertThat(toaster.extractBread()).isEqualTo("yummy");
}
@Test
public void proxyReactiveWhenNotPreAuthorizedThenDenies() {
this.spring.register(ReactiveDefaultsConfig.class).autowire();
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
Authentication user = TestAuthentication.authenticatedUser();
StepVerifier
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
.verifyError(AccessDeniedException.class);
StepVerifier
.create(toaster.reactiveExtractBread().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
.verifyError(AccessDeniedException.class);
}
@Test
public void proxyReactiveWhenPreAuthorizedThenAllows() {
this.spring.register(ReactiveDefaultsConfig.class).autowire();
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
Authentication admin = TestAuthentication.authenticatedAdmin();
StepVerifier
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(admin)))
.expectNext()
.verifyComplete();
}
@EnableMethodSecurity
@Configuration
static class DefaultsConfig {
}
@EnableReactiveMethodSecurity
@Configuration
static class ReactiveDefaultsConfig {
}
static class Toaster {
@PreAuthorize("hasRole('ADMIN')")
void makeToast() {
}
@PostAuthorize("hasRole('ADMIN')")
String extractBread() {
return "yummy";
}
@PreAuthorize("hasRole('ADMIN')")
Mono<Void> reactiveMakeToast() {
return Mono.empty();
}
@PostAuthorize("hasRole('ADMIN')")
Mono<String> reactiveExtractBread() {
return Mono.just("yummy");
}
}
}
@@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.method.configuration;
import reactor.core.publisher.Mono;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
@@ -45,4 +47,20 @@ public class Authz {
return message != null && message.contains(authentication.getName());
}
public AuthorizationResult checkResult(boolean result) {
return new AuthzResult(result);
}
public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
return Mono.just(checkResult(result));
}
public static class AuthzResult extends AuthorizationDecision {
public AuthzResult(boolean granted) {
super(granted);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -16,23 +16,42 @@
package org.springframework.security.config.annotation.method.configuration;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.parameters.P;
import org.springframework.util.StringUtils;
/**
* @author Rob Winch
*/
@MethodSecurityService.Mask("classmask")
public interface MethodSecurityService {
@PreAuthorize("denyAll")
@@ -53,6 +72,9 @@ public interface MethodSecurityService {
@RolesAllowed("ADMIN")
String jsr250RolesAllowed();
@RolesAllowed("USER")
String jsr250RolesAllowedUser();
@Secured({ "ROLE_USER", "RUN_AS_SUPER" })
Authentication runAs();
@@ -68,6 +90,9 @@ public interface MethodSecurityService {
@PreAuthorize("hasRole('ADMIN')")
void preAuthorizeAdmin();
@PreAuthorize("hasRole('USER')")
void preAuthorizeUser();
@PreAuthorize("hasPermission(#object,'read')")
String hasPermission(String object);
@@ -90,8 +115,248 @@ public interface MethodSecurityService {
@PostAuthorize("returnObject.size == 2")
List<String> manyAnnotations(List<String> array);
@PreFilter("filterObject != 'DropOnPreFilter'")
@PreAuthorize("#list.remove('DropOnPreAuthorize')")
@Secured("ROLE_SECURED")
@RolesAllowed("JSR250")
@PostAuthorize("#list.remove('DropOnPostAuthorize')")
@PostFilter("filterObject != 'DropOnPostFilter'")
List<String> allAnnotations(List<String> list);
@RequireUserRole
@RequireAdminRole
void repeatedAnnotations();
@PreAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = StartMaskingHandlerChild.class)
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
String preAuthorizeThrowAccessDeniedManually();
@PostAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = CardNumberMaskingPostProcessor.class)
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PostAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = PostMaskingPostProcessor.class)
String postAuthorizeThrowAccessDeniedManually();
@PreAuthorize("denyAll()")
@Mask("methodmask")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
String preAuthorizeDeniedMethodWithMaskAnnotation();
@PreAuthorize("denyAll()")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
String preAuthorizeDeniedMethodWithNoMaskAnnotation();
@NullDenied(role = "ADMIN")
String postAuthorizeDeniedWithNullDenied();
@PostAuthorize("denyAll()")
@Mask("methodmask")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
String postAuthorizeDeniedMethodWithMaskAnnotation();
@PostAuthorize("denyAll()")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
String postAuthorizeDeniedMethodWithNoMaskAnnotation();
@PreAuthorize("hasRole('ADMIN')")
@Mask(expression = "@myMasker.getMask()")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
String preAuthorizeWithMaskAnnotationUsingBean();
@PostAuthorize("hasRole('ADMIN')")
@Mask(expression = "@myMasker.getMask(returnObject)")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
String postAuthorizeWithMaskAnnotationUsingBean();
@AuthorizeReturnObject
UserRecordWithEmailProtected getUserRecordWithEmailProtected();
@PreAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = UserFallbackDeniedHandler.class)
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();
@PreAuthorize("@authz.checkResult(#result)")
@PostAuthorize("@authz.checkResult(!#result)")
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
String checkCustomResult(boolean result);
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
return "***";
}
}
class StartMaskingHandlerChild extends StarMaskingHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
return super.handleDeniedInvocation(methodInvocation, result) + "-child";
}
}
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
MaskValueResolver maskValueResolver;
MaskAnnotationHandler(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
}
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return handle(methodInvocation, authorizationResult);
}
}
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedHandler {
MaskValueResolver maskValueResolver;
MaskAnnotationPostProcessor(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
@Override
public Object handleDeniedInvocation(MethodInvocation mi, AuthorizationResult authorizationResult) {
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, mi, null);
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
}
}
class MaskValueResolver {
DefaultMethodSecurityExpressionHandler expressionHandler;
MaskValueResolver(ApplicationContext context) {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
this.expressionHandler.setApplicationContext(context);
}
String resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
if (StringUtils.hasText(mask.value())) {
return mask.value();
}
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
EvaluationContext evaluationContext = this.expressionHandler
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
if (returnObject != null) {
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
}
return expression.getValue(evaluationContext, String.class);
}
}
class PostMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return "***";
}
}
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
static String MASK = "****-****-****-";
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return "***";
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult contextObject, AuthorizationResult result) {
String cardNumber = (String) contextObject.getResult();
return MASK + cardNumber.substring(cardNumber.length() - 4);
}
}
class NullPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return null;
}
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Mask {
String value() default "";
String expression() default "";
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@PostAuthorize("hasRole('{role}')")
@HandleAuthorizationDenied(handlerClass = NullPostProcessor.class)
@interface NullDenied {
String role();
}
class UserFallbackDeniedHandler implements MethodAuthorizationDeniedHandler {
private static final UserRecordWithEmailProtected FALLBACK = new UserRecordWithEmailProtected("Protected",
"Protected");
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return FALLBACK;
}
}
}
@@ -28,4 +28,14 @@ public class MethodSecurityServiceConfig {
return new MethodSecurityServiceImpl();
}
@Bean
ReactiveMethodSecurityService reactiveService() {
return new ReactiveMethodSecurityServiceImpl();
}
@Bean
Authz authz() {
return new Authz();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.List;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -56,6 +58,11 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
return null;
}
@Override
public String jsr250RolesAllowedUser() {
return null;
}
@Override
public Authentication runAs() {
return SecurityContextHolder.getContext().getAuthentication();
@@ -73,6 +80,10 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
public void preAuthorizeAdmin() {
}
@Override
public void preAuthorizeUser() {
}
@Override
public String preAuthorizePermitAll() {
return null;
@@ -108,8 +119,88 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
return object;
}
@Override
public List<String> allAnnotations(List<String> list) {
return null;
}
@Override
public void repeatedAnnotations() {
}
@Override
public String postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}
@Override
public String preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}
@Override
public String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}
@Override
public String preAuthorizeThrowAccessDeniedManually() {
throw new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false));
}
@Override
public String postAuthorizeThrowAccessDeniedManually() {
throw new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false));
}
@Override
public String preAuthorizeDeniedMethodWithMaskAnnotation() {
return "ok";
}
@Override
public String preAuthorizeDeniedMethodWithNoMaskAnnotation() {
return "ok";
}
@Override
public String postAuthorizeDeniedWithNullDenied() {
return "ok";
}
@Override
public String postAuthorizeDeniedMethodWithMaskAnnotation() {
return "ok";
}
@Override
public String postAuthorizeDeniedMethodWithNoMaskAnnotation() {
return "ok";
}
@Override
public String preAuthorizeWithMaskAnnotationUsingBean() {
return "ok";
}
@Override
public String postAuthorizeWithMaskAnnotationUsingBean() {
return "ok";
}
@Override
public UserRecordWithEmailProtected getUserRecordWithEmailProtected() {
return new UserRecordWithEmailProtected("username", "useremail@example.com");
}
@Override
public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
return new UserRecordWithEmailProtected("username", "useremail@example.com");
}
@Override
public String checkCustomResult(boolean result) {
return "ok";
}
}
@@ -0,0 +1,29 @@
/*
* 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.
* 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.annotation.method.configuration;
public class MyMasker {
public String getMask(String value) {
return value + "-masked";
}
public String getMask() {
return "mask";
}
}
@@ -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.
@@ -17,9 +17,14 @@
package org.springframework.security.config.annotation.method.configuration;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -36,6 +41,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.security.access.AccessDeniedException;
@@ -46,12 +52,24 @@ import org.springframework.security.access.annotation.ExpressionProtectedBusines
import org.springframework.security.access.annotation.Jsr250BusinessServiceImpl;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext;
@@ -70,9 +88,12 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
@@ -439,7 +460,6 @@ public class PrePostMethodSecurityConfigurationTests {
assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse();
}
// gh-13572
@Test
public void configureWhenBeanOverridingDisallowedThenWorks() {
this.spring.register(MethodSecurityServiceConfig.class, BusinessServiceConfig.class)
@@ -447,10 +467,506 @@ public class PrePostMethodSecurityConfigurationTests {
.autowire();
}
@WithMockUser(roles = "ADMIN")
@Test
public void methodSecurityAdminWhenRoleHierarchyBeanAvailableThenUses() {
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
this.methodSecurityService.preAuthorizeUser();
this.methodSecurityService.securedUser();
this.methodSecurityService.jsr250RolesAllowedUser();
}
@WithMockUser
@Test
public void methodSecurityUserWhenRoleHierarchyBeanAvailableThenUses() {
this.spring.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class).autowire();
this.methodSecurityService.preAuthorizeUser();
this.methodSecurityService.securedUser();
this.methodSecurityService.jsr250RolesAllowedUser();
}
@WithMockUser(roles = "ADMIN")
@Test
public void methodSecurityAdminWhenAuthorizationEventPublisherBeanAvailableThenUses() {
this.spring
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
AuthorizationEventPublisherConfig.class)
.autowire();
this.methodSecurityService.preAuthorizeUser();
this.methodSecurityService.securedUser();
this.methodSecurityService.jsr250RolesAllowedUser();
}
@WithMockUser
@Test
public void methodSecurityUserWhenAuthorizationEventPublisherBeanAvailableThenUses() {
this.spring
.register(RoleHierarchyConfig.class, MethodSecurityServiceConfig.class,
AuthorizationEventPublisherConfig.class)
.autowire();
this.methodSecurityService.preAuthorizeUser();
this.methodSecurityService.securedUser();
this.methodSecurityService.jsr250RolesAllowedUser();
}
@Test
public void allAnnotationsWhenAdviceBeforeOffsetPreFilterThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPreFilterConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(5);
assertThat(filtered).containsExactly("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
}
@Test
public void allAnnotationsWhenAdviceBeforeOffsetPreAuthorizeThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPreAuthorizeConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(4);
assertThat(filtered).containsExactly("DropOnPreAuthorize", "DropOnPostAuthorize", "DropOnPostFilter",
"DoNotDrop");
}
@Test
public void allAnnotationsWhenAdviceBeforeOffsetSecuredThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetSecuredConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(3);
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser
public void allAnnotationsWhenAdviceBeforeOffsetJsr250WithInsufficientRolesThenFails() {
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
}
@Test
@WithMockUser(roles = "SECURED")
public void allAnnotationsWhenAdviceBeforeOffsetJsr250ThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetJsr250Config.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(3);
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser(roles = { "SECURED" })
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeWithInsufficientRolesThenFails() {
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.methodSecurityService.allAnnotations(new ArrayList<>(list)));
}
@Test
@WithMockUser(roles = { "SECURED", "JSR250" })
public void allAnnotationsWhenAdviceBeforeOffsetPostAuthorizeThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPostAuthorizeConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(3);
assertThat(filtered).containsExactly("DropOnPostAuthorize", "DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser(roles = { "SECURED", "JSR250" })
public void allAnnotationsWhenAdviceBeforeOffsetPostFilterThenReturnsFilteredList() {
this.spring.register(ReturnBeforeOffsetPostFilterConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(2);
assertThat(filtered).containsExactly("DropOnPostFilter", "DoNotDrop");
}
@Test
@WithMockUser(roles = { "SECURED", "JSR250" })
public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
this.spring.register(ReturnAfterAllOffsetConfig.class).autowire();
List<String> list = Arrays.asList("DropOnPreFilter", "DropOnPreAuthorize", "DropOnPostAuthorize",
"DropOnPostFilter", "DoNotDrop");
List<String> filtered = this.methodSecurityService.allAnnotations(new ArrayList<>(list));
assertThat(filtered).hasSize(1);
assertThat(filtered).containsExactly("DoNotDrop");
}
@Test
@WithMockUser
public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.hasRole("USER")).isTrue();
}
@Test
@WithMockUser
public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.hasUserRole()).isTrue();
}
@Test
public void methodWhenParameterizedAnnotationThenFails() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(service::placeholdersOnlyResolvedByMetaAnnotations);
}
@Test
@WithMockUser(authorities = "SCOPE_message:read")
public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.readMessage()).isEqualTo("message");
}
@Test
@WithMockUser(roles = "ADMIN")
public void methodWhenMultiplePlaceholdersHasRoleThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.readMessage()).isEqualTo("message");
}
@Test
@WithMockUser
public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
service.startsWithDave("daveMatthews");
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> service.startsWithDave("jenniferHarper"));
}
@Test
@WithMockUser
public void methodWhenPreFilterMetaAnnotationThenFilters() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.parametersContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
.containsExactly("dave");
}
@Test
@WithMockUser
public void methodWhenPostFilterMetaAnnotationThenFilters() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.resultsContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
.containsExactly("dave");
}
@Test
@WithMockUser(authorities = "airplane:read")
public void findByIdWhenAuthorizedResultThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Flight flight = flights.findById("1");
assertThatNoException().isThrownBy(flight::getAltitude);
assertThatNoException().isThrownBy(flight::getSeats);
}
@Test
@WithMockUser(authorities = "seating:read")
public void findByIdWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Flight flight = flights.findById("1");
assertThatNoException().isThrownBy(flight::getSeats);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
}
@Test
@WithMockUser(authorities = "seating:read")
public void findAllWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll().forEachRemaining((flight) -> {
assertThatNoException().isThrownBy(flight::getSeats);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
});
}
@Test
public void removeWhenAuthorizedResultThenRemoves() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.remove("1");
}
@Test
@WithMockUser(authorities = "airplane:read")
public void findAllWhenPostFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll()
.forEachRemaining((flight) -> assertThat(flight.getPassengers()).extracting(Passenger::getName)
.doesNotContain("Kevin Mitnick"));
}
@Test
@WithMockUser(authorities = "airplane:read")
public void findAllWhenPreFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll().forEachRemaining((flight) -> {
flight.board(new ArrayList<>(List.of("John")));
assertThat(flight.getPassengers()).extracting(Passenger::getName).doesNotContain("John");
flight.board(new ArrayList<>(List.of("John Doe")));
assertThat(flight.getPassengers()).extracting(Passenger::getName).contains("John Doe");
});
}
@Test
@WithMockUser(authorities = "seating:read")
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll().forEachRemaining((flight) -> {
List<Passenger> passengers = flight.getPassengers();
passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(passenger::getName));
});
}
@Test
@WithMockUser
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
MethodSecurityService.CardNumberMaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("****-****-****-1111");
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("***");
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class,
MethodSecurityService.StartMaskingHandlerChild.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("***-child");
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenHandled() {
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.StarMaskingHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
assertThat(service.preAuthorizeThrowAccessDeniedManually()).isEqualTo("***");
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenHandled() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.PostMaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
assertThat(service.postAuthorizeThrowAccessDeniedManually()).isEqualTo("***");
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeDeniedMethodWithMaskAnnotation();
assertThat(result).isEqualTo("methodmask");
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeDeniedMethodWithNoMaskAnnotation();
assertThat(result).isEqualTo("classmask");
}
@Test
@WithMockUser
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MetaAnnotationPlaceholderConfig.class,
MethodSecurityService.NullPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeDeniedWithNullDenied();
assertThat(result).isNull();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeDeniedMethodWithMaskAnnotation();
assertThat(result).isEqualTo("methodmask");
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeDeniedMethodWithNoMaskAnnotation();
assertThat(result).isEqualTo("classmask");
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("ok-masked");
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationPostProcessor.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("ok");
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("mask");
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.MaskAnnotationHandler.class,
MyMasker.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.preAuthorizeWithMaskAnnotationUsingBean();
assertThat(result).isEqualTo("ok");
}
@Test
@WithMockUser
void getUserWhenAuthorizedAndUserEmailIsProtectedAndNotAuthorizedThenReturnEmailMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
UserRecordWithEmailProtected.EmailMaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
UserRecordWithEmailProtected user = service.getUserRecordWithEmailProtected();
assertThat(user.email()).isEqualTo("use******@example.com");
assertThat(user.name()).isEqualTo("username");
}
@Test
@WithMockUser
void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.UserFallbackDeniedHandler.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
UserRecordWithEmailProtected user = service.getUserWithFallbackWhenUnauthorized();
assertThat(user.email()).isEqualTo("Protected");
assertThat(user.name()).isEqualTo("Protected");
}
@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
assertThat(service.checkCustomResult(false)).isNull();
verify(handler).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
verify(handler, never()).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
clearInvocations(handler);
assertThat(service.checkCustomResult(true)).isNull();
verify(handler).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
private static Advisor returnAdvisor(int order) {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*MethodSecurityServiceImpl.*");
MethodInterceptor interceptor = (mi) -> mi.getArguments()[0];
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, interceptor);
advisor.setOrder(order);
return advisor;
}
@Configuration
static class AuthzConfig {
@Bean
Authz authz() {
return new Authz();
}
}
@Configuration
@EnableCustomMethodSecurity
static class CustomMethodSecurityServiceConfig {
@@ -627,4 +1143,341 @@ public class PrePostMethodSecurityConfigurationTests {
}
@Configuration
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
static class RoleHierarchyConfig {
@Bean
static RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
roleHierarchyImpl.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchyImpl;
}
}
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPreFilterConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePreFilter() {
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPreAuthorizeConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePreAuthorize() {
return returnAdvisor(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetSecuredConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforeSecured() {
return returnAdvisor(AuthorizationInterceptorsOrder.SECURED.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetJsr250Config {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforeJsr250() {
return returnAdvisor(AuthorizationInterceptorsOrder.JSR250.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPostAuthorizeConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePreAuthorize() {
return returnAdvisor(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnBeforeOffsetPostFilterConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnBeforePostFilter() {
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET - 1);
}
}
@Configuration
@Import(OffsetConfig.class)
static class ReturnAfterAllOffsetConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor returnAfterAll() {
return returnAdvisor(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + OffsetConfig.OFFSET + 1);
}
}
@Configuration
@EnableMethodSecurity(offset = OffsetConfig.OFFSET, jsr250Enabled = true, securedEnabled = true)
static class OffsetConfig {
static final int OFFSET = 2;
@Bean
MethodSecurityService methodSecurityService() {
return new MethodSecurityServiceImpl();
}
@Bean
Authz authz() {
return new Authz();
}
}
@Configuration
@EnableMethodSecurity
static class MetaAnnotationPlaceholderConfig {
@Bean
PrePostTemplateDefaults methodSecurityDefaults() {
return new PrePostTemplateDefaults();
}
@Bean
MetaAnnotationService metaAnnotationService() {
return new MetaAnnotationService();
}
}
static class MetaAnnotationService {
@RequireRole(role = "#role")
boolean hasRole(String role) {
return true;
}
@RequireRole(role = "'USER'")
boolean hasUserRole() {
return true;
}
@PreAuthorize("hasRole({role})")
void placeholdersOnlyResolvedByMetaAnnotations() {
}
@HasClaim(claim = "message:read", roles = { "'ADMIN'" })
String readMessage() {
return "message";
}
@ResultStartsWith("dave")
String startsWithDave(String value) {
return value;
}
@ParameterContains("dave")
List<String> parametersContainDave(List<String> list) {
return list;
}
@ResultContains("dave")
List<String> resultsContainDave(List<String> list) {
return list;
}
}
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole({role})")
@interface RequireRole {
String role();
}
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})")
@interface HasClaim {
String claim();
String[] roles() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.startsWith('{value}')")
@interface ResultStartsWith {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@PreFilter("filterObject.contains('{value}')")
@interface ParameterContains {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@PostFilter("filterObject.contains('{value}')")
@interface ResultContains {
String value();
}
@EnableMethodSecurity
@Configuration
static class AuthorizeResultConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
return (f) -> f.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
}
@Bean
FlightRepository flights() {
FlightRepository flights = new FlightRepository();
Flight one = new Flight("1", 35000d, 35);
one.board(new ArrayList<>(List.of("Marie Curie", "Kevin Mitnick", "Ada Lovelace")));
flights.save(one);
Flight two = new Flight("2", 32000d, 72);
two.board(new ArrayList<>(List.of("Albert Einstein")));
flights.save(two);
return flights;
}
@Bean
RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
}
}
@AuthorizeReturnObject
static class FlightRepository {
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
Iterator<Flight> findAll() {
return this.flights.values().iterator();
}
Flight findById(String id) {
return this.flights.get(id);
}
Flight save(Flight flight) {
this.flights.put(flight.getId(), flight);
return flight;
}
void remove(String id) {
this.flights.remove(id);
}
}
@AuthorizeReturnObject
static class Flight {
private final String id;
private final Double altitude;
private final Integer seats;
private final List<Passenger> passengers = new ArrayList<>();
Flight(String id, Double altitude, Integer seats) {
this.id = id;
this.altitude = altitude;
this.seats = seats;
}
String getId() {
return this.id;
}
@PreAuthorize("hasAuthority('airplane:read')")
Double getAltitude() {
return this.altitude;
}
@PreAuthorize("hasAuthority('seating:read')")
Integer getSeats() {
return this.seats;
}
@PostAuthorize("hasAuthority('seating:read')")
@PostFilter("filterObject.name != 'Kevin Mitnick'")
List<Passenger> getPassengers() {
return this.passengers;
}
@PreAuthorize("hasAuthority('seating:read')")
@PreFilter("filterObject.contains(' ')")
void board(List<String> passengers) {
for (String passenger : passengers) {
this.passengers.add(new Passenger(passenger));
}
}
}
public static class Passenger {
String name;
public Passenger(String name) {
this.name = name;
}
@PreAuthorize("hasAuthority('airplane:read')")
public String getName() {
return this.name;
}
}
@EnableMethodSecurity
static class CustomResultConfig {
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}
}
}
@@ -0,0 +1,215 @@
/*
* 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.
* 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.annotation.method.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
@SecurityTestExecutionListeners
public class PrePostReactiveMethodSecurityConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Test
@WithMockUser
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.CardNumberMaskingPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
.expectNext("****-****-****-1111")
.verifyComplete();
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"))
.expectNext("***")
.verifyComplete();
}
@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class,
ReactiveMethodSecurityService.StartMaskingHandlerChild.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111"))
.expectNext("***-child")
.verifyComplete();
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeDeniedMethodWithMaskAnnotation())
.expectNext("methodmask")
.verifyComplete();
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeDeniedMethodWithNoMaskAnnotation())
.expectNext("classmask")
.verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.PostMaskingPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeThrowAccessDeniedManually()).expectNext("***").verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenHandled() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.StarMaskingHandler.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeThrowAccessDeniedManually()).expectNext("***").verifyComplete();
}
@Test
@WithMockUser
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class, ReactiveMethodSecurityService.NullPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeDeniedWithNullDenied()).verifyComplete();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeDeniedMethodWithMaskAnnotation())
.expectNext("methodmask")
.verifyComplete();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeDeniedMethodWithNoMaskAnnotation())
.expectNext("classmask")
.verifyComplete();
}
@Test
@WithMockUser
void postAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean())
.expectNext("ok-masked")
.verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationPostProcessor.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.postAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
}
@Test
@WithMockUser
void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationUsingBeanThenHandlerCanUseMaskFromOtherAnnotation() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("mask").verifyComplete();
}
@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMethodNormally() {
this.spring
.register(MethodSecurityServiceEnabledConfig.class,
ReactiveMethodSecurityService.MaskAnnotationHandler.class, MyMasker.class)
.autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete();
}
@Configuration
@EnableReactiveMethodSecurity
static class MethodSecurityServiceEnabledConfig {
@Bean
ReactiveMethodSecurityService methodSecurityService() {
return new ReactiveMethodSecurityServiceImpl();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -16,22 +16,53 @@
package org.springframework.security.config.annotation.method.configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.intercept.method.MockMethodInvocation;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* @author Tadaya Tsuyukubo
@@ -41,14 +72,13 @@ public class ReactiveMethodSecurityConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
@Autowired(required = false)
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;
@Test
public void rolePrefixWithGrantedAuthorityDefaults() throws NoSuchMethodException {
this.spring.register(WithRolePrefixConfiguration.class).autowire();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
"CUSTOM_ABC");
Authentication authentication = TestAuthentication.authenticatedUser(authorities("CUSTOM_ABC"));
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
methodInvocation);
@@ -62,8 +92,7 @@ public class ReactiveMethodSecurityConfigurationTests {
@Test
public void rolePrefixWithDefaultConfig() throws NoSuchMethodException {
this.spring.register(ReactiveMethodSecurityConfiguration.class).autowire();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
"ROLE_ABC");
Authentication authentication = TestAuthentication.authenticatedUser(authorities("ROLE_ABC"));
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
methodInvocation);
@@ -75,8 +104,7 @@ public class ReactiveMethodSecurityConfigurationTests {
@Test
public void rolePrefixWithGrantedAuthorityDefaultsAndSubclassWithProxyingEnabled() throws NoSuchMethodException {
this.spring.register(SubclassConfig.class).autowire();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("principal", "credential",
"ROLE_ABC");
Authentication authentication = TestAuthentication.authenticatedUser(authorities("ROLE_ABC"));
MockMethodInvocation methodInvocation = new MockMethodInvocation(new Foo(), Foo.class, "bar", String.class);
EvaluationContext context = this.methodSecurityExpressionHandler.createEvaluationContext(authentication,
methodInvocation);
@@ -85,6 +113,132 @@ public class ReactiveMethodSecurityConfigurationTests {
assertThat(root.hasRole("ABC")).isTrue();
}
@Test
public void findByIdWhenAuthorizedResultThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getAltitude)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(1)
.verifyComplete();
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getSeats)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(1)
.verifyComplete();
}
@Test
public void findByIdWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getSeats)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(1)
.verifyComplete();
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getAltitude)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyError(AccessDeniedException.class);
}
@Test
public void findAllWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getSeats)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(2)
.verifyComplete();
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getAltitude)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyError(AccessDeniedException.class);
}
@Test
public void removeWhenAuthorizedResultThenRemoves() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
StepVerifier.create(flights.remove("1").contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyComplete();
}
@Test
public void findAllWhenPostFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getPassengers)
.flatMap(Passenger::getName)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNext("Marie Curie", "Ada Lovelace", "Albert Einstein")
.verifyComplete();
}
@Test
public void findAllWhenPreFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Authentication pilot = TestAuthentication.authenticatedUser(authorities("airplane:read"));
StepVerifier
.create(flights.findAll()
.flatMap((flight) -> flight.board(Flux.just("John Doe", "John")).then(Mono.just(flight)))
.flatMap(Flight::getPassengers)
.flatMap(Passenger::getName)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNext("Marie Curie", "Ada Lovelace", "John Doe", "Albert Einstein", "John Doe")
.verifyComplete();
}
@Test
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Authentication pilot = TestAuthentication.authenticatedUser(authorities("seating:read"));
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getPassengers)
.flatMap(Passenger::getName)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyError(AccessDeniedException.class);
}
@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
assertThat(service.checkCustomResult(false).block()).isNull();
verify(handler).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
verify(handler, never()).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
clearInvocations(handler);
assertThat(service.checkCustomResult(true).block()).isNull();
verify(handler).handleDeniedInvocationResult(any(), any(Authz.AuthzResult.class));
verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
}
private static Consumer<User.UserBuilder> authorities(String... authorities) {
return (builder) -> builder.authorities(authorities);
}
@Configuration
@EnableReactiveMethodSecurity // this imports ReactiveMethodSecurityConfiguration
static class WithRolePrefixConfiguration {
@@ -108,4 +262,130 @@ public class ReactiveMethodSecurityConfigurationTests {
}
@EnableReactiveMethodSecurity
@Configuration
static class AuthorizeResultConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
}
@Bean
FlightRepository flights() {
FlightRepository flights = new FlightRepository();
Flight one = new Flight("1", 35000d, 35);
one.board(Flux.just("Marie Curie", "Kevin Mitnick", "Ada Lovelace")).block();
flights.save(one).block();
Flight two = new Flight("2", 32000d, 72);
two.board(Flux.just("Albert Einstein")).block();
flights.save(two).block();
return flights;
}
@Bean
Function<Passenger, Mono<Boolean>> isNotKevin() {
return (passenger) -> passenger.getName().map((name) -> !name.equals("Kevin Mitnick"));
}
}
@AuthorizeReturnObject
static class FlightRepository {
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
Flux<Flight> findAll() {
return Flux.fromIterable(this.flights.values());
}
Mono<Flight> findById(String id) {
return Mono.just(this.flights.get(id));
}
Mono<Flight> save(Flight flight) {
this.flights.put(flight.getId(), flight);
return Mono.just(flight);
}
Mono<Void> remove(String id) {
this.flights.remove(id);
return Mono.empty();
}
}
@AuthorizeReturnObject
static class Flight {
private final String id;
private final Double altitude;
private final Integer seats;
private final List<Passenger> passengers = new ArrayList<>();
Flight(String id, Double altitude, Integer seats) {
this.id = id;
this.altitude = altitude;
this.seats = seats;
}
String getId() {
return this.id;
}
@PreAuthorize("hasAuthority('airplane:read')")
Mono<Double> getAltitude() {
return Mono.just(this.altitude);
}
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
Mono<Integer> getSeats() {
return Mono.just(this.seats);
}
@PostAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
@PostFilter("@isNotKevin.apply(filterObject)")
Flux<Passenger> getPassengers() {
return Flux.fromIterable(this.passengers);
}
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
@PreFilter("filterObject.contains(' ')")
Mono<Void> board(Flux<String> passengers) {
return passengers.doOnNext((passenger) -> this.passengers.add(new Passenger(passenger))).then();
}
}
public static class Passenger {
String name;
public Passenger(String name) {
this.name = name;
}
@PreAuthorize("hasAuthority('airplane:read')")
public Mono<String> getName() {
return Mono.just(this.name);
}
}
@EnableReactiveMethodSecurity
static class CustomResultConfig {
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}
}
}
@@ -0,0 +1,255 @@
/*
* 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.
* 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.annotation.method.configuration;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
/**
* @author Rob Winch
*/
@ReactiveMethodSecurityService.Mask("classmask")
public interface ReactiveMethodSecurityService {
@PreAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = StartMaskingHandlerChild.class)
Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
@PreAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class)
Mono<String> preAuthorizeThrowAccessDeniedManually();
@PostAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = CardNumberMaskingPostProcessor.class)
Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber);
@PostAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = PostMaskingPostProcessor.class)
Mono<String> postAuthorizeThrowAccessDeniedManually();
@PreAuthorize("denyAll()")
@Mask("methodmask")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation();
@PreAuthorize("denyAll()")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation();
@NullDenied(role = "ADMIN")
Mono<String> postAuthorizeDeniedWithNullDenied();
@PostAuthorize("denyAll()")
@Mask("methodmask")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation();
@PostAuthorize("denyAll()")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation();
@PreAuthorize("hasRole('ADMIN')")
@Mask(expression = "@myMasker.getMask()")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationHandler.class)
Mono<String> preAuthorizeWithMaskAnnotationUsingBean();
@PostAuthorize("hasRole('ADMIN')")
@Mask(expression = "@myMasker.getMask(returnObject)")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationPostProcessor.class)
Mono<String> postAuthorizeWithMaskAnnotationUsingBean();
@PreAuthorize("@authz.checkReactiveResult(#result)")
@PostAuthorize("@authz.checkReactiveResult(!#result)")
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
Mono<String> checkCustomResult(boolean result);
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
return "***";
}
}
class StartMaskingHandlerChild extends StarMaskingHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
return super.handleDeniedInvocation(methodInvocation, result) + "-child";
}
}
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
MaskValueResolver maskValueResolver;
MaskAnnotationHandler(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult result) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
}
}
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedHandler {
MaskValueResolver maskValueResolver;
MaskAnnotationPostProcessor(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}
@Override
public Object handleDeniedInvocation(MethodInvocation mi, AuthorizationResult authorizationResult) {
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, mi, null);
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
}
}
class MaskValueResolver {
DefaultMethodSecurityExpressionHandler expressionHandler;
MaskValueResolver(ApplicationContext context) {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
this.expressionHandler.setApplicationContext(context);
}
Mono<String> resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
if (StringUtils.hasText(mask.value())) {
return Mono.just(mask.value());
}
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
EvaluationContext evaluationContext = this.expressionHandler
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
if (returnObject != null) {
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
}
return Mono.just(expression.getValue(evaluationContext, String.class));
}
}
class PostMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return "***";
}
}
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
static String MASK = "****-****-****-";
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return "***";
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult contextObject, AuthorizationResult result) {
String cardNumber = (String) contextObject.getResult();
return MASK + cardNumber.substring(cardNumber.length() - 4);
}
}
class NullPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return null;
}
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Mask {
String value() default "";
String expression() default "";
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@PostAuthorize("hasRole('{value}')")
@HandleAuthorizationDenied(handlerClass = NullPostProcessor.class)
@interface NullDenied {
String role();
}
}
@@ -0,0 +1,91 @@
/*
* 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.
* 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.annotation.method.configuration;
import reactor.core.publisher.Mono;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurityService {
@Override
public Mono<String> preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return Mono.just(cardNumber);
}
@Override
public Mono<String> preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
return Mono.just(cardNumber);
}
@Override
public Mono<String> preAuthorizeThrowAccessDeniedManually() {
return Mono.error(new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false)));
}
@Override
public Mono<String> postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return Mono.just(cardNumber);
}
@Override
public Mono<String> postAuthorizeThrowAccessDeniedManually() {
return Mono.error(new AuthorizationDeniedException("Access Denied", new AuthorizationDecision(false)));
}
@Override
public Mono<String> preAuthorizeDeniedMethodWithMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> preAuthorizeDeniedMethodWithNoMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeDeniedWithNullDenied() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeDeniedMethodWithMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeDeniedMethodWithNoMaskAnnotation() {
return Mono.just("ok");
}
@Override
public Mono<String> preAuthorizeWithMaskAnnotationUsingBean() {
return Mono.just("ok");
}
@Override
public Mono<String> postAuthorizeWithMaskAnnotationUsingBean() {
return Mono.just("ok");
}
@Override
public Mono<String> checkCustomResult(boolean result) {
return Mono.just("ok");
}
}
@@ -0,0 +1,65 @@
/*
* 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.
* 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.annotation.method.configuration;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodInvocationResult;
public class UserRecordWithEmailProtected {
private final String name;
private final String email;
public UserRecordWithEmailProtected(String name, String email) {
this.name = name;
this.email = email;
}
public String name() {
return this.name;
}
@PostAuthorize("hasRole('ADMIN')")
@HandleAuthorizationDenied(handlerClass = EmailMaskingPostProcessor.class)
public String email() {
return this.email;
}
public static class EmailMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation,
AuthorizationResult authorizationResult) {
return "***";
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
String email = (String) methodInvocationResult.getResult();
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
}
}
}
@@ -0,0 +1,68 @@
/*
* 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.
* 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.annotation.method.configuration.issue14637;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @author Josh Cummings
*/
@Configuration
@EnableJpaRepositories("org.springframework.security.config.annotation.method.configuration.issue14637.repo")
@EnableTransactionManagement
public class ApplicationConfig {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setDatabase(Database.HSQL);
vendorAdapter.setGenerateDdl(true);
vendorAdapter.setShowSql(true);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan(Entry.class.getPackage().getName());
factory.setDataSource(dataSource());
return factory;
}
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory().getObject());
return txManager;
}
}
@@ -0,0 +1,50 @@
/*
* 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.
* 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.annotation.method.configuration.issue14637;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
import org.springframework.security.config.annotation.method.configuration.issue14637.repo.EntryRepository;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Josh Cummings
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { ApplicationConfig.class, SecurityConfig.class })
public class Issue14637Tests {
@Autowired
private EntryRepository entries;
@Test
@WithMockUser
public void authenticateWhenInvalidPasswordThenBadCredentialsException() {
Entry entry = new Entry();
entry.setId(123L);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.entries.save(entry));
}
}
@@ -0,0 +1,29 @@
/*
* 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.
* 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.annotation.method.configuration.issue14637;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
/**
* @author Josh Cummings
*/
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}
@@ -0,0 +1,42 @@
/*
* 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.
* 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.annotation.method.configuration.issue14637.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* @author Josh Cummings
*/
@Entity
public class Entry {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
}
@@ -0,0 +1,31 @@
/*
* 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.
* 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.annotation.method.configuration.issue14637.repo;
import org.springframework.data.repository.CrudRepository;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.issue14637.domain.Entry;
/**
* @author Josh Cummings
*/
public interface EntryRepository extends CrudRepository<Entry, String> {
@PreAuthorize("#entry.id == null")
Entry save(Entry entry);
}
@@ -47,6 +47,9 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
import org.springframework.security.authentication.password.CompromisedPasswordDecision;
import org.springframework.security.authentication.password.CompromisedPasswordException;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -60,8 +63,8 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -395,6 +398,41 @@ public class HttpSecurityConfigurationTests {
this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated());
}
@Test
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordCompromisedThenUnauthorized() throws Exception {
this.spring
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
.autowire();
this.mockMvc.perform(formLogin().password("password"))
.andExpectAll(status().isFound(), redirectedUrl("/login?error"), unauthenticated());
}
@Test
void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToResetPassword() throws Exception {
this.spring
.register(SecurityEnabledRedirectIfPasswordExceptionConfig.class, UserDetailsConfig.class,
CompromisedPasswordCheckerConfig.class)
.autowire();
this.mockMvc.perform(formLogin().password("password"))
.andExpectAll(status().isFound(), redirectedUrl("/reset-password"), unauthenticated());
}
@Test
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordNotCompromisedThenSuccess() throws Exception {
this.spring
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
.autowire();
UserDetailsManager userDetailsManager = this.spring.getContext().getBean(UserDetailsManager.class);
UserDetails notCompromisedPwUser = User.withDefaultPasswordEncoder()
.username("user2")
.password("password2")
.roles("USER")
.build();
userDetailsManager.createUser(notCompromisedPwUser);
this.mockMvc.perform(formLogin().user("user2").password("password2"))
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
}
@RestController
static class NameController {
@@ -455,7 +493,7 @@ public class HttpSecurityConfigurationTests {
static class UserDetailsConfig {
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService() {
// @formatter:off
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
@@ -732,4 +770,52 @@ public class HttpSecurityConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CompromisedPasswordCheckerConfig {
@Bean
TestCompromisedPasswordChecker compromisedPasswordChecker() {
return new TestCompromisedPasswordChecker();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
static class SecurityEnabledRedirectIfPasswordExceptionConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
return http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin((form) -> form
.failureHandler((request, response, exception) -> {
if (exception instanceof CompromisedPasswordException) {
response.sendRedirect("/reset-password");
return;
}
response.sendRedirect("/login?error");
})
)
.build();
// @formatter:on
}
}
private static class TestCompromisedPasswordChecker implements CompromisedPasswordChecker {
@Override
public CompromisedPasswordDecision check(String password) {
if ("password".equals(password)) {
return new CompromisedPasswordDecision(true);
}
return new CompromisedPasswordDecision(false);
}
}
}
@@ -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.
@@ -38,8 +38,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
@@ -52,6 +50,7 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
@@ -59,17 +58,14 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -77,13 +73,10 @@ import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -327,6 +320,47 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
}
@Test
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
testTokenExchangeGrant();
}
@Test
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
testTokenExchangeGrant();
}
private void testTokenExchangeGrant() {
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
.willReturn(accessTokenResponse);
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0");
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(authentication)
.attribute(HttpServletRequest.class.getName(), this.request)
.attribute(HttpServletResponse.class.getName(), this.response)
.build();
// @formatter:on
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
assertThat(authorizedClient).isNotNull();
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
.forClass(TokenExchangeGrantRequest.class);
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
assertThat(grantRequest.getClientRegistration().getRegistrationId())
.isEqualTo(clientRegistration.getRegistrationId());
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
}
private static OAuth2AccessToken getExpiredAccessToken() {
Instant expiresAt = Instant.now().minusSeconds(60);
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
@@ -353,37 +387,32 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
@Bean
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
return new MockAuthorizationCodeClient();
return new MockAccessTokenResponseClient<>();
}
@Bean
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient() {
return new MockRefreshTokenClient();
return new MockAccessTokenResponseClient<>();
}
@Bean
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
return new MockClientCredentialsClient();
return new MockAccessTokenResponseClient<>();
}
@Bean
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient() {
return new MockPasswordClient();
return new MockAccessTokenResponseClient<>();
}
@Bean
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerTokenResponseClient() {
return new MockJwtBearerClient();
return new MockAccessTokenResponseClient<>();
}
@Bean
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
return mock(DefaultOAuth2UserService.class);
}
@Bean
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
return mock(OidcUserService.class);
OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
}
@@ -400,28 +429,35 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
@Bean
RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider() {
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockRefreshTokenClient());
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockClientCredentialsClient());
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
PasswordOAuth2AuthorizedClientProvider passwordProvider() {
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockPasswordClient());
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockJwtBearerClient());
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider() {
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@@ -429,21 +465,10 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
abstract static class OAuth2ClientBaseConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.oauth2Client(Customizer.withDefaults());
return http.build();
// @formatter:on
}
@Bean
ClientRegistrationRepository clientRegistrationRepository() {
// @formatter:off
return new InMemoryClientRegistrationRepository(Arrays.asList(
return new InMemoryClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
@@ -463,7 +488,15 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
.clientId("okta-client-id")
.clientSecret("okta-client-secret")
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
.build()));
.build(),
ClientRegistration.withRegistrationId("auth0")
.clientName("Auth0")
.clientId("auth0-client-id")
.clientSecret("auth0-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.scope("user.read", "user.write")
.build());
// @formatter:on
}
@@ -494,51 +527,11 @@ public class OAuth2AuthorizedClientManagerConfigurationTests {
}
private static class MockAuthorizationCodeClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
implements OAuth2AccessTokenResponseClient<T> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
}
private static class MockRefreshTokenClient
implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
}
private static class MockClientCredentialsClient
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
}
private static class MockPasswordClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
}
private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) {
public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
@@ -40,8 +40,10 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@@ -543,6 +545,17 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.mvc.perform(request).andExpect(status().isOk());
request = get("/user/deny");
this.mvc.perform(request).andExpect(status().isUnauthorized());
UserDetails user = TestAuthentication.withUsername("taehong").build();
Authentication authentication = TestAuthentication.authenticated(user);
request = get("/v2/user/{username}", user.getUsername()).with(authentication(authentication));
this.mvc.perform(request).andExpect(status().isOk());
request = get("/v2/user/{username}", "withNoAuthentication");
this.mvc.perform(request).andExpect(status().isUnauthorized());
request = get("/v2/user/{username}", "another").with(authentication(authentication));
this.mvc.perform(request).andExpect(status().isForbidden());
}
private static RequestPostProcessor remoteAddress(String remoteAddress) {
@@ -596,6 +609,20 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
}
@Test
public void getWhenNotConfigAndAuthenticatedThenRespondsWithForbidden() throws Exception {
this.spring.register(NotConfig.class, BasicController.class).autowire();
MockHttpServletRequestBuilder requestWithUser = get("/").with(user("user"));
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
}
@Test
public void getWhenNotConfigAndNotAuthenticatedThenRespondsWithOk() throws Exception {
this.spring.register(NotConfig.class, BasicController.class).autowire();
MockHttpServletRequestBuilder requestWithUser = get("/");
this.mvc.perform(requestWithUser).andExpect(status().isOk());
}
@Configuration
@EnableWebSecurity
static class GrantedAuthorityDefaultHasRoleConfig {
@@ -1053,6 +1080,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/user/{username}").access(new WebExpressionAuthorizationManager("#username == 'user'"))
.requestMatchers("/v2/user/{username}").hasVariable("username").equalTo(Authentication::getName)
);
// @formatter:on
return http.build();
@@ -1066,6 +1094,11 @@ public class AuthorizeHttpRequestsConfigurerTests {
return username;
}
@RequestMapping("/v2/user/{username}")
String pathV2(@PathVariable("username") String username) {
return username;
}
}
}
@@ -1136,6 +1169,24 @@ public class AuthorizeHttpRequestsConfigurerTests {
}
@Configuration
@EnableWebSecurity
static class NotConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic(withDefaults())
.authorizeHttpRequests((requests) -> requests
.anyRequest().not().authenticated()
);
// @formatter:on
return http.build();
}
}
@Configuration
static class AuthorizationEventPublisherConfig {
@@ -0,0 +1,589 @@
/*
* 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.
* 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.annotation.web.reactive;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.JwtBearerReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.TokenExchangeReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
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.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
/**
* Tests for
* {@link ReactiveOAuth2ClientConfiguration.ReactiveOAuth2AuthorizedClientManagerConfiguration}.
*
* @author Steve Riesenberg
*/
public class ReactiveOAuth2AuthorizedClientManagerConfigurationTests {
private static ReactiveOAuth2AccessTokenResponseClient<? super AbstractOAuth2AuthorizationGrantRequest> MOCK_RESPONSE_CLIENT;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
@Autowired
private ReactiveClientRegistrationRepository clientRegistrationRepository;
@Autowired(required = false)
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
@Autowired(required = false)
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
@Autowired(required = false)
private AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider;
private MockServerWebExchange exchange;
@BeforeEach
@SuppressWarnings("unchecked")
public void setUp() {
MOCK_RESPONSE_CLIENT = mock(ReactiveOAuth2AccessTokenResponseClient.class);
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
this.exchange = MockServerWebExchange.builder(request).build();
}
@Test
public void loadContextWhenOAuth2ClientEnabledThenConfigured() {
this.spring.register(MinimalOAuth2ClientConfig.class).autowire();
assertThat(this.authorizedClientManager).isNotNull();
}
@Test
public void authorizeWhenAuthorizationCodeAuthorizedClientProviderBeanThenUsed() {
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("google")
.principal(authentication)
.attribute(ServerWebExchange.class.getName(), this.exchange)
.build();
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
.extracting(OAuth2AuthorizationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo("client_authorization_required");
// @formatter:on
verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class));
}
@Test
public void authorizeWhenAuthorizedClientServiceBeanThenUsed() {
this.spring.register(CustomAuthorizedClientServiceConfig.class).autowire();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("google")
.principal(authentication)
.attribute(ServerWebExchange.class.getName(), this.exchange)
.build();
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
.extracting(OAuth2AuthorizationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo("client_authorization_required");
// @formatter:on
verify(this.authorizedClientService).loadAuthorizedClient(authorizeRequest.getClientRegistrationId(),
authentication.getName());
}
@Test
public void authorizeWhenRefreshTokenAccessTokenResponseClientBeanThenUsed() {
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
testRefreshTokenGrant();
}
@Test
public void authorizeWhenRefreshTokenAuthorizedClientProviderBeanThenUsed() {
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
testRefreshTokenGrant();
}
private void testRefreshTokenGrant() {
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
.willReturn(Mono.just(accessTokenResponse));
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google")
.block();
assertThat(clientRegistration).isNotNull();
OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken());
this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.exchange)
.block();
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(authentication)
.attribute(ServerWebExchange.class.getName(), this.exchange)
.build();
// @formatter:on
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
assertThat(authorizedClient).isNotNull();
ArgumentCaptor<OAuth2RefreshTokenGrantRequest> grantRequestCaptor = ArgumentCaptor
.forClass(OAuth2RefreshTokenGrantRequest.class);
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
OAuth2RefreshTokenGrantRequest grantRequest = grantRequestCaptor.getValue();
assertThat(grantRequest.getClientRegistration().getRegistrationId())
.isEqualTo(clientRegistration.getRegistrationId());
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN);
assertThat(grantRequest.getAccessToken()).isEqualTo(existingAuthorizedClient.getAccessToken());
assertThat(grantRequest.getRefreshToken()).isEqualTo(existingAuthorizedClient.getRefreshToken());
}
@Test
public void authorizeWhenClientCredentialsAccessTokenResponseClientBeanThenUsed() {
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
testClientCredentialsGrant();
}
@Test
public void authorizeWhenClientCredentialsAuthorizedClientProviderBeanThenUsed() {
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
testClientCredentialsGrant();
}
private void testClientCredentialsGrant() {
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2ClientCredentialsGrantRequest.class)))
.willReturn(Mono.just(accessTokenResponse));
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github")
.block();
assertThat(clientRegistration).isNotNull();
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(authentication)
.attribute(ServerWebExchange.class.getName(), this.exchange)
.build();
// @formatter:on
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
assertThat(authorizedClient).isNotNull();
ArgumentCaptor<OAuth2ClientCredentialsGrantRequest> grantRequestCaptor = ArgumentCaptor
.forClass(OAuth2ClientCredentialsGrantRequest.class);
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
OAuth2ClientCredentialsGrantRequest grantRequest = grantRequestCaptor.getValue();
assertThat(grantRequest.getClientRegistration().getRegistrationId())
.isEqualTo(clientRegistration.getRegistrationId());
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS);
}
@Test
public void authorizeWhenPasswordAccessTokenResponseClientBeanThenUsed() {
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
testPasswordGrant();
}
@Test
public void authorizeWhenPasswordAuthorizedClientProviderBeanThenUsed() {
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
testPasswordGrant();
}
private void testPasswordGrant() {
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2PasswordGrantRequest.class)))
.willReturn(Mono.just(accessTokenResponse));
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook")
.block();
assertThat(clientRegistration).isNotNull();
MockServerHttpRequest request = MockServerHttpRequest.post("/")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body("username=user&password=password");
this.exchange = MockServerWebExchange.builder(request).build();
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(authentication)
.attribute(ServerWebExchange.class.getName(), this.exchange)
.build();
// @formatter:on
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
assertThat(authorizedClient).isNotNull();
ArgumentCaptor<OAuth2PasswordGrantRequest> grantRequestCaptor = ArgumentCaptor
.forClass(OAuth2PasswordGrantRequest.class);
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
OAuth2PasswordGrantRequest grantRequest = grantRequestCaptor.getValue();
assertThat(grantRequest.getClientRegistration().getRegistrationId())
.isEqualTo(clientRegistration.getRegistrationId());
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD);
assertThat(grantRequest.getUsername()).isEqualTo("user");
assertThat(grantRequest.getPassword()).isEqualTo("password");
}
@Test
public void authorizeWhenJwtBearerAccessTokenResponseClientBeanThenUsed() {
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
testJwtBearerGrant();
}
@Test
public void authorizeWhenJwtBearerAuthorizedClientProviderBeanThenUsed() {
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
testJwtBearerGrant();
}
private void testJwtBearerGrant() {
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(JwtBearerGrantRequest.class)))
.willReturn(Mono.just(accessTokenResponse));
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta").block();
assertThat(clientRegistration).isNotNull();
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(authentication)
.attribute(ServerWebExchange.class.getName(), this.exchange)
.build();
// @formatter:on
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
assertThat(authorizedClient).isNotNull();
ArgumentCaptor<JwtBearerGrantRequest> grantRequestCaptor = ArgumentCaptor.forClass(JwtBearerGrantRequest.class);
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
JwtBearerGrantRequest grantRequest = grantRequestCaptor.getValue();
assertThat(grantRequest.getClientRegistration().getRegistrationId())
.isEqualTo(clientRegistration.getRegistrationId());
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER);
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
}
@Test
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire();
testTokenExchangeGrant();
}
@Test
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire();
testTokenExchangeGrant();
}
private void testTokenExchangeGrant() {
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
.willReturn(Mono.just(accessTokenResponse));
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0").block();
assertThat(clientRegistration).isNotNull();
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(authentication)
.attribute(ServerWebExchange.class.getName(), this.exchange)
.build();
// @formatter:on
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
assertThat(authorizedClient).isNotNull();
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
.forClass(TokenExchangeGrantRequest.class);
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
assertThat(grantRequest.getClientRegistration().getRegistrationId())
.isEqualTo(clientRegistration.getRegistrationId());
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
}
private static OAuth2AccessToken getExpiredAccessToken() {
Instant expiresAt = Instant.now().minusSeconds(60);
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "scopes", issuedAt, expiresAt,
new HashSet<>(Arrays.asList("read", "write")));
}
private static Jwt getJwt() {
Instant issuedAt = Instant.now();
return new Jwt("token", issuedAt, issuedAt.plusSeconds(300),
Collections.singletonMap(JoseHeaderNames.ALG, "RS256"),
Collections.singletonMap(JwtClaimNames.SUB, "user"));
}
@Configuration
@EnableWebFluxSecurity
static class MinimalOAuth2ClientConfig extends OAuth2ClientBaseConfig {
@Bean
ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
}
@Configuration
@EnableWebFluxSecurity
static class CustomAuthorizedClientServiceConfig extends OAuth2ClientBaseConfig {
@Bean
ReactiveOAuth2AuthorizedClientService authorizedClientService() {
ReactiveOAuth2AuthorizedClientService authorizedClientService = mock(
ReactiveOAuth2AuthorizedClientService.class);
given(authorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(Mono.empty());
return authorizedClientService;
}
}
@Configuration
@EnableWebFluxSecurity
static class CustomAccessTokenResponseClientsConfig extends MinimalOAuth2ClientConfig {
@Bean
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
@Bean
ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenAccessResponseClient() {
return new MockAccessTokenResponseClient<>();
}
@Bean
ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
@Bean
ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
@Bean
ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
@Bean
ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
}
@Configuration
@EnableWebFluxSecurity
static class CustomAuthorizedClientProvidersConfig extends MinimalOAuth2ClientConfig {
@Bean
AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCode() {
return spy(new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider());
}
@Bean
RefreshTokenReactiveOAuth2AuthorizedClientProvider refreshToken() {
RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
ClientCredentialsReactiveOAuth2AuthorizedClientProvider clientCredentials() {
ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
PasswordReactiveOAuth2AuthorizedClientProvider password() {
PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearer() {
JwtBearerReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
@Bean
TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchange() {
TokenExchangeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>());
return authorizedClientProvider;
}
}
abstract static class OAuth2ClientBaseConfig {
@Bean
ReactiveClientRegistrationRepository clientRegistrationRepository() {
// @formatter:off
return new InMemoryReactiveClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.build(),
CommonOAuth2Provider.GITHUB.getBuilder("github")
.clientId("github-client-id")
.clientSecret("github-client-secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build(),
CommonOAuth2Provider.FACEBOOK.getBuilder("facebook")
.clientId("facebook-client-id")
.clientSecret("facebook-client-secret")
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.build(),
CommonOAuth2Provider.OKTA.getBuilder("okta")
.clientId("okta-client-id")
.clientSecret("okta-client-secret")
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
.build(),
ClientRegistration.withRegistrationId("auth0")
.clientName("Auth0")
.clientId("auth0-client-id")
.clientSecret("auth0-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.scope("user.read", "user.write")
.build());
// @formatter:on
}
@Bean
Consumer<DefaultReactiveOAuth2AuthorizedClientManager> authorizedClientManagerConsumer() {
return (authorizedClientManager) -> authorizedClientManager
.setContextAttributesMapper((authorizeRequest) -> {
ServerWebExchange exchange = Objects
.requireNonNull(authorizeRequest.getAttribute(ServerWebExchange.class.getName()));
return exchange.getFormData().map((parameters) -> {
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
Map<String, Object> attributes = Collections.emptyMap();
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
attributes = new HashMap<>();
attributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
attributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return attributes;
});
});
}
}
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
implements ReactiveOAuth2AccessTokenResponseClient<T> {
@Override
public Mono<OAuth2AccessTokenResponse> getTokenResponse(T grantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(grantRequest);
}
}
}
@@ -16,16 +16,39 @@
package org.springframework.security.config.annotation.web.reactive;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.password.CompromisedPasswordDecision;
import org.springframework.security.authentication.password.CompromisedPasswordException;
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
/**
* Tests for {@link ServerHttpSecurityConfiguration}.
@@ -37,6 +60,16 @@ public class ServerHttpSecurityConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
WebTestClient webClient;
@Autowired
void setup(ApplicationContext context) {
if (!context.containsBean(WebHttpHandlerBuilder.WEB_HANDLER_BEAN_NAME)) {
return;
}
this.webClient = WebTestClient.bindToApplicationContext(context).configureClient().build();
}
@Test
public void loadConfigWhenReactiveUserDetailsServiceConfiguredThenServerHttpSecurityExists() {
this.spring
@@ -57,9 +90,151 @@ public class ServerHttpSecurityConfigurationTests {
assertThat(serverHttpSecurity).isNotNull();
}
@Test
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordCompromisedThenUnauthorized() {
this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
.autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
// @formatter:off
this.webClient.mutateWith(csrf())
.post()
.uri("/login")
.body(BodyInserters.fromFormData(data))
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().location("/login?error");
// @formatter:on
}
@Test
void loginWhenCompromisePasswordCheckerConfiguredAndPasswordNotCompromisedThenUnauthorized() {
this.spring.register(FormLoginConfig.class, UserDetailsConfig.class, CompromisedPasswordCheckerConfig.class)
.autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "admin");
data.add("password", "password2");
// @formatter:off
this.webClient.mutateWith(csrf())
.post()
.uri("/login")
.body(BodyInserters.fromFormData(data))
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().location("/");
// @formatter:on
}
@Test
void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToResetPassword() {
this.spring
.register(FormLoginRedirectToResetPasswordConfig.class, UserDetailsConfig.class,
CompromisedPasswordCheckerConfig.class)
.autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
// @formatter:off
this.webClient.mutateWith(csrf())
.post()
.uri("/login")
.body(BodyInserters.fromFormData(data))
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().location("/reset-password");
// @formatter:on
}
@Configuration
static class SubclassConfig extends ServerHttpSecurityConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableWebFlux
@EnableWebFluxSecurity
static class FormLoginConfig {
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchange) -> exchange
.anyExchange().authenticated()
)
.formLogin(Customizer.withDefaults());
// @formatter:on
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebFlux
@EnableWebFluxSecurity
static class FormLoginRedirectToResetPasswordConfig {
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchange) -> exchange
.anyExchange().authenticated()
)
.formLogin((form) -> form
.authenticationFailureHandler((webFilterExchange, exception) -> {
String redirectUrl = "/login?error";
if (exception instanceof CompromisedPasswordException) {
redirectUrl = "/reset-password";
}
return new DefaultServerRedirectStrategy().sendRedirect(webFilterExchange.getExchange(), URI.create(redirectUrl));
})
);
// @formatter:on
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
static class UserDetailsConfig {
@Bean
MapReactiveUserDetailsService userDetailsService() {
// @formatter:off
UserDetails user = PasswordEncodedUser.user();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password2")
.roles("USER", "ADMIN")
.build();
// @formatter:on
return new MapReactiveUserDetailsService(user, admin);
}
}
@Configuration(proxyBeanMethods = false)
static class CompromisedPasswordCheckerConfig {
@Bean
TestReactivePasswordChecker compromisedPasswordChecker() {
return new TestReactivePasswordChecker();
}
}
static class TestReactivePasswordChecker implements ReactiveCompromisedPasswordChecker {
@Override
public Mono<CompromisedPasswordDecision> check(String password) {
if ("password".equals(password)) {
return Mono.just(new CompromisedPasswordDecision(true));
}
return Mono.just(new CompromisedPasswordDecision(false));
}
}
}
@@ -65,7 +65,7 @@ public class XsdDocumentedTests {
String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd";
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.2.xsd";
String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.3.xsd";
XmlSupport xml = new XmlSupport();
@@ -151,8 +151,8 @@ public class XsdDocumentedTests {
.list((dir, name) -> name.endsWith(".xsd"));
// @formatter:on
assertThat(schemas.length)
.withFailMessage("the count is equal to 24, if not then schemaDocument needs updating")
.isEqualTo(24);
.withFailMessage("the count is equal to 25, if not then schemaDocument needs updating")
.isEqualTo(25);
}
/**
@@ -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.
@@ -49,18 +49,20 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.TokenExchangeOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -316,6 +318,47 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user");
}
@Test
public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() {
this.spring.configLocations(xml("clients")).autowire();
testTokenExchangeGrant();
}
@Test
public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() {
this.spring.configLocations(xml("providers")).autowire();
testTokenExchangeGrant();
}
private void testTokenExchangeGrant() {
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build();
given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class)))
.willReturn(accessTokenResponse);
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0");
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(authentication)
.attribute(HttpServletRequest.class.getName(), this.request)
.attribute(HttpServletResponse.class.getName(), this.response)
.build();
// @formatter:on
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
assertThat(authorizedClient).isNotNull();
ArgumentCaptor<TokenExchangeGrantRequest> grantRequestCaptor = ArgumentCaptor
.forClass(TokenExchangeGrantRequest.class);
verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture());
TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue();
assertThat(grantRequest.getClientRegistration().getRegistrationId())
.isEqualTo(clientRegistration.getRegistrationId());
assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken());
}
private static OAuth2AccessToken getExpiredAccessToken() {
Instant expiresAt = Instant.now().minusSeconds(60);
Instant issuedAt = expiresAt.minus(Duration.ofDays(1));
@@ -356,6 +399,14 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
.clientId("okta-client-id")
.clientSecret("okta-client-secret")
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
.build(),
ClientRegistration.withRegistrationId("auth0")
.clientName("Auth0")
.clientId("auth0-client-id")
.clientSecret("auth0-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.scope("user.read", "user.write")
.build());
// @formatter:on
}
@@ -378,95 +429,65 @@ public class OAuth2AuthorizedClientManagerRegistrarTests {
});
}
public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider() {
public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCode() {
return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider());
}
public static RefreshTokenOAuth2AuthorizedClientProvider refreshTokenAuthorizedClientProvider() {
public static RefreshTokenOAuth2AuthorizedClientProvider refreshToken() {
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(refreshTokenAccessTokenResponseClient());
return authorizedClientProvider;
}
public static MockRefreshTokenClient refreshTokenAccessTokenResponseClient() {
return new MockRefreshTokenClient();
public static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsAuthorizedClientProvider() {
public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentials() {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(clientCredentialsAccessTokenResponseClient());
return authorizedClientProvider;
}
public static OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
return new MockClientCredentialsClient();
return new MockAccessTokenResponseClient<>();
}
public static PasswordOAuth2AuthorizedClientProvider passwordAuthorizedClientProvider() {
public static PasswordOAuth2AuthorizedClientProvider password() {
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(passwordAccessTokenResponseClient());
return authorizedClientProvider;
}
public static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
return new MockPasswordClient();
return new MockAccessTokenResponseClient<>();
}
public static JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
public static JwtBearerOAuth2AuthorizedClientProvider jwtBearer() {
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient());
return authorizedClientProvider;
}
public static OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
return new MockJwtBearerClient();
return new MockAccessTokenResponseClient<>();
}
private static class MockAuthorizationCodeClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
public static TokenExchangeOAuth2AuthorizedClientProvider tokenExchange() {
TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient());
return authorizedClientProvider;
}
private static class MockRefreshTokenClient
implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
public static OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
return new MockAccessTokenResponseClient<>();
}
private static class MockClientCredentialsClient
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
private static class MockAccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
implements OAuth2AccessTokenResponseClient<T> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
}
private static class MockPasswordClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
}
private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) {
public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
@@ -16,6 +16,7 @@
package org.springframework.security.config.saml2;
import jakarta.servlet.http.HttpServletRequest;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
@@ -23,16 +24,21 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link RelyingPartyRegistrationsBeanDefinitionParser}.
@@ -118,6 +124,7 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
// @formatter:on
@Autowired
@Qualifier("registrations")
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
public final SpringTestContext spring = new SpringTestContext(this);
@@ -268,6 +275,19 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384");
}
@Test
public void parseWhenRelayStateResolverThenUses() {
this.spring.configLocations(xml("RelayStateResolver")).autowire();
Converter<HttpServletRequest, String> relayStateResolver = this.spring.getContext().getBean(Converter.class);
OpenSaml4AuthenticationRequestResolver authenticationRequestResolver = this.spring.getContext()
.getBean(OpenSaml4AuthenticationRequestResolver.class);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/saml2/authenticate/one");
request.setServletPath("/saml2/authenticate/one");
authenticationRequestResolver.resolve(request);
verify(relayStateResolver).convert(request);
}
private static MockResponse xmlResponse(String xml) {
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).setBody(xml);
}
@@ -56,9 +56,11 @@ import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
@@ -592,6 +594,7 @@ public class ServerHttpSecurityTests {
}
@Test
@SuppressWarnings("unchecked")
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
@@ -613,8 +616,11 @@ public class ServerHttpSecurityTests {
OAuth2LoginAuthenticationWebFilter authenticationWebFilter = getWebFilter(securityFilterChain,
OAuth2LoginAuthenticationWebFilter.class)
.get();
Object handler = ReflectionTestUtils.getField(authenticationWebFilter, "authenticationSuccessHandler");
assertThat(ReflectionTestUtils.getField(handler, "requestCache")).isSameAs(requestCache);
DelegatingServerAuthenticationSuccessHandler handler = (DelegatingServerAuthenticationSuccessHandler) ReflectionTestUtils
.getField(authenticationWebFilter, "authenticationSuccessHandler");
List<ServerAuthenticationSuccessHandler> delegates = (List<ServerAuthenticationSuccessHandler>) ReflectionTestUtils
.getField(handler, "delegates");
assertThat(ReflectionTestUtils.getField(delegates.get(0), "requestCache")).isSameAs(requestCache);
}
@Test
@@ -0,0 +1,587 @@
/*
* 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.
* 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.web.server;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.SessionLimit;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.DefaultWebSessionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
public class SessionManagementSpecTests {
public final SpringTestContext spring = new SpringTestContext(this);
WebTestClient client;
@Autowired
public void setApplicationContext(ApplicationContext context) {
this.client = WebTestClient.bindToApplicationContext(context).build();
}
@Test
void loginWhenMaxSessionPreventsLoginThenSecondLoginFails() {
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
// second login should fail
ResponseCookie secondLoginSessionCookie = this.client.mutateWith(csrf())
.post()
.uri("/login")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromFormData(data))
.exchange()
.expectHeader()
.location("/login?error")
.returnResult(Void.class)
.getResponseCookies()
.getFirst("SESSION");
assertThat(secondLoginSessionCookie).isNull();
// first login should still be valid
this.client.mutateWith(csrf())
.get()
.uri("/")
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
}
@Test
void httpBasicWhenUsingSavingAuthenticationInWebSessionAndPreventLoginThenSecondRequestFails() {
this.spring.register(ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig.class).autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
// first request be successful
ResponseCookie sessionCookie = this.client.get()
.uri("/")
.headers((headers) -> headers.setBasicAuth("user", "password"))
.exchange()
.expectStatus()
.isOk()
.expectCookie()
.exists("SESSION")
.returnResult(Void.class)
.getResponseCookies()
.getFirst("SESSION");
// request with no session should fail
this.client.get()
.uri("/")
.headers((headers) -> headers.setBasicAuth("user", "password"))
.exchange()
.expectStatus()
.isUnauthorized();
// request with session obtained from first request should be successful
this.client.get()
.uri("/")
.headers((headers) -> headers.setBasicAuth("user", "password"))
.cookie(sessionCookie.getName(), sessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
}
@Test
void loginWhenMaxSessionPerAuthenticationThenUserLoginFailsAndAdminLoginSucceeds() {
ConcurrentSessionsMaxSessionPreventsLoginConfig.sessionLimit = (authentication) -> {
if (authentication.getName().equals("admin")) {
return Mono.empty();
}
return Mono.just(1);
};
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginConfig.class).autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
MultiValueMap<String, String> adminCreds = new LinkedMultiValueMap<>();
adminCreds.add("username", "admin");
adminCreds.add("password", "password");
ResponseCookie userFirstLoginSessionCookie = loginReturningCookie(data);
ResponseCookie adminFirstLoginSessionCookie = loginReturningCookie(adminCreds);
// second user login should fail
this.client.mutateWith(csrf())
.post()
.uri("/login")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromFormData(data))
.exchange()
.expectHeader()
.location("/login?error");
// first login should still be valid
this.client.mutateWith(csrf())
.get()
.uri("/")
.cookie(userFirstLoginSessionCookie.getName(), userFirstLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
ResponseCookie adminSecondLoginSessionCookie = loginReturningCookie(adminCreds);
this.client.mutateWith(csrf())
.get()
.uri("/")
.cookie(adminFirstLoginSessionCookie.getName(), adminFirstLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
this.client.mutateWith(csrf())
.get()
.uri("/")
.cookie(adminSecondLoginSessionCookie.getName(), adminSecondLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
}
@Test
void loginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(1);
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
// first login should not be valid
this.client.get()
.uri("/")
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isFound()
.expectHeader()
.location("/login");
// second login should be valid
this.client.get()
.uri("/")
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
}
@Test
void loginWhenMaxSessionDoesNotPreventLoginThenLeastRecentlyUsedSessionIsInvalidated() {
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.sessionLimit = SessionLimit.of(2);
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.class).autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
ResponseCookie firstLoginSessionCookie = loginReturningCookie(data);
ResponseCookie secondLoginSessionCookie = loginReturningCookie(data);
// update last access time for first request
this.client.get()
.uri("/")
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
ResponseCookie thirdLoginSessionCookie = loginReturningCookie(data);
// second login should be invalid, it is the least recently used session
this.client.get()
.uri("/")
.cookie(secondLoginSessionCookie.getName(), secondLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isFound()
.expectHeader()
.location("/login");
// first login should be valid
this.client.get()
.uri("/")
.cookie(firstLoginSessionCookie.getName(), firstLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
// third login should be valid
this.client.get()
.uri("/")
.cookie(thirdLoginSessionCookie.getName(), thirdLoginSessionCookie.getValue())
.exchange()
.expectStatus()
.isOk();
}
@Test
void oauth2LoginWhenMaxSessionsThenPreventLogin() {
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
OAuth2LoginConcurrentSessionsConfig.preventLogin = true;
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
prepareOAuth2Config();
// @formatter:off
ResponseCookie sessionCookie = this.client.get()
.uri("/login/oauth2/code/client-credentials")
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().valueEquals("Location", "/")
.expectCookie().exists("SESSION")
.returnResult(Void.class)
.getResponseCookies()
.getFirst("SESSION");
this.client.get()
.uri("/login/oauth2/code/client-credentials")
.exchange()
.expectHeader().location("/login?error");
this.client.get().uri("/")
.cookie(sessionCookie.getName(), sessionCookie.getValue())
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("ok");
// @formatter:on
}
@Test
void oauth2LoginWhenMaxSessionDoesNotPreventLoginThenSecondLoginSucceedsAndFirstSessionIsInvalidated() {
OAuth2LoginConcurrentSessionsConfig.maxSessions = 1;
OAuth2LoginConcurrentSessionsConfig.preventLogin = false;
this.spring.register(OAuth2LoginConcurrentSessionsConfig.class).autowire();
prepareOAuth2Config();
// @formatter:off
ResponseCookie firstLoginCookie = this.client.get()
.uri("/login/oauth2/code/client-credentials")
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().valueEquals("Location", "/")
.expectCookie().exists("SESSION")
.returnResult(Void.class)
.getResponseCookies()
.getFirst("SESSION");
ResponseCookie secondLoginCookie = this.client.get()
.uri("/login/oauth2/code/client-credentials")
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().valueEquals("Location", "/")
.expectCookie().exists("SESSION")
.returnResult(Void.class)
.getResponseCookies()
.getFirst("SESSION");
this.client.get().uri("/")
.cookie(firstLoginCookie.getName(), firstLoginCookie.getValue())
.exchange()
.expectStatus().isFound()
.expectHeader().location("/login");
this.client.get().uri("/")
.cookie(secondLoginCookie.getName(), secondLoginCookie.getValue())
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("ok");
// @formatter:on
}
@Test
void loginWhenAuthenticationSuccessHandlerOverriddenThenConcurrentSessionHandlersBackOff() {
this.spring.register(ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig.class).autowire();
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
// first login should be successful
login(data).expectStatus().isFound().expectHeader().location("/");
// second login should be successful, there should be no concurrent session
// control
login(data).expectStatus().isFound().expectHeader().location("/");
}
private void prepareOAuth2Config() {
OAuth2LoginConcurrentSessionsConfig config = this.spring.getContext()
.getBean(OAuth2LoginConcurrentSessionsConfig.class);
ServerAuthenticationConverter converter = config.authenticationConverter;
ReactiveAuthenticationManager manager = config.manager;
ServerOAuth2AuthorizationRequestResolver resolver = config.resolver;
OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
OAuth2User user = TestOAuth2Users.create();
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes();
OAuth2LoginAuthenticationToken result = new OAuth2LoginAuthenticationToken(
TestClientRegistrations.clientRegistration().build(), exchange, user, user.getAuthorities(),
accessToken);
given(converter.convert(any())).willReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c")));
given(manager.authenticate(any())).willReturn(Mono.just(result));
given(resolver.resolve(any())).willReturn(Mono.empty());
}
private ResponseCookie loginReturningCookie(MultiValueMap<String, String> data) {
return login(data).expectCookie()
.exists("SESSION")
.returnResult(Void.class)
.getResponseCookies()
.getFirst("SESSION");
}
private WebTestClient.ResponseSpec login(MultiValueMap<String, String> data) {
return this.client.mutateWith(csrf())
.post()
.uri("/login")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromFormData(data))
.exchange();
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
@Import(Config.class)
static class ConcurrentSessionsMaxSessionPreventsLoginConfig {
static SessionLimit sessionLimit = SessionLimit.of(1);
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
.formLogin(Customizer.withDefaults())
.sessionManagement((sessionManagement) -> sessionManagement
.concurrentSessions((concurrentSessions) -> concurrentSessions
.maximumSessions(sessionLimit)
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
@Import(Config.class)
static class OAuth2LoginConcurrentSessionsConfig {
static int maxSessions = 1;
static boolean preventLogin = true;
ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class);
ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class);
@Bean
SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http,
DefaultWebSessionManager webSessionManager) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges
.anyExchange().authenticated()
)
.oauth2Login((oauth2Login) -> oauth2Login
.authenticationConverter(this.authenticationConverter)
.authenticationManager(this.manager)
.authorizationRequestResolver(this.resolver)
)
.sessionManagement((sessionManagement) -> sessionManagement
.concurrentSessions((concurrentSessions) -> concurrentSessions
.maximumSessions(SessionLimit.of(maxSessions))
.maximumSessionsExceededHandler(preventLogin
? new PreventLoginServerMaximumSessionsExceededHandler()
: new InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.getSessionStore()))
)
);
// @formatter:on
return http.build();
}
@Bean
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryReactiveClientRegistrationRepository(
TestClientRegistrations.clientCredentials().build());
}
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
@Import(Config.class)
static class ConcurrentSessionsMaxSessionPreventsLoginFalseConfig {
static SessionLimit sessionLimit = SessionLimit.of(1);
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
.formLogin(Customizer.withDefaults())
.sessionManagement((sessionManagement) -> sessionManagement
.concurrentSessions((concurrentSessions) -> concurrentSessions
.maximumSessions(sessionLimit)
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
@Import(Config.class)
static class ConcurrentSessionsFormLoginOverrideAuthenticationSuccessHandlerConfig {
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
.formLogin((login) -> login
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
)
.sessionManagement((sessionManagement) -> sessionManagement
.concurrentSessions((concurrentSessions) -> concurrentSessions
.maximumSessions(SessionLimit.of(1))
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
@Import(Config.class)
static class ConcurrentSessionsHttpBasicWithWebSessionMaxSessionPreventsLoginConfig {
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
.httpBasic((basic) -> basic
.securityContextRepository(new WebSessionServerSecurityContextRepository())
)
.sessionManagement((sessionManagement) -> sessionManagement
.concurrentSessions((concurrentSessions) -> concurrentSessions
.maximumSessions(SessionLimit.of(1))
.maximumSessionsExceededHandler(new PreventLoginServerMaximumSessionsExceededHandler())
)
);
// @formatter:on
return http.build();
}
}
@Configuration
@Import({ ReactiveAuthenticationTestConfiguration.class, DefaultController.class })
static class Config {
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
DefaultWebSessionManager webSessionManager() {
return new DefaultWebSessionManager();
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry() {
return new InMemoryReactiveSessionRegistry();
}
}
@RestController
static class DefaultController {
@GetMapping("/")
String index() {
return "ok";
}
}
}
@@ -816,4 +816,41 @@ class AuthorizeHttpRequestsDslTests {
}
}
@Test
fun `request when ip address does not match then responds with forbidden`() {
this.spring.register(HasIpAddressConfig::class.java).autowire()
this.mockMvc.perform(get("/path")
.with { request ->
request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error")
request.apply {
dispatcherType = DispatcherType.ERROR
}
})
.andExpect(status().isForbidden)
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
open class HasIpAddressConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, hasIpAddress("10.0.0.0/24"))
}
}
return http.build()
}
@RestController
internal class PathController {
@RequestMapping("/path")
fun path() {
}
}
}
}
@@ -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.
@@ -33,6 +33,7 @@ import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.core.userdetails.User
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
@@ -367,6 +368,50 @@ class FormLoginDslTests {
verify(exactly = 1) { CustomAuthenticationDetailsSourceConfig.AUTHENTICATION_DETAILS_SOURCE.buildDetails(any()) }
}
@Configuration
@EnableWebSecurity
open class CustomUsernameParameterConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
formLogin {
usernameParameter = "custom-username"
}
}
return http.build()
}
}
@Test
fun `form login when custom username parameter then used`() {
this.spring.register(CustomUsernameParameterConfig::class.java, UserConfig::class.java).autowire()
this.mockMvc.perform(formLogin().userParameter("custom-username"))
.andExpect(authenticated())
}
@Configuration
@EnableWebSecurity
open class CustomPasswordParameterConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
formLogin {
passwordParameter = "custom-password"
}
}
return http.build()
}
}
@Test
fun `form login when custom password parameter then used`() {
this.spring.register(CustomPasswordParameterConfig::class.java, UserConfig::class.java).autowire()
this.mockMvc.perform(formLogin().passwordParam("custom-password"))
.andExpect(authenticated())
}
@Configuration
@EnableWebSecurity
open class CustomAuthenticationDetailsSourceConfig {
@@ -0,0 +1,126 @@
/*
* 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.
* 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.annotation.web
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.BeanCreationException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.TestAuthentication
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.web.SecurityFilterChain
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import java.util.*
/**
* Tests for [Saml2LogoutDsl]
*
* @author Josh Cummings
*/
@ExtendWith(SpringTestContextExtension::class)
class Saml2LogoutDslTests {
@JvmField
val spring = SpringTestContext(this)
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `saml2Logout when no relying party registration repository then exception`() {
Assertions.assertThatThrownBy { this.spring.register(Saml2LogoutNoRelyingPartyRegistrationRepoConfig::class.java).autowire() }
.isInstanceOf(BeanCreationException::class.java)
.hasMessageContaining("relyingPartyRegistrationRepository cannot be null")
}
@Test
@Throws(Exception::class)
fun `saml2Logout when defaults and not saml login then default logout`() {
this.spring.register(Saml2LogoutDefaultsConfig::class.java).autowire()
val user = TestAuthentication.authenticatedUser()
val result: MvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.post("/logout").with(SecurityMockMvcRequestPostProcessors.authentication(user))
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(MockMvcResultMatchers.status().isFound())
.andReturn()
val location = result.response.getHeader("Location")
Assertions.assertThat(location).isEqualTo("/login?logout")
}
@Test
@Throws(Exception::class)
fun saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() {
this.spring.register(Saml2LogoutDefaultsConfig::class.java).autowire()
val principal = DefaultSaml2AuthenticatedPrincipal("user", emptyMap())
principal.relyingPartyRegistrationId = "registration-id"
val user = Saml2Authentication(principal, "response", AuthorityUtils.createAuthorityList("ROLE_USER"))
val result: MvcResult = this.mockMvc.perform(MockMvcRequestBuilders.post("/logout")
.with(SecurityMockMvcRequestPostProcessors.authentication(user))
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(MockMvcResultMatchers.status().isFound())
.andReturn()
val location = result.response.getHeader("Location")
Assertions.assertThat(location).startsWith("https://ap.example.org/logout/saml2/request")
}
@Configuration
@EnableWebSecurity
open class Saml2LogoutNoRelyingPartyRegistrationRepoConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
saml2Logout { }
}
return http.build()
}
}
@Configuration
@EnableWebSecurity
open class Saml2LogoutDefaultsConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
saml2Logout { }
}
return http.build()
}
@Bean
open fun registrations(): RelyingPartyRegistrationRepository =
InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build())
}
}
@@ -0,0 +1,282 @@
/*
* 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.
* 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.web.server
import org.junit.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.http.MediaType
import org.springframework.http.ResponseCookie
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration
import org.springframework.security.core.session.InMemoryReactiveSessionRegistry
import org.springframework.security.core.session.ReactiveSessionRegistry
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler
import org.springframework.security.web.server.authentication.SessionLimit
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
import org.springframework.web.server.session.DefaultWebSessionManager
/**
* Tests for [ServerSessionManagementDsl]
*
* @author Marcus da Coregio
*/
@ExtendWith(SpringTestContextExtension::class)
class ServerSessionManagementDslTests {
@JvmField
val spring = SpringTestContext(this)
private lateinit var client: WebTestClient
@Autowired
fun setup(context: ApplicationContext) {
this.client = WebTestClient
.bindToApplicationContext(context)
.configureClient()
.build()
}
@Test
fun `login when max sessions prevent login then second login fails`() {
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginTrueConfig::class.java).autowire()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data.add("username", "user")
data.add("password", "password")
val firstLoginSessionCookie = loginReturningCookie(data)
// second login should fail
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
.post()
.uri("/login")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromFormData(data))
.exchange()
.expectHeader()
.location("/login?error")
// first login should still be valid
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
.get()
.uri("/")
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
.exchange()
.expectStatus()
.isOk()
}
@Test
fun `login when max sessions does not prevent login then seconds login succeeds and first session is invalidated`() {
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.maxSessions = 1
this.spring.register(SessionManagementSpecTests.ConcurrentSessionsMaxSessionPreventsLoginFalseConfig::class.java)
.autowire()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data.add("username", "user")
data.add("password", "password")
val firstLoginSessionCookie = loginReturningCookie(data)
val secondLoginSessionCookie = loginReturningCookie(data)
// first login should not be valid
this.client.get()
.uri("/")
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
.exchange()
.expectStatus()
.isFound()
.expectHeader()
.location("/login")
// second login should be valid
this.client.get()
.uri("/")
.cookie(secondLoginSessionCookie!!.name, secondLoginSessionCookie.value)
.exchange()
.expectStatus()
.isOk()
}
@Test
fun `login when max sessions does not prevent login then least recently used session is invalidated`() {
ConcurrentSessionsMaxSessionPreventsLoginFalseConfig.maxSessions = 2
this.spring.register(ConcurrentSessionsMaxSessionPreventsLoginFalseConfig::class.java).autowire()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data.add("username", "user")
data.add("password", "password")
val firstLoginSessionCookie = loginReturningCookie(data)
val secondLoginSessionCookie = loginReturningCookie(data)
// update last access time for first request
this.client.get()
.uri("/")
.cookie(firstLoginSessionCookie!!.name, firstLoginSessionCookie.value)
.exchange()
.expectStatus()
.isOk()
val thirdLoginSessionCookie = loginReturningCookie(data)
// second login should be invalid, it is the least recently used session
this.client.get()
.uri("/")
.cookie(secondLoginSessionCookie!!.name, secondLoginSessionCookie.value)
.exchange()
.expectStatus()
.isFound()
.expectHeader()
.location("/login")
// first login should be valid
this.client.get()
.uri("/")
.cookie(firstLoginSessionCookie.name, firstLoginSessionCookie.value)
.exchange()
.expectStatus()
.isOk()
// third login should be valid
this.client.get()
.uri("/")
.cookie(thirdLoginSessionCookie!!.name, thirdLoginSessionCookie.value)
.exchange()
.expectStatus()
.isOk()
}
private fun loginReturningCookie(data: MultiValueMap<String, String>): ResponseCookie? {
return login(data).expectCookie()
.exists("SESSION")
.returnResult(Void::class.java)
.responseCookies
.getFirst("SESSION")
}
private fun login(data: MultiValueMap<String, String>): WebTestClient.ResponseSpec {
return client.mutateWith(SecurityMockServerConfigurers.csrf())
.post()
.uri("/login")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromFormData(data))
.exchange()
.expectStatus()
.is3xxRedirection()
.expectHeader()
.location("/")
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
@Import(Config::class)
open class ConcurrentSessionsMaxSessionPreventsLoginFalseConfig {
companion object {
var maxSessions = 1
}
@Bean
open fun springSecurity(http: ServerHttpSecurity, webSessionManager: DefaultWebSessionManager): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin { }
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(maxSessions)
maximumSessionsExceededHandler = InvalidateLeastUsedServerMaximumSessionsExceededHandler(webSessionManager.sessionStore)
}
}
}
}
}
@Configuration
@EnableWebFlux
@EnableWebFluxSecurity
@Import(Config::class)
open class ConcurrentSessionsMaxSessionPreventsLoginTrueConfig {
@Bean
open fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
formLogin { }
sessionManagement {
sessionConcurrency {
maximumSessions = SessionLimit.of(1)
maximumSessionsExceededHandler =
PreventLoginServerMaximumSessionsExceededHandler()
}
}
}
}
}
@Configuration
@Import(
ReactiveAuthenticationTestConfiguration::class,
DefaultController::class
)
open class Config {
@Bean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
open fun webSessionManager(): DefaultWebSessionManager {
return DefaultWebSessionManager()
}
@Bean
open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
return InMemoryReactiveSessionRegistry()
}
}
@RestController
open class DefaultController {
@GetMapping("/")
fun index(): String {
return "ok"
}
}
}
@@ -53,4 +53,7 @@
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="jwtBearerAccessTokenResponseClient"/>
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="tokenExchangeAccessTokenResponseClient"/>
</b:beans>
@@ -42,18 +42,21 @@
factory-method="authorizedClientManagerConsumer"/>
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="authorizationCodeAuthorizedClientProvider"/>
factory-method="authorizationCode"/>
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="refreshTokenAuthorizedClientProvider"/>
factory-method="refreshToken"/>
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="clientCredentialsAuthorizedClientProvider"/>
factory-method="clientCredentials"/>
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="passwordAuthorizedClientProvider"/>
factory-method="password"/>
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="jwtBearerAuthorizedClientProvider"/>
factory-method="jwtBearer"/>
<b:bean class="org.springframework.security.config.http.OAuth2AuthorizedClientManagerRegistrarTests"
factory-method="tokenExchange"/>
</b:beans>
@@ -6,7 +6,7 @@
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.2.xsd">
http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.3.xsd">
<tx:annotation-driven />
@@ -23,20 +23,20 @@
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<relying-party-registrations>
<relying-party-registrations id="registrations">
<relying-party-registration registration-id="one"
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
assertion-consumer-service-binding="REDIRECT"
asserting-party-id="google"/>
<relying-party-registration registration-id="two"
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
assertion-consumer-service-binding="POST"
asserting-party-id="simple-saml"/>
<asserting-party asserting-party-id="google" entity-id="https://accounts.google.com/o/saml2/idp/entity-id"
want-authn-requests-signed="true"
single-sign-on-service-location="https://accounts.google.com/o/saml2/idp/sso-url"
@@ -48,7 +48,7 @@
certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
</asserting-party>
<asserting-party asserting-party-id="simple-saml"
entity-id="https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"
single-sign-on-service-location="https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php"
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2021 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<relying-party-registrations id="registrations">
<relying-party-registration registration-id="one"
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
assertion-consumer-service-binding="REDIRECT"
asserting-party-id="google">
<signing-credential
certificate-location="classpath:org/springframework/security/config/saml2/rp-certificate.crt"
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
</relying-party-registration>
<asserting-party asserting-party-id="google" entity-id="https://accounts.google.com/o/saml2/idp/entity-id"
want-authn-requests-signed="true"
single-sign-on-service-location="https://accounts.google.com/o/saml2/idp/sso-url"
single-sign-on-service-binding="POST">
<verification-credential
certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
<encryption-credential
certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
</asserting-party>
</relying-party-registrations>
<b:bean class="org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver">
<b:constructor-arg ref="registrations"/>
<b:property name="relayStateResolver" ref="relayStateResolver"/>
</b:bean>
<b:bean name="relayStateResolver" class="org.mockito.Mockito" factory-method="mock">
<b:constructor-arg value="org.springframework.core.convert.converter.Converter" type="java.lang.Class"/>
</b:bean>
</b:beans>
@@ -23,14 +23,14 @@
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<relying-party-registrations>
<relying-party-registrations id="registrations">
<relying-party-registration registration-id="one"
entity-id="{baseUrl}/saml2/service-provider-metadata/{registrationId}"
assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"
assertion-consumer-service-binding="REDIRECT"
asserting-party-id="google"/>
<asserting-party asserting-party-id="google" entity-id="https://accounts.google.com/o/saml2/idp/entity-id"
want-authn-requests-signed="true"
single-sign-on-service-location="https://accounts.google.com/o/saml2/idp/sso-url"
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -125,7 +125,7 @@ public interface SecurityExpressionOperations {
* given the permission
* @param target the target domain object to check permission on
* @param permission the permission to check on the domain object (i.e. "read",
* "write", etc).
* "write", etc.).
* @return true if permission is granted to the {@link #getAuthentication()}, else
* false
*/
@@ -136,8 +136,8 @@ public interface SecurityExpressionOperations {
* object with a given id, type, and permission.
* @param targetId the identifier of the domain object to determine access
* @param targetType the type (i.e. com.example.domain.Message)
* @param permission the perission to check on the domain object (i.e. "read",
* "write", etc)
* @param permission the permission to check on the domain object (i.e. "read",
* "write", etc.)
* @return true if permission is granted to the {@link #getAuthentication()}, else
* false
*/
@@ -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.
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -30,6 +31,7 @@ import org.springframework.core.log.LogMessage;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.Assert;
/**
* <p>
@@ -74,31 +76,69 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
* your intentions clearer.
*
* @author Michael Mayr
* @author Josh Cummings
*/
public class RoleHierarchyImpl implements RoleHierarchy {
private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class);
/**
* Raw hierarchy configuration where each line represents single or multiple level
* role chain.
*/
private String roleHierarchyStringRepresentation = null;
/**
* {@code rolesReachableInOneStepMap} is a Map that under the key of a specific role
* name contains a set of all roles reachable from this role in 1 step (i.e. parsed
* {@link #roleHierarchyStringRepresentation} grouped by the higher role)
*/
private Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
/**
* {@code rolesReachableInOneOrMoreStepsMap} is a Map that under the key of a specific
* role name contains a set of all roles reachable from this role in 1 or more steps
* (i.e. fully resolved hierarchy from {@link #rolesReachableInOneStepMap})
*/
private Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
/**
* @deprecated Use {@link RoleHierarchyImpl#fromHierarchy} instead
*/
@Deprecated
public RoleHierarchyImpl() {
}
private RoleHierarchyImpl(Map<String, Set<GrantedAuthority>> hierarchy) {
this.rolesReachableInOneOrMoreStepsMap = buildRolesReachableInOneOrMoreStepsMap(hierarchy);
}
/**
* Create a role hierarchy instance with the given definition, similar to the
* following:
*
* <pre>
* ROLE_A &gt; ROLE_B
* ROLE_B &gt; ROLE_AUTHENTICATED
* ROLE_AUTHENTICATED &gt; ROLE_UNAUTHENTICATED
* </pre>
* @param hierarchy the role hierarchy to use
* @return a {@link RoleHierarchyImpl} that uses the given {@code hierarchy}
*/
public static RoleHierarchyImpl fromHierarchy(String hierarchy) {
return new RoleHierarchyImpl(buildRolesReachableInOneStepMap(hierarchy));
}
/**
* Factory method that creates a {@link Builder} instance with the default role prefix
* "ROLE_"
* @return a {@link Builder} instance with the default role prefix "ROLE_"
* @since 6.3
*/
public static Builder withDefaultRolePrefix() {
return withRolePrefix("ROLE_");
}
/**
* Factory method that creates a {@link Builder} instance with the specified role
* prefix.
* @param rolePrefix the prefix to be used for the roles in the hierarchy.
* @return a new {@link Builder} instance with the specified role prefix
* @throws IllegalArgumentException if the provided role prefix is null
* @since 6.3
*/
public static Builder withRolePrefix(String rolePrefix) {
Assert.notNull(rolePrefix, "rolePrefix must not be null");
return new Builder(rolePrefix);
}
/**
* Set the role hierarchy and pre-calculate for every role the set of all reachable
* roles, i.e. all roles lower in the hierarchy of every given role. Pre-calculation
@@ -106,13 +146,15 @@ public class RoleHierarchyImpl implements RoleHierarchy {
* time). During pre-calculation, cycles in role hierarchy are detected and will cause
* a <tt>CycleInRoleHierarchyException</tt> to be thrown.
* @param roleHierarchyStringRepresentation - String definition of the role hierarchy.
* @deprecated Use {@link RoleHierarchyImpl#fromHierarchy} instead
*/
@Deprecated
public void setHierarchy(String roleHierarchyStringRepresentation) {
this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
logger.debug(LogMessage.format("setHierarchy() - The following role hierarchy was set: %s",
roleHierarchyStringRepresentation));
buildRolesReachableInOneStepMap();
buildRolesReachableInOneOrMoreStepsMap();
Map<String, Set<GrantedAuthority>> hierarchy = buildRolesReachableInOneStepMap(
roleHierarchyStringRepresentation);
this.rolesReachableInOneOrMoreStepsMap = buildRolesReachableInOneOrMoreStepsMap(hierarchy);
}
@Override
@@ -156,21 +198,21 @@ public class RoleHierarchyImpl implements RoleHierarchy {
* Parse input and build the map for the roles reachable in one step: the higher role
* will become a key that references a set of the reachable lower roles.
*/
private void buildRolesReachableInOneStepMap() {
this.rolesReachableInOneStepMap = new HashMap<>();
for (String line : this.roleHierarchyStringRepresentation.split("\n")) {
private static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneStepMap(String hierarchy) {
Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = new HashMap<>();
for (String line : hierarchy.split("\n")) {
// Split on > and trim excessive whitespace
String[] roles = line.trim().split("\\s+>\\s+");
for (int i = 1; i < roles.length; i++) {
String higherRole = roles[i - 1];
GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i]);
Set<GrantedAuthority> rolesReachableInOneStepSet;
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
if (!rolesReachableInOneStepMap.containsKey(higherRole)) {
rolesReachableInOneStepSet = new HashSet<>();
this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
}
else {
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
rolesReachableInOneStepSet = rolesReachableInOneStepMap.get(higherRole);
}
rolesReachableInOneStepSet.add(lowerRole);
logger.debug(LogMessage.format(
@@ -178,6 +220,7 @@ public class RoleHierarchyImpl implements RoleHierarchy {
higherRole, lowerRole));
}
}
return rolesReachableInOneStepMap;
}
/**
@@ -186,30 +229,105 @@ public class RoleHierarchyImpl implements RoleHierarchy {
* CycleInRoleHierarchyException if a cycle in the role hierarchy definition is
* detected)
*/
private void buildRolesReachableInOneOrMoreStepsMap() {
this.rolesReachableInOneOrMoreStepsMap = new HashMap<>();
private static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneOrMoreStepsMap(
Map<String, Set<GrantedAuthority>> hierarchy) {
Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = new HashMap<>();
// iterate over all higher roles from rolesReachableInOneStepMap
for (String roleName : this.rolesReachableInOneStepMap.keySet()) {
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(this.rolesReachableInOneStepMap.get(roleName));
for (String roleName : hierarchy.keySet()) {
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(hierarchy.get(roleName));
Set<GrantedAuthority> visitedRolesSet = new HashSet<>();
while (!rolesToVisitSet.isEmpty()) {
// take a role from the rolesToVisit set
GrantedAuthority lowerRole = rolesToVisitSet.iterator().next();
rolesToVisitSet.remove(lowerRole);
if (!visitedRolesSet.add(lowerRole)
|| !this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())) {
if (!visitedRolesSet.add(lowerRole) || !hierarchy.containsKey(lowerRole.getAuthority())) {
continue; // Already visited role or role with missing hierarchy
}
else if (roleName.equals(lowerRole.getAuthority())) {
throw new CycleInRoleHierarchyException();
}
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(lowerRole.getAuthority()));
rolesToVisitSet.addAll(hierarchy.get(lowerRole.getAuthority()));
}
this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
logger.debug(LogMessage.format(
"buildRolesReachableInOneOrMoreStepsMap() - From role %s one can reach %s in one or more steps.",
roleName, visitedRolesSet));
}
return rolesReachableInOneOrMoreStepsMap;
}
/**
* Builder class for constructing a {@link RoleHierarchyImpl} based on a hierarchical
* role structure.
*
* @author Federico Herrera
* @since 6.3
*/
public static final class Builder {
private final String rolePrefix;
private final Map<String, Set<GrantedAuthority>> hierarchy;
private Builder(String rolePrefix) {
this.rolePrefix = rolePrefix;
this.hierarchy = new LinkedHashMap<>();
}
/**
* Creates a new hierarchy branch to define a role and its child roles.
* @param role the highest role in this branch
* @return a {@link ImpliedRoles} to define the child roles for the
* <code>role</code>
*/
public ImpliedRoles role(String role) {
Assert.hasText(role, "role must not be empty");
return new ImpliedRoles(role);
}
/**
* Builds and returns a {@link RoleHierarchyImpl} describing the defined role
* hierarchy.
* @return a {@link RoleHierarchyImpl}
*/
public RoleHierarchyImpl build() {
return new RoleHierarchyImpl(this.hierarchy);
}
private Builder addHierarchy(String role, String... impliedRoles) {
Set<GrantedAuthority> withPrefix = new HashSet<>();
for (String impliedRole : impliedRoles) {
withPrefix.add(new SimpleGrantedAuthority(this.rolePrefix.concat(impliedRole)));
}
this.hierarchy.put(this.rolePrefix.concat(role), withPrefix);
return this;
}
/**
* Builder class for constructing child roles within a role hierarchy branch.
*/
public final class ImpliedRoles {
private final String role;
private ImpliedRoles(String role) {
this.role = role;
}
/**
* Specifies implied role(s) for the current role in the hierarchy.
* @param impliedRoles role name(s) implied by the role.
* @return the same {@link Builder} instance
* @throws IllegalArgumentException if <code>impliedRoles</code> is null,
* empty or contains any null element.
*/
public Builder implies(String... impliedRoles) {
Assert.notEmpty(impliedRoles, "at least one implied role must be provided");
Assert.noNullElements(impliedRoles, "implied role name(s) cannot be empty");
return Builder.this.addHierarchy(this.role, impliedRoles);
}
}
}

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