1
0
mirror of synced 2026-07-17 16:45:14 +00:00

Compare commits

..

65 Commits

Author SHA1 Message Date
Rob Winch 02b8d4cb32 rm .github/workflows for unsupported branch 2025-05-02 12:35:27 -05:00
Rob Winch e0655bee7c rm merge-dependabot-pr.yml from Unsupported Branch 2025-04-25 13:16:06 -05:00
Marcus Hert Da Coregio 1a90041271 Only Merge PRs from Original Repository
(cherry picked from commit 31babecfbd)
2024-07-26 14:44:35 -03:00
Marcus Hert Da Coregio 2ee188ee36 Change Skip Condition
(cherry picked from commit 4c12940304)
2024-07-26 14:44:35 -03:00
Josh Cummings 5bfd1f59d1 Merge branch '5.8.x' into 6.1.x 2024-05-20 16:21:04 -06:00
Josh Cummings 4b10c2daac Turn Off Build Scans
Currently, --scan is preventing PR builds from completing.

Issue gh-15120
2024-05-20 16:20:43 -06:00
Josh Cummings 0e4d29763f Merge branch '5.8.x' into 6.1.x 2024-05-20 12:42:52 -06:00
Josh Cummings 63e24a3026 Adjust JavaDoc Classpath
Issue gh-14931
2024-05-20 12:42:01 -06:00
Marcus Hert Da Coregio 514600721c Merge branch '5.8.x' into 6.1.x 2024-05-20 13:51:57 -03:00
github-actions[bot] 2563b854ea Update Antora Spring UI to v0.4.15 2024-05-20 13:51:29 -03:00
github-actions[bot] 9027dceb71 Update Antora Spring UI to v0.4.15 2024-05-20 13:51:18 -03:00
dependabot[bot] d61a85f74b 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 20:39:20 -07:00
dependabot[bot] 8b3d33ed64 Bump org.springframework:spring-framework-bom from 6.0.19 to 6.0.20
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.0.19 to 6.0.20.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.0.19...v6.0.20)

---
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:31:22 -07:00
dependabot[bot] defe27d53e 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:31:02 -07:00
dependabot[bot] c5b86370ff 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:29:46 -07:00
dependabot[bot] da796e44e5 Bump io.projectreactor:reactor-bom from 2022.0.18 to 2022.0.19
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2022.0.18 to 2022.0.19.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2022.0.18...2022.0.19)

---
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:24:35 -07:00
Marcus Hert Da Coregio f5b9dc3282 Merge branch '5.8.x' into 6.1.x
Closes gh-15062
2024-05-13 11:22:09 -03:00
Patrick Schmidt 1ebd663253 doc: added hint to declare GrantedAuthorityDefaults as infrastructure bean
Closes gh-14779
2024-05-13 11:19:41 -03:00
Marcus Hert Da Coregio a8c60393f1 Merge branch '5.8.x' into 6.1.x
Closes gh-15059
2024-05-13 08:53:01 -03:00
Marcus Hert Da Coregio 09198447af Attach Antora Docs to Pull Requests
Closes gh-14992
2024-05-13 08:50:24 -03:00
dependabot[bot] 9ebee3e984 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 21:01:01 -07:00
dependabot[bot] 60e3f27211 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:59:55 -07:00
Steve Riesenberg f7650b3ec0 Merge branch '5.8.x' into 6.1.x
Closes gh-15037
2024-05-09 13:41:05 -05:00
Steve Riesenberg e1ec77a7a1 Add Build Updates section to generated release notes
Closes gh-14485
2024-05-09 13:40:25 -05:00
Steve Riesenberg 16e0d61005 Use correct API docs URL
Closes gh-15030
2024-05-08 15:58:45 -05:00
dependabot[bot] befa003f94 Bump io.spring.ge.conventions from 0.0.16 to 0.0.17
Bumps [io.spring.ge.conventions](https://github.com/spring-io/gradle-enterprise-conventions) from 0.0.16 to 0.0.17.
- [Release notes](https://github.com/spring-io/gradle-enterprise-conventions/releases)
- [Commits](https://github.com/spring-io/gradle-enterprise-conventions/compare/v0.0.16...v0.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-08 10:07:54 -05:00
Steve Riesenberg 74a77d2f72 Migrate to com.gradle.develocity plugin
Issue gh-15021
2024-05-08 10:07:53 -05:00
Steve Riesenberg a80fdee533 Update Gradle Enterprise plugin to 3.17.2
Issue gh-15020
2024-05-08 10:07:53 -05:00
Steve Riesenberg 53b581bb8c Merge branch '5.8.x' into 6.1.x 2024-05-08 10:05:03 -05:00
dependabot[bot] d686037419 Bump io.spring.ge.conventions from 0.0.16 to 0.0.17
Bumps [io.spring.ge.conventions](https://github.com/spring-io/gradle-enterprise-conventions) from 0.0.16 to 0.0.17.
- [Release notes](https://github.com/spring-io/gradle-enterprise-conventions/releases)
- [Commits](https://github.com/spring-io/gradle-enterprise-conventions/compare/v0.0.16...v0.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-08 10:03:34 -05:00
Steve Riesenberg 5c8311b7d3 Migrate to com.gradle.develocity plugin
Issue gh-15021
2024-05-08 10:03:34 -05:00
Steve Riesenberg d86545dd41 Update Gradle Enterprise plugin to 3.17.2
Issue gh-15020
2024-05-08 09:56:05 -05:00
Steve Riesenberg ee1031356a Polish RELEASE.adoc 2024-05-06 16:04:35 -05:00
Steve Riesenberg f74aeaffa1 Update RELEASE.adoc for release automation 2024-05-03 17:03:21 -05:00
dependabot[bot] ec1b15ed68 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:20:49 -07:00
Marcus Hert Da Coregio 93aa48f863 Merge branch '5.8.x' into 6.1.x
Closes gh-14994
2024-04-30 08:58:59 -03:00
Angel Gruevski ce995a0178 Correct a mistake in the documentation related to RequestCacheAwareFilter
Corrected a mistake in the documentation for the Spring Architecture where it was mentioned that RequestCacheAwareFilter is used to save the HttpServletRequest instead of the ExceptionTranslationFilter

Closes gh-14855
2024-04-30 08:58:25 -03:00
dependabot[bot] bbc5c5ce84 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:34:16 -07:00
Josh Cummings 1b8cf6cc55 Merge branch '5.8.x' into 6.1.x 2024-04-26 17:21:06 -06:00
Josh Cummings e5ee45d568 Fix Import Error
Issue gh-14880
2024-04-26 17:20:53 -06:00
Josh Cummings 38ae090d3d Merge branch '5.8.x' into 6.1.x
Closes gh-14973
2024-04-26 17:13:39 -06:00
Abimael Sergio 3b9991fc89 Improve PasswordEncoder Error Messaging
Closes gh-14880
2024-04-26 17:13:17 -06:00
Josh Cummings 1ecb036fba Merge branch '5.8.x' into 6.1.x 2024-04-26 17:09:05 -06:00
sheheryarumair 0e211382ee Remove useBase64 parameter 2024-04-26 17:05:49 -06:00
Josh Cummings 2477c22674 Fix Role Name Typo
Closes gh-14965
2024-04-26 15:37:51 -06:00
Josh Cummings d88f2e5133 Merge branch '5.8.x' into 6.1.x
Closes gh-14957
2024-04-25 16:08:47 -06:00
Josh Cummings c35e107744 Add ProxyRestriction Validator
Closes gh-14931
2024-04-25 16:04:37 -06:00
Josh Cummings 3f7d9e389d Update to OAuth2 OIDC SDK 9.43.4
Issue gh-14920
2024-04-23 08:23:50 -06:00
Josh Cummings 992243fc8b Update to Nimbus 9.37.3
Closes gh-14836
2024-04-23 08:23:27 -06:00
github-actions[bot] d0341bf893 Merge branch '5.8.x' into 6.1.x 2024-04-22 01:13:08 +00:00
dependabot[bot] 8271258618 Bump slackapi/slack-github-action from 1.25.0 to 1.26.0
Bumps [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) from 1.25.0 to 1.26.0.
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Commits](https://github.com/slackapi/slack-github-action/compare/v1.25.0...v1.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-21 18:12:20 -07:00
Rob Winch 761fdd228d Merge branch '5.8.x' into 6.1.x 2024-04-19 14:24:58 -05:00
Rob Winch 1010d253a4 Merge branch '5.7.x' into 5.8.x 2024-04-19 14:20:16 -05:00
Rob Winch c8342fcfae Use antora-extensions 1.10.0 2024-04-19 14:16:02 -05:00
github-actions[bot] c97f710747 Merge branch '5.8.x' into 6.1.x 2024-04-18 00:03:05 +00:00
dependabot[bot] 2287d67106 Bump io.projectreactor.tools:blockhound
Bumps [io.projectreactor.tools:blockhound](https://github.com/reactor/BlockHound) from 1.0.8.RELEASE to 1.0.9.RELEASE.
- [Release notes](https://github.com/reactor/BlockHound/releases)
- [Commits](https://github.com/reactor/BlockHound/compare/1.0.8.RELEASE...1.0.9.RELEASE)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-17 17:01:58 -07:00
dependabot[bot] da4af08362 Bump io.spring.gradle:spring-security-release-plugin from 1.0.2 to 1.0.3
Bumps [io.spring.gradle:spring-security-release-plugin](https://github.com/spring-io/spring-security-release-tools) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/spring-io/spring-security-release-tools/releases)
- [Commits](https://github.com/spring-io/spring-security-release-tools/compare/v1.0.2...v1.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-17 17:00:41 -07:00
Josh Cummings 61df8e493d Merge branch '5.8.x' into 6.1.x 2024-04-17 17:00:24 -06:00
Josh Cummings 470e2c5c97 Address Build Issues
Issue gh-14837
2024-04-17 16:59:40 -06:00
Josh Cummings ade45771b2 Merge branch '5.8.x' into 6.1.x
Closes gh-14921
2024-04-17 16:44:50 -06:00
Josh Cummings 657760af5b Improve Logging
Closes gh-14837
2024-04-17 16:43:29 -06:00
github-actions[bot] 8a03482856 Next development version 2024-04-15 16:01:13 +00:00
github-actions[bot] 71a51db3a1 Next development version 2024-04-15 15:58:06 +00:00
github-actions[bot] 17873adf1f Release 5.8.12 2024-04-15 15:21:00 +00:00
github-actions[bot] 4bab573562 Release 6.1.9 2024-04-15 15:20:31 +00:00
551 changed files with 2724 additions and 21660 deletions
-1
View File
@@ -15,4 +15,3 @@
*.otg binary
*.png binary
*.hsx binary
*.serialized binary
-72
View File
@@ -1,72 +0,0 @@
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: "/"
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" ]
- package-ecosystem: "gradle"
target-branch: "6.1.x"
milestone: 318 # 6.1.x
directory: "/"
schedule:
interval: "daily"
time: "03:00"
timezone: "Etc/UTC"
labels: [ "type: dependency-upgrade" ]
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: "/"
schedule:
interval: "daily"
time: "03:00"
timezone: "Etc/UTC"
labels: [ "type: dependency-upgrade" ]
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" ]
@@ -1,23 +0,0 @@
name: Clean build artifacts
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Delete artifacts in cron job
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
echo "Running clean build artifacts logic"
output=$(curl -X GET -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts | grep '"id"' | cut -d : -f2 | sed 's/,*$//g')
echo Output is $output
for id in $output; do curl -X DELETE -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts/$id; done;
@@ -1,134 +0,0 @@
name: CI
on:
push:
branches-ignore:
- "dependabot/**"
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
env:
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
permissions:
contents: read
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@v1
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
jdk: [ 17 ]
with:
runs-on: ${{ matrix.os }}
java-version: ${{ matrix.jdk }}
distribution: temurin
secrets: inherit
test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@v1
strategy:
matrix:
include:
- java-version: 21-ea
toolchain: 21
- java-version: 17
toolchain: 17
with:
java-version: ${{ matrix.java-version }}
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=6.1.+ -PreactorVersion=2023.0.+ -PspringDataVersion=2023.1.+ --stacktrace
secrets: inherit
check-samples:
name: Check Samples
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'spring-projects' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: 17
distribution: temurin
- name: Check samples project
env:
LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos
SAMPLES_DIR: ../spring-security-samples
run: |
# Extract version from gradle.properties
version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
# Extract samplesBranch from gradle.properties
samples_branch=$(cat gradle.properties | grep "samplesBranch=" | awk -F'=' '{print $2}')
./gradlew publishMavenJavaPublicationToLocalRepository
./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR"
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" :runAllTests
check-tangles:
name: Check for Package Tangles
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'spring-projects' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: 17
distribution: temurin
- name: Check for package tangles
env:
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
run: |
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
deploy-artifacts:
name: Deploy Artifacts
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-docs:
name: Deploy Docs
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1
with:
should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-schema:
name: Deploy Schema
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
perform-release:
name: Perform Release
needs: [ deploy-artifacts, deploy-docs, deploy-schema ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
with:
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
milestone-repo-url: https://repo.spring.io/artifactory/milestone
release-repo-url: https://repo1.maven.org/maven2
artifact-path: org/springframework/security/spring-security-core
slack-announcing-id: spring-security-announcing
secrets: inherit
notify_result:
name: Check for failures
needs: [ perform-release ]
if: failure()
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Send Slack message
# Workaround while waiting for Gamesight/slack-workflow-status#38 to be fixed
# See https://github.com/Gamesight/slack-workflow-status/issues/38
uses: sjohnr/slack-workflow-status@v1-beta
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
-33
View File
@@ -1,33 +0,0 @@
name: Deploy Docs
on:
push:
branches-ignore:
- "gh-pages"
- "dependabot/**"
tags: '**'
repository_dispatch:
types: request-build-reference # legacy
#schedule:
#- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch:
permissions: read-all
jobs:
build:
runs-on: ubuntu-latest
if: github.repository_owner == 'spring-projects'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: docs-build
fetch-depth: 1
- name: Dispatch (partial build)
if: github.ref_type == 'branch'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
- name: Dispatch (full build)
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
@@ -1,32 +0,0 @@
name: Execute Gradle Wrapper Upgrade
on:
schedule:
- cron: '0 2 * * *' # 2am UTC
workflow_dispatch:
jobs:
upgrade_wrapper:
name: Execution
runs-on: ubuntu-latest
steps:
- name: Set up Git configuration
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global url."https://unused-username:${TOKEN}@github.com/".insteadOf "https://github.com/"
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
- name: Upgrade Wrappers
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
env:
WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-52
View File
@@ -1,52 +0,0 @@
name: Merge Dependabot PR
on: pull_request_target
run-name: Merge Dependabot PR ${{ github.ref_name }}
permissions: write-all
jobs:
merge-dependabot-pr:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- uses: actions/checkout@v4
with:
show-progress: false
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Set Milestone to Dependabot Pull Request
id: set-milestone
run: |
if test -f pom.xml
then
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
else
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
fi
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
if [ -z $MILESTONE ]
then
gh run cancel ${{ github.run_id }}
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
else
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
echo mergeEnabled=true >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Dependabot pull request
if: steps.set-milestone.outputs.mergeEnabled
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
@@ -1,40 +0,0 @@
name: Check Milestone
on:
milestone:
types: [created, opened, edited]
env:
DUE_ON: ${{ github.event.milestone.due_on }}
TITLE: ${{ github.event.milestone.title }}
permissions:
contents: read
jobs:
spring-releasetrain-checks:
name: Check DueOn is on a Release Date
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Print Milestone Being Checked
run: echo "Validating DueOn '$DUE_ON' for milestone '$TITLE'"
- name: Validate DueOn
if: env.DUE_ON != ''
run: |
export TOOL_VERSION=0.1.1
wget "https://repo.maven.apache.org/maven2/io/spring/releasetrain/spring-release-train-tools/$TOOL_VERSION/spring-release-train-tools-$TOOL_VERSION.jar"
java -cp "spring-release-train-tools-$TOOL_VERSION.jar" io.spring.releasetrain.CheckMilestoneDueOnMain --dueOn "$DUE_ON" --expectedDayOfWeek MONDAY --expectedMondayCount 3
notify_result:
name: Check for failures
needs: [spring-releasetrain-checks]
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'
-26
View File
@@ -1,26 +0,0 @@
name: PR Build
on: pull_request
env:
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew clean build --continue --scan
-24
View File
@@ -1,24 +0,0 @@
name: Release Scheduler
on:
schedule:
- cron: '15 15 * * MON' # Every Monday at 3:15pm UTC
workflow_dispatch:
permissions: read-all
jobs:
dispatch_scheduled_releases:
name: Dispatch scheduled releases
if: github.repository_owner == 'spring-projects'
strategy:
matrix:
# List of active maintenance branches.
branch: [ main, 6.1.x, 5.8.x ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Dispatch
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run update-scheduled-release-version.yml -r ${{ matrix.branch }}
@@ -1,22 +0,0 @@
name: Trigger Dependabot Auto Merge Forward
on:
push:
branches:
- '*.x'
permissions: read-all
jobs:
trigger-worflow:
name: Trigger Workflow
runs-on: ubuntu-latest
if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- id: trigger
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run dependabot-auto-merge-forward.yml -r main
@@ -1,13 +0,0 @@
name: Update Scheduled Release Version
on:
workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main.
permissions:
contents: read
jobs:
update-scheduled-release-version:
name: Update Scheduled Release Version
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@v1
secrets: inherit
+2
View File
@@ -28,3 +28,5 @@ s101plugin.state
!.idea/checkstyle-idea.xml
!.idea/externalDependencies.xml
node_modules
+207 -220
View File
@@ -1,54 +1,236 @@
= Release Process
The release process for Spring Security is partially automated. The following table outlines which steps are automated and which are manual. Follow the links to read about each step.
The release process for Spring Security is entirely automated via the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc[Spring Security Release Plugin] and https://github.com/spring-io/spring-security-release-tools/tree/main/.github/workflows[reusable workflows].
The following table outlines the steps that are taken by the automation.
[cols="1,1"]
WARNING: The `5.8.x` branch does not have all of the improvements from the `6.x.x` branches. See "Status (5.8.x)" for which steps are still manual.
In case of a failure, you can follow the links below to read about each step, which includes instructions for performing the step manually if applicable.
See <<frequently-asked-questions,FAQ>> for troubleshooting tips.
[cols="1,1,1"]
|===
| Step | Status
| Step | Status (5.8.x) | Status (6.0.x+)
| <<update-dependencies>>
| :x: manual
| :white_check_mark: automated
| :white_check_mark: automated
| <<check-all-issues-are-closed>>
| :white_check_mark: automated (scheduled release will abort if any issues are open)
| :white_check_mark: automated
| :white_check_mark: automated
| <<update-release-version>>
| :white_check_mark: automated
| <<update-antora-version>>
| :white_check_mark: automated
| <<build-locally>>
| :x: manual (when updating dependencies)
| <<push-release-commit>>
| :white_check_mark: automated
| <<announce-release-on-slack>>
| :white_check_mark: automated
| <<tag-release>>
| :white_check_mark: automated
| :white_check_mark: automated
| <<update-to-next-development-version>>
| <<push-release-commit>>
| :white_check_mark: automated
| :white_check_mark: automated
| <<build-locally>>
| :white_check_mark: automated
| :white_check_mark: automated
| <<update-release-notes-on-github>>
| :white_check_mark: automated
| :white_check_mark: automated
| <<update-version-on-project-page>>
| :x: manual
| <<update-release-notes-on-github>>
| :white_check_mark: automated
| <<close-create-milestone,Close milestone>>
| :x: manual (move issues to new milestone before release)
| :x: manual
| :white_check_mark: automated
| <<announce-release-on-slack>>
| :white_check_mark: automated
| :white_check_mark: automated
| <<update-to-next-development-version>>
| :white_check_mark: automated
| :white_check_mark: automated
| <<close-create-milestone,Create milestone>>
| :white_check_mark: automated (if not already created)
| :white_check_mark: automated
| :white_check_mark: automated
| <<announce-release-on-other-channels>>
| :x: manual
| :x: manual
|===
[#update-dependencies]
== Update dependencies
Dependency versions are managed in the file xref:./gradle/libs.versions.toml[libs.versions.toml] and are automatically updated by xref:./.github/dependabot.yml[dependabot].
[#check-all-issues-are-closed]
== Check all issues are closed
The first step of a release is to check if there are any open issues remaining in a milestone.
NOTE: A scheduled release will not proceed if there are any open issues.
TIP: If you need to prevent a release from occurring automatically, the easiest way to block a release is to add an unresolved issue to the milestone.
The https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#checkMilestoneHasNoOpenIssues[`checkMilestoneHasOpenIssues`] command will check if there are any open issues for the release.
Before running the command manually, replace the following values:
* `<next-version>` - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`. This is optional since you are unlikely to reach the rate limit for such a simple check.
[source,bash]
----
./gradlew checkMilestoneHasOpenIssues -PnextVersion=<next-version> -PgitHubAccessToken=<github-personal-access-token>
----
Alternatively, you can manually check using the https://github.com/spring-projects/spring-security/milestones[milestones] page.
[#update-release-version]
== Update release version
If all issues for the release are <<check-all-issues-are-closed,closed>>, the version number is automatically updated using the milestone title.
When performing this step manually, update the version number in `gradle.properties` for the release (for example `5.5.0`) and commit the change using the message "Release x.y.z".
[#tag-release]
== Tag release
The release will automatically be tagged using the milestone title.
It is not required to tag manually.
However, you can perform this step manually by running the following command:
[source,bash]
----
git tag 5.5.0
----
[#push-release-commit]
== Push release commit
During a scheduled release, the release commit will automatically be pushed to trigger a build.
If performing this step manually, you can push the commit and tag and GitHub actions will build and deploy the artifacts with the following command:
[source,bash]
----
git push --atomic origin main 5.5.0
----
The build will automatically wait for artifacts to be released to Maven Central.
You can get notified manually when uploading is complete by running the following:
[source,bash]
----
./scripts/release/wait-for-done.sh 5.5.0
----
[#build-locally]
== Build
All checks will automatically be performed by the build prior to uploading the artifacts to Maven Central.
If something goes wrong, you can run the build locally using:
[source,bash]
----
./gradlew check
----
[#update-release-notes-on-github]
== Update release notes on GitHub
Once the release has been uploaded to Maven Central, release notes will automatically be generated and a GitHub release will be created.
To do this manually, you can use the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#generateChangelog[`generateChangelog`] command to generate the release notes by replacing:
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
[source,bash]
----
./gradlew generateChangelog -PnextVersion=<next-version>
----
Then copy the release notes to your clipboard (your mileage may vary with the following command):
[source,bash]
----
cat build/changelog/release-notes.md | xclip -selection clipboard
----
Finally, create the
https://github.com/spring-projects/spring-security/releases[release on
GitHub], associate it with the tag, and paste the generated notes.
Alternatively, you can run the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#createGitHubRelease[`createGitHubRelease`] command to perform these steps automatically, replacing:
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
* `<branch>` - The name of the branch to be tagged (if the release commit has not already been tagged)
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `write:org`
[source,bash]
----
./gradlew createGitHubRelease -PnextVersion=<next-version> -Pbranch=<branch> -PcreateRelease=true -PgitHubAccessToken=<github-personal-access-token>
----
[#update-version-on-project-page]
== Update version on project page
The build will automatically update the project versions on https://spring.io/projects/spring-security#learn.
To do this manually, you can use the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#createSaganRelease[`createSaganRelease`] and https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#deleteSaganRelease[`deleteSaganRelease`] commands using the following parameters:
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
* `<previous-version>` - Replace with the previous release which will be removed from the listed versions (i.e. 5.5.0-RC1)
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `read:org` as https://spring.io/restdocs/index.html#authentication[documented for spring.io api]
[source,bash]
----
./gradlew createSaganRelease deleteSaganRelease -PnextVersion=<next-version> -PpreviousVersion=<previous-version> -PgitHubAccessToken=<github-personal-access-token>
----
Alternatively, you can log into Contentful and update the versions manually on the Spring Security project page.
[#close-create-milestone]
== Close / Create milestone
The release milestone will be automatically closed once the release is complete.
To proceed manually, perform the following steps:
1. Visit https://github.com/spring-projects/spring-security/milestones[GitHub
Milestones] and create a new milestone for the next release version
2. Move any open issues from the existing milestone you just released to the new milestone
3. Close the milestone for the release
NOTE: Remember that scheduled releases <<check-all-issues-are-closed,will not proceed>> if there are still open issues in the milestone.
[#announce-release-on-slack]
== Announce release on Slack
The release will automatically be announced on Slack.
If proceeding manually, announce the release on Slack in the channel https://pivotal.slack.com/messages/spring-release[#spring-release], including the keyword `+spring-security-announcing+` in the message.
Something like:
....
spring-security-announcing `5.5.0` is available now
....
[#update-to-next-development-version]
== Update to next development version
After the release is complete and artifacts have been uploaded to Maven Central, the build will automatically update to the next development version, commit and push.
If proceeding manually, update the version in `gradle.properties` to the next `+SNAPSHOT+` version with the commit message "Next development version" and then push.
[#announce-release-on-other-channels]
== Announce release on other channels
* Create a blog post on Contentful
* Tweet from https://twitter.com/springsecurity[@SpringSecurity]
[[frequently-asked-questions]]
== Frequently Asked Questions
*When should I update dependencies manually?* Dependencies should be updated at the latest the end of the week prior to the release. This is usually the Friday following the 2nd Monday of the month (counting from the first week with a Monday). When in doubt, check the https://github.com/spring-projects/spring-security/milestones[milestones] page for release due dates.
*When do scheduled releases occur?* Automated releases are scheduled to occur at *3:15 PM UTC* on the *3rd Monday of the month* (counting from the first week with a Monday).
@@ -59,9 +241,10 @@ The scheduled release process currently runs every Monday but only releases when
The automated release process occurs on the following branches:
* `main`
* `6.0.x`
* `6.2.x`
* `6.1.x`
* `6.0.x` (commercial only)
* `5.8.x`
* `5.7.x`
For each of the above branches, the automated process performs the following checks before proceeding with the release:
@@ -71,7 +254,7 @@ For each of the above branches, the automated process performs the following che
[IMPORTANT]
You should ensure all issues are closed or moved to another milestone prior to a scheduled release.
If the above checks pass, the version number is updated (in `gradle.properties` and `antora.yml`) and a commit is pushed to trigger the CI process.
If the above checks pass, the version number is updated (in `gradle.properties`) and a commit is pushed to trigger the CI process.
*How do I trigger a release manually?* You can trigger a release manually in two ways:
@@ -81,199 +264,3 @@ If the above checks pass, the version number is updated (in `gradle.properties`
*When should additional manual steps be performed?* All other automated steps listed above occur during the normal CI process. Additional manual steps can be performed at any time once the builds pass and releases are finished.
*What if something goes wrong?* If the normal CI process fails, you can retry by re-running the failed jobs with the "Re-run failed jobs" option in GitHub Actions. If changes are required, you should revert the "Release x.y.z" commit, delete the tag, and proceed manually.
[#update-dependencies]
== Update dependencies
Ensure you have no changes in your local repository.
Change to a new branch.
For example:
[source,bash]
----
$ git checkout -b 5.5.0-RC1-dependencies
----
Review the rules in build.gradle to ensure the rules make sense.
For example, we should not allow major version updates in a patch release.
Also ensure that all of the exclusions still make sense.
The following Gradle command will update your dependencies creating a commit for each dependency update.
The first invocation of the command will take quite a while (~20 minutes depending on internet speed) to run because it is indexing all the versions of all the dependencies.
[source,bash]
----
$ ./gradlew updateDependencies
----
Review the commits to ensure that the updated dependency versions make sense for this release. For example, we should not perform a major version update for a patch release.
[source,bash]
----
$ git log
----
If any of the versions dont make sense, update `build.gradle` to ensure that the version is excluded.
Run all the checks:
[source,bash]
----
$ ./gradlew check
----
If they dont work, you can run a git bisect to discover what broke the build.
Fix any commits that broke the build.
Check out the original brach:
[source,bash]
----
$ git checkout -
----
The following command will update the dependencies again but this time creating a ticket for each update and placing `Closes gh-<number>` in the commit. Replacing the following values:
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
[source,bash]
----
$ ./gradlew updateDependencies -PupdateMode=GITHUB_ISSUE -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version>
----
Apply any fixes from your previous branch that were necessary.
[#check-all-issues-are-closed]
== Check all issues are closed
The following command will check if there are any open issues for the ticket.
Before running the command, replace the following values:
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`. This is optional since you are unlikely to reach the rate limit for such a simple check.
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
[source,bash]
----
$ ./gradlew gitHubCheckMilestoneHasNoOpenIssues -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version>
----
Alternatively, you can manually check using https://github.com/spring-projects/spring-security/milestones
[#update-release-version]
== Update release version
Update the version number in `gradle.properties` for the release, for example `5.5.0-M1`, `5.5.0-RC1`, `5.5.0`
[#update-antora-version]
== Update antora version
You will need to update the antora.yml version.
If you are unsure of what the values should be, the following task will instruct you what the expected values are:
[source,bash]
----
./gradlew :spring-security-docs:antoraCheckVersion
----
[#build-locally]
== Build locally
Run the build using
[source,bash]
----
$ ./gradlew check
----
[#push-release-commit]
== Push release commit
Push the commit and GitHub actions will build and deploy the artifacts
If you are pushing to Maven Central, then you can get notified when its uploaded by running the following:
[source,bash]
----
$ ./scripts/release/wait-for-done.sh 5.5.0
----
[#announce-release-on-slack]
== Announce release on Slack
* Announce via Slack on
https://pivotal.slack.com/messages/spring-release[#spring-release],
including the keyword `+spring-security-announcing+` in the message.
Something like:
....
spring-security-announcing 5.5.0 is available.
....
[#tag-release]
== Tag release
* Tag the release and then push the tag
....
git tag 5.4.0-RC1
git push origin 5.4.0-RC1
....
[#update-to-next-development-version]
== Update to next development version
* Update `gradle.properties` version to next `+SNAPSHOT+` version, update antora.yml, and then push
[#update-version-on-project-page]
== Update version on project page
The following command will update https://spring.io/projects/spring-security#learn with the new release version using the following parameters
<github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `read:org` as https://spring.io/restdocs/index.html#authentication[documented for spring.io api]
<next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
<previous-version> - Replace with the previous release which will be removed from the listed versions (i.e. 5.5.0-M3)
[source,bash]
----
$ ./gradlew saganCreateRelease saganDeleteRelease -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version> -PpreviousVersion=<previous-version>
----
[#update-release-notes-on-github]
== Update release notes on GitHub
Generate the Release Notes replacing:
* <next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
----
$ ./gradlew generateChangelog -PnextVersion=<next-version>
----
* Copy the release notes to your clipboard (your mileage may vary with
the following command)
....
cat build/changelog/release-notes.md | xclip -selection clipboard
....
* Create the
https://github.com/spring-projects/spring-security/releases[release on
GitHub], associate it with the tag, and paste the generated notes
[#close-create-milestone]
== Close / Create milestone
* In
https://github.com/spring-projects/spring-security/milestones[GitHub
Milestones], create a new milestone for the next release version
* Move any open issues from the existing milestone you just released to
the new milestone
* Close the milestone for the release.
[#announce-release-on-other-channels]
== Announce release on other channels
* Create a https://spring.io/admin/blog[Blog]
* Tweet from [@SpringSecurity](https://twitter.com/springsecurity)
@@ -118,7 +118,8 @@ public class AclPermissionEvaluator implements PermissionEvaluator {
if (permission instanceof Permission[]) {
return Arrays.asList((Permission[]) permission);
}
if (permission instanceof String permString) {
if (permission instanceof String) {
String permString = (String) permission;
Permission p = buildPermission(permString);
if (p != null) {
return Arrays.asList(p);
@@ -56,9 +56,10 @@ public abstract class AbstractPermission implements Permission {
if (obj == null) {
return false;
}
if (!(obj instanceof Permission other)) {
if (!(obj instanceof Permission)) {
return false;
}
Permission other = (Permission) obj;
return (this.mask == other.getMask());
}
@@ -112,7 +112,7 @@ public class JdbcAclServiceTests {
given(this.jdbcOperations.query(anyString(), eq(args), any(RowMapper.class))).willReturn(result);
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L);
List<ObjectIdentity> objectIdentities = this.aclService.findChildren(objectIdentity);
assertThat(objectIdentities).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("5577");
}
@@ -127,7 +127,7 @@ public class JdbcAclServiceTests {
public void findChildrenWithoutIdType() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 4711L);
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockUntypedIdDomainObject.class.getName());
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(5000L);
}
@@ -143,7 +143,7 @@ public class JdbcAclServiceTests {
public void findChildrenOfIdTypeLong() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US-PAL");
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities).hasSize(2);
assertThat(objectIdentities.size()).isEqualTo(2);
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(4711L);
assertThat(objectIdentities.get(1).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
@@ -155,7 +155,7 @@ public class JdbcAclServiceTests {
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
this.aclServiceIntegration.setAclClassIdSupported(true);
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("US-PAL");
}
@@ -165,7 +165,7 @@ public class JdbcAclServiceTests {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockUntypedIdDomainObject.class, 5000L);
this.aclServiceIntegration.setAclClassIdSupported(true);
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo("costcenter");
assertThat(objectIdentities.get(0).getIdentifier())
.isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762"));
@@ -186,7 +186,7 @@ public class JdbcAclServiceTests {
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
this.aclServiceIntegration.setAclClassIdSupported(true);
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
assertThat(objectIdentities).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL");
}
@@ -454,7 +454,7 @@ public class JdbcMutableAclServiceTests {
CustomSid customSid = new CustomSid("Custom sid");
given(customJdbcMutableAclService.createOrRetrieveSidPrimaryKey("Custom sid", false, false)).willReturn(1L);
Long result = customJdbcMutableAclService.createOrRetrieveSidPrimaryKey(customSid, false);
assertThat(Long.valueOf(1L)).isEqualTo(result);
assertThat(new Long(1L)).isEqualTo(result);
}
protected Authentication getAuth() {
@@ -120,9 +120,9 @@ public class SidTests {
PrincipalSid principalSid = new PrincipalSid(authentication);
GrantedAuthority ga = new SimpleGrantedAuthority("ROLE_TEST");
GrantedAuthoritySid gaSid = new GrantedAuthoritySid(ga);
assertThat("johndoe").isEqualTo(principalSid.getPrincipal());
assertThat("johndoe".equals(principalSid.getPrincipal())).isTrue();
assertThat("scott".equals(principalSid.getPrincipal())).isFalse();
assertThat("ROLE_TEST").isEqualTo(gaSid.getGrantedAuthority());
assertThat("ROLE_TEST".equals(gaSid.getGrantedAuthority())).isTrue();
assertThat("ROLE_TEST2".equals(gaSid.getGrantedAuthority())).isFalse();
}
+6
View File
@@ -29,4 +29,10 @@ dependencies {
testAspect sourceSets.main.output
}
sourceSets.main.aspectj.srcDir "src/main/java"
sourceSets.main.java.srcDirs = files()
sourceSets.test.aspectj.srcDir "src/test/java"
sourceSets.test.java.srcDirs = files()
compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml"
@@ -143,8 +143,8 @@ public class AnnotationSecurityAspectTests {
SecurityContextHolder.getContext().setAuthentication(this.anne);
List<String> objects = this.prePostSecured.postFilterMethod();
assertThat(objects).hasSize(2);
assertThat(objects).contains("apple");
assertThat(objects).contains("aubergine");
assertThat(objects.contains("apple")).isTrue();
assertThat(objects.contains("aubergine")).isTrue();
}
private void configureForElAnnotations() {
+15 -35
View File
@@ -14,10 +14,6 @@ buildscript {
}
}
plugins {
alias(libs.plugins.org.gradle.wrapper.upgrade)
}
apply plugin: 'io.spring.nohttp'
apply plugin: 'locks'
apply plugin: 's101'
@@ -35,42 +31,29 @@ ext.milestoneBuild = !(snapshotBuild || releaseBuild)
repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
}
springRelease {
weekOfMonth = 3
dayOfWeek = 1
referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html"
apiDocUrl = "https://docs.spring.io/spring-security/docs/{version}/api/"
apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/"
replaceSnapshotVersionInReferenceDocUrl = true
}
def toolchainVersion() {
if (project.hasProperty('testToolchain')) {
return project.property('testToolchain').toString().toInteger()
}
return 17
}
subprojects {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(toolchainVersion())
plugins.withType(JavaPlugin) {
java {
sourceCompatibility=JavaVersion.VERSION_17
}
}
kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks.withType(JavaCompile).configureEach {
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
options.release.set(17)
}
}
allprojects {
if (!['spring-security-bom', 'spring-security-docs'].contains(project.name)) {
apply plugin: 'io.spring.javaformat'
@@ -95,12 +78,18 @@ allprojects {
}
}
}
tasks.withType(JavaCompile).configureEach {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(17)
}
}
}
if (hasProperty('buildScan')) {
develocity {
buildScan {
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
termsOfServiceAgree = 'yes'
termsOfUseUrl = 'https://gradle.com/help/legal-terms-of-use'
termsOfUseAgree = 'yes'
}
}
@@ -120,12 +109,3 @@ s101 {
repository = 'https://structure101.com/binaries/latest'
configurationDirectory = project.file("etc/s101")
}
wrapperUpgrade {
gradle {
'spring-security' {
repo = 'spring-projects/spring-security'
baseBranch = '6.1.x' // runs only on 6.1.x and the update is merged forward to main
}
}
}
-1
View File
@@ -11,7 +11,6 @@ java {
repositories {
gradlePluginPortal()
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
sourceSets {
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-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
@@ -35,7 +35,9 @@ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin<Pro
@Override
public void apply(Project project) {
project.getPlugins().apply(CheckProhibitedDependenciesLifecyclePlugin.class);
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> configureProhibitedDependencyChecks(project));
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> {
configureProhibitedDependencyChecks(project);
});
}
private void configureProhibitedDependencyChecks(Project project) {
@@ -34,6 +34,8 @@ public class CheckProhibitedDependenciesLifecyclePlugin implements Plugin<Projec
task.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies");
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkProhibitedDependencies));
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> {
checkTask.dependsOn(checkProhibitedDependencies);
});
}
}
@@ -21,6 +21,7 @@ import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.publish.Publication;
import org.gradle.api.publish.PublishingExtension;
import org.gradle.api.publish.plugins.PublishingPlugin;
import org.gradle.plugins.signing.SigningExtension;
import org.gradle.plugins.signing.SigningPlugin;
@@ -43,7 +44,12 @@ public class SpringSigningPlugin implements Plugin<Project> {
private void sign(Project project) {
SigningExtension signing = project.getExtensions().findByType(SigningExtension.class);
signing.setRequired((Callable<Boolean>) () -> project.getGradle().getTaskGraph().hasTask("publishArtifacts"));
signing.setRequired(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return project.getGradle().getTaskGraph().hasTask("publishArtifacts");
}
});
String signingKeyId = (String) project.findProperty("signingKeyId");
String signingKey = (String) project.findProperty("signingKey");
String signingPassword = (String) project.findProperty("signingPassword");
@@ -39,7 +39,6 @@ import org.springframework.security.core.Authentication;
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.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
@@ -197,9 +196,6 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
public CasAuthenticationFilter() {
@@ -218,10 +214,9 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
}
this.logger.debug(
LogMessage.format("Authentication success. Updating SecurityContextHolder to contain: %s", authResult));
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
this.securityContextHolderStrategy.setContext(context);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
@@ -312,12 +307,6 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
this.securityContextRepository = securityContextRepository;
}
@Override
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
super.setSecurityContextHolderStrategy(securityContextHolderStrategy);
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
/**
* Indicates if the request is elgible to process a service ticket. This method exists
* for readability.
@@ -37,8 +37,6 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.test.util.ReflectionTestUtils;
@@ -234,15 +232,4 @@ public class CasAuthenticationFilterTests {
verify(securityContextRepository).saveContext(any(SecurityContext.class), any(), any());
}
@Test
void successfulAuthenticationWhenSecurityContextHolderStrategySetThenUses() throws ServletException, IOException {
SecurityContextHolderStrategy securityContextRepository = mock(SecurityContextHolderStrategy.class);
given(securityContextRepository.createEmptyContext()).willReturn(new SecurityContextImpl());
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setSecurityContextHolderStrategy(securityContextRepository);
filter.successfulAuthentication(new MockHttpServletRequest(), new MockHttpServletResponse(),
new MockFilterChain(), mock(Authentication.class));
verify(securityContextRepository).setContext(any(SecurityContext.class));
}
}
+1 -7
View File
@@ -80,6 +80,7 @@ dependencies {
testImplementation "org.hibernate.orm:hibernate-core"
testImplementation 'org.hsqldb:hsqldb'
testImplementation 'org.mockito:mockito-core'
testImplementation "org.mockito:mockito-inline"
testImplementation('org.seleniumhq.selenium:htmlunit-driver') {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'xml-apis', module: 'xml-apis'
@@ -112,7 +113,6 @@ dependencies {
testImplementation ('org.apache.maven.resolver:maven-resolver-transport-http') {
exclude group: "org.slf4j", module: "jcl-over-slf4j"
}
testImplementation libs.org.instancio.instancio.junit
testRuntimeOnly 'org.hsqldb:hsqldb'
}
@@ -154,9 +154,3 @@ tasks.withType(KotlinCompile).configureEach {
jvmTarget = "17"
}
}
configure(project.tasks.withType(Test)) {
doFirst {
systemProperties['springSecurityVersion'] = version
}
}
@@ -294,7 +294,7 @@ public class RSocketMessageHandlerITests {
@MessageMapping({ "secure.send", "send" })
Mono<Void> send(Mono<String> payload) {
return payload.doOnNext(this::add).then(Mono.fromRunnable(this::doNotifyAll));
return payload.doOnNext(this::add).then(Mono.fromRunnable(() -> doNotifyAll()));
}
private synchronized void doNotifyAll() {
@@ -87,7 +87,7 @@ public class LdapUserServiceBeanDefinitionParserTests {
Set<String> authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities());
assertThat(authorities).hasSize(3);
assertThat(authorities).contains("ROLE_DEVELOPERS");
assertThat(authorities.contains("ROLE_DEVELOPERS")).isTrue();
}
@Test
@@ -128,7 +128,7 @@ public class LdapUserServiceBeanDefinitionParserTests {
Set<String> authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities());
assertThat(authorities).hasSize(3);
assertThat(authorities).contains("ROLE_DEVELOPER");
assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue();
}
@@ -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.1. Please update your schema declarations to the 6.1 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\\.1.*.xsd.*")
|| schemaLocation.matches("(?m).*spring-security.xsd.*")
|| !schemaLocation.matches("(?m).*spring-security.*");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2013 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.
@@ -27,7 +27,6 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.util.Assert;
import org.springframework.web.filter.DelegatingFilterProxy;
@@ -118,10 +117,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
* @param configurer
* @return the {@link SecurityConfigurerAdapter} for further customizations
* @throws Exception
* @deprecated For removal in 7.0. Use
* {@link #with(SecurityConfigurerAdapter, Customizer)} instead.
*/
@Deprecated(since = "6.2", forRemoval = true)
@SuppressWarnings("unchecked")
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
configurer.addObjectPostProcessor(this.objectPostProcessor);
@@ -143,23 +139,6 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
return configurer;
}
/**
* Applies a {@link SecurityConfigurerAdapter} to this {@link SecurityBuilder} and
* invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}.
* @param configurer
* @return the {@link SecurityBuilder} for further customizations
* @throws Exception
* @since 6.2
*/
@SuppressWarnings("unchecked")
public <C extends SecurityConfigurerAdapter<O, B>> B with(C configurer, Customizer<C> customizer) throws Exception {
configurer.addObjectPostProcessor(this.objectPostProcessor);
configurer.setBuilder((B) this);
add(configurer);
customizer.customize(configurer);
return (B) this;
}
/**
* Sets an object that is shared by multiple {@link SecurityConfigurer}.
* @param sharedType the Class to key the shared object by.
@@ -41,8 +41,7 @@ final class MethodSecuritySelector implements ImportSelector {
@Override
public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())
&& !importMetadata.hasMetaAnnotation(EnableMethodSecurity.class.getName())) {
if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())) {
return new String[0];
}
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
@@ -70,7 +70,6 @@ import org.springframework.security.config.annotation.web.configurers.SessionMan
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
@@ -2836,16 +2835,6 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
return HttpSecurity.this;
}
public OidcLogoutConfigurer<HttpSecurity> oidcLogout() throws Exception {
return getOrApply(new OidcLogoutConfigurer<>());
}
public HttpSecurity oidcLogout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oidcLogoutCustomizer)
throws Exception {
oidcLogoutCustomizer.customize(getOrApply(new OidcLogoutConfigurer<>()));
return HttpSecurity.this;
}
/**
* Configures OAuth 2.0 Client support.
* @return the {@link OAuth2ClientConfigurer} for further customizations
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,7 +47,6 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.cors.CorsConfigurationSource;
import static org.springframework.security.config.Customizer.withDefaults;
@@ -125,18 +124,10 @@ class HttpSecurityConfiguration {
.apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());
// @formatter:on
applyCorsIfAvailable(http);
applyDefaultConfigurers(http);
return http;
}
private void applyCorsIfAvailable(HttpSecurity http) throws Exception {
String[] beanNames = this.context.getBeanNamesForType(CorsConfigurationSource.class);
if (beanNames.length == 1) {
http.cors(withDefaults());
}
}
private AuthenticationManager authenticationManager() throws Exception {
return this.authenticationConfiguration.getAuthenticationManager();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,46 +16,19 @@
package org.springframework.security.config.annotation.web.configuration;
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.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
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.context.annotation.ImportSelector;
import org.springframework.core.ResolvableType;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider;
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.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
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.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
@@ -75,8 +48,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
* @since 5.1
* @see OAuth2ImportSelector
*/
@Import({ OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class,
OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerConfiguration.class })
@Import(OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class)
final class OAuth2ClientConfiguration {
private static final boolean webMvcPresent;
@@ -93,22 +65,8 @@ final class OAuth2ClientConfiguration {
if (!webMvcPresent) {
return new String[0];
}
return new String[] {
OAuth2ClientConfiguration.class.getName() + ".OAuth2ClientWebMvcSecurityConfiguration" };
}
}
/**
* @author Joe Grandja
* @since 6.2.0
*/
@Configuration(proxyBeanMethods = false)
static class OAuth2AuthorizedClientManagerConfiguration {
@Bean(name = OAuth2AuthorizedClientManagerRegistrar.BEAN_NAME)
OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() {
return new OAuth2AuthorizedClientManagerRegistrar();
return new String[] { "org.springframework.security.config.annotation.web.configuration."
+ "OAuth2ClientConfiguration.OAuth2ClientWebMvcSecurityConfiguration" };
}
}
@@ -116,12 +74,16 @@ final class OAuth2ClientConfiguration {
@Configuration(proxyBeanMethods = false)
static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer {
private ClientRegistrationRepository clientRegistrationRepository;
private OAuth2AuthorizedClientRepository authorizedClientRepository;
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient;
private OAuth2AuthorizedClientManager authorizedClientManager;
private SecurityContextHolderStrategy securityContextHolderStrategy;
private OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
OAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
@@ -135,6 +97,26 @@ final class OAuth2ClientConfiguration {
}
}
@Autowired(required = false)
void setClientRegistrationRepository(List<ClientRegistrationRepository> clientRegistrationRepositories) {
if (clientRegistrationRepositories.size() == 1) {
this.clientRegistrationRepository = clientRegistrationRepositories.get(0);
}
}
@Autowired(required = false)
void setAuthorizedClientRepository(List<OAuth2AuthorizedClientRepository> authorizedClientRepositories) {
if (authorizedClientRepositories.size() == 1) {
this.authorizedClientRepository = authorizedClientRepositories.get(0);
}
}
@Autowired(required = false)
void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
}
@Autowired(required = false)
void setAuthorizedClientManager(List<OAuth2AuthorizedClientManager> authorizedClientManagers) {
if (authorizedClientManagers.size() == 1) {
@@ -147,267 +129,35 @@ final class OAuth2ClientConfiguration {
this.securityContextHolderStrategy = strategy;
}
@Autowired
void setAuthorizedClientManagerRegistrar(
OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) {
this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar;
}
private OAuth2AuthorizedClientManager getAuthorizedClientManager() {
if (this.authorizedClientManager != null) {
return this.authorizedClientManager;
}
return this.authorizedClientManagerRegistrar.getAuthorizedClientManagerIfAvailable();
}
}
/**
* A registrar for registering the default {@link OAuth2AuthorizedClientManager} bean
* definition, if not already present.
*
* @author Joe Grandja
* @author Steve Riesenberg
* @since 6.2.0
*/
static final class OAuth2AuthorizedClientManagerRegistrar
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(
AuthorizationCodeOAuth2AuthorizedClientProvider.class,
RefreshTokenOAuth2AuthorizedClientProvider.class,
ClientCredentialsOAuth2AuthorizedClientProvider.class,
PasswordOAuth2AuthorizedClientProvider.class,
JwtBearerOAuth2AuthorizedClientProvider.class
);
// @formatter:on
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
private ListableBeanFactory beanFactory;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (getBeanNamesForType(OAuth2AuthorizedClientManager.class).length != 0
|| getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
return;
}
BeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(OAuth2AuthorizedClientManager.class)
.setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME)
.getBeanDefinition();
registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry),
beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ListableBeanFactory) beanFactory;
}
OAuth2AuthorizedClientManager getAuthorizedClientManagerIfAvailable() {
if (getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
return null;
}
return getAuthorizedClientManager();
}
OAuth2AuthorizedClientManager getAuthorizedClientManager() {
ClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils
.beanOfTypeIncludingAncestors(this.beanFactory, ClientRegistrationRepository.class, true, true);
OAuth2AuthorizedClientRepository authorizedClientRepository = BeanFactoryUtils
.beanOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientRepository.class, true, true);
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviderBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientProvider.class, true, true)
.values();
OAuth2AuthorizedClientProvider authorizedClientProvider;
if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) {
authorizedClientProvider = authorizedClientProviderBeans.iterator().next();
}
else {
List<OAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
authorizedClientProviders
.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders
.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans));
OAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider(
authorizedClientProviderBeans);
if (jwtBearerAuthorizedClientProvider != null) {
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
OAuth2AuthorizedClientManager authorizedClientManager = null;
if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) {
if (this.accessTokenResponseClient != null) {
// @formatter:off
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
.builder()
.authorizationCode()
.refreshToken()
.clientCredentials((configurer) -> configurer.accessTokenResponseClient(this.accessTokenResponseClient))
.password()
.build();
// @formatter:on
DefaultOAuth2AuthorizedClientManager defaultAuthorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientRepository);
defaultAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager = defaultAuthorizedClientManager;
}
else {
authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientRepository);
}
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
}
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
Consumer<DefaultOAuth2AuthorizedClientManager> authorizedClientManagerConsumer = getBeanOfType(
ResolvableType.forClassWithGenerics(Consumer.class, DefaultOAuth2AuthorizedClientManager.class));
if (authorizedClientManagerConsumer != null) {
authorizedClientManagerConsumer.accept(authorizedClientManager);
}
return authorizedClientManager;
}
private boolean hasDelegatingAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
if (authorizedClientProviders.size() != 1) {
return false;
}
return authorizedClientProviders.iterator().next() instanceof DelegatingOAuth2AuthorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
AuthorizationCodeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, AuthorizationCodeOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new AuthorizationCodeOAuth2AuthorizedClientProvider();
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, RefreshTokenOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
}
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2RefreshTokenGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, ClientCredentialsOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
}
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2ClientCredentialsGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, PasswordOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
}
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2PasswordGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, JwtBearerOAuth2AuthorizedClientProvider.class);
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
JwtBearerGrantRequest.class));
if (accessTokenResponseClient != null) {
if (authorizedClientProvider == null) {
authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
}
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
authorizedClientProviders);
additionalAuthorizedClientProviders
.removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass()));
return additionalAuthorizedClientProviders;
}
private <T extends OAuth2AuthorizedClientProvider> T getAuthorizedClientProviderByType(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders, Class<T> providerClass) {
T authorizedClientProvider = null;
for (OAuth2AuthorizedClientProvider current : authorizedClientProviders) {
if (providerClass.isInstance(current)) {
assertAuthorizedClientProviderIsNull(authorizedClientProvider);
authorizedClientProvider = providerClass.cast(current);
}
}
return authorizedClientProvider;
}
private static void assertAuthorizedClientProviderIsNull(
OAuth2AuthorizedClientProvider authorizedClientProvider) {
if (authorizedClientProvider != null) {
// @formatter:off
throw new BeanInitializationException(String.format(
"Unable to create an %s bean. Expected one bean of type %s, but found multiple. " +
"Please consider defining only a single bean of this type, or define an %s bean yourself.",
OAuth2AuthorizedClientManager.class.getName(),
authorizedClientProvider.getClass().getName(),
OAuth2AuthorizedClientManager.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();
}
}
}
@@ -35,7 +35,6 @@ import org.springframework.security.authorization.SpringAuthorizationEventPublis
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.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
@@ -63,8 +62,6 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
private final Supplier<RoleHierarchy> roleHierarchy;
private String rolePrefix = "ROLE_";
/**
* Creates an instance.
* @param context the {@link ApplicationContext} to use
@@ -79,11 +76,6 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
}
this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0)
? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy());
String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
if (grantedAuthorityDefaultsBeanNames.length > 0) {
GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class);
this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
}
}
/**
@@ -287,8 +279,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* customizations
*/
public AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
return access(withRoleHierarchy(AuthorityAuthorizationManager
.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })));
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasRole(role)));
}
/**
@@ -300,8 +291,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* customizations
*/
public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) {
return access(withRoleHierarchy(
AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles)));
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(roles)));
}
/**
@@ -40,6 +40,7 @@ import org.springframework.security.web.csrf.CsrfLogoutHandler;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
import org.springframework.security.web.session.InvalidSessionStrategy;
@@ -82,7 +83,7 @@ import org.springframework.util.Assert;
public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {
private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;
@@ -104,7 +105,7 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
/**
* Specify the {@link CsrfTokenRepository} to use. The default is an
* {@link HttpSessionCsrfTokenRepository}.
* {@link HttpSessionCsrfTokenRepository} wrapped by {@link LazyCsrfTokenRepository}.
* @param csrfTokenRepository the {@link CsrfTokenRepository} to use
* @return the {@link CsrfConfigurer} for further customizations
*/
@@ -296,7 +296,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
* @param sessionAuthenticationStrategy
* @return the {@link SessionManagementConfigurer} for further customizations
*/
public SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
SessionAuthenticationStrategy sessionAuthenticationStrategy) {
this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy);
return this;
@@ -1,35 +0,0 @@
/*
* 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.annotation.web.configurers.oauth2.client;
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;
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new OidcBackChannelLogoutTokenValidator(clientRegistration));
}
}
@@ -16,8 +16,6 @@
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
@@ -309,22 +307,7 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
if (this.accessTokenResponseClient != null) {
return this.accessTokenResponseClient;
}
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2AuthorizationCodeGrantRequest.class);
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> bean = getBeanOrNull(resolvableType);
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
}
@SuppressWarnings("unchecked")
private <T> T getBeanOrNull(ResolvableType type) {
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
if (context != null) {
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
}
}
return null;
return new DefaultAuthorizationCodeTokenResponseClient();
}
}
@@ -25,8 +25,6 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
@@ -114,13 +112,4 @@ final class OAuth2ClientConfigurerUtils {
return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null);
}
static <B extends HttpSecurityBuilder<B>> OidcSessionRegistry getOidcSessionRegistry(B builder) {
OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class);
if (sessionRegistry == null) {
sessionRegistry = new InMemoryOidcSessionRegistry();
builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry);
}
return sessionRegistry;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,18 +22,9 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.ResolvableType;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
@@ -41,14 +32,9 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.AbstractSessionEvent;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionIdChangedEvent;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
@@ -56,9 +42,6 @@ import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationC
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -84,10 +67,7 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -144,7 +124,6 @@ import org.springframework.util.ReflectionUtils;
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
* configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
* login page will be made available</li>
* <li>{@link OidcSessionRegistry}</li>
* </ul>
*
* @author Joe Grandja
@@ -223,18 +202,6 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return this;
}
/**
* Sets the registry for managing the OIDC client-provider session link
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
* @return the {@link OAuth2LoginConfigurer} for further configuration
* @since 6.2
*/
public OAuth2LoginConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
return this;
}
/**
* Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization
* Server's Authorization Endpoint.
@@ -363,7 +330,10 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
super.init(http);
}
}
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = getAccessTokenResponseClient();
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = this.tokenEndpointConfig.accessTokenResponseClient;
if (accessTokenResponseClient == null) {
accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
}
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = getOAuth2UserService();
OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider = new OAuth2LoginAuthenticationProvider(
accessTokenResponseClient, oauth2UserService);
@@ -430,7 +400,6 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
authenticationFilter
.setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);
}
configureOidcSessionRegistry(http);
super.configure(http);
}
@@ -473,16 +442,6 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null);
}
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> getAccessTokenResponseClient() {
if (this.tokenEndpointConfig.accessTokenResponseClient != null) {
return this.tokenEndpointConfig.accessTokenResponseClient;
}
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2AuthorizationCodeGrantRequest.class);
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> bean = getBeanOrNull(resolvableType);
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
}
private OAuth2UserService<OidcUserRequest, OidcUser> getOidcUserService() {
if (this.userInfoEndpointConfig.oidcUserService != null) {
return this.userInfoEndpointConfig.oidcUserService;
@@ -581,33 +540,6 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return AnyRequestMatcher.INSTANCE;
}
private void configureOidcSessionRegistry(B http) {
if (http.getConfigurer(OidcLogoutConfigurer.class) == null
&& http.getSharedObject(OidcSessionRegistry.class) == null) {
return;
}
OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http);
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
if (sessionConfigurer != null) {
OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy();
sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry);
sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy);
}
OidcClientSessionEventListener listener = new OidcClientSessionEventListener();
listener.setSessionRegistry(sessionRegistry);
registerDelegateApplicationListener(listener);
}
private void registerDelegateApplicationListener(ApplicationListener<?> delegate) {
DelegatingApplicationListener delegating = getBeanOrNull(
ResolvableType.forType(DelegatingApplicationListener.class));
if (delegating == null) {
return;
}
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
delegating.addListener(smartListener);
}
/**
* Configuration options for the Authorization Server's Authorization Endpoint.
*/
@@ -855,88 +787,4 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
}
private static final class OidcClientSessionEventListener implements ApplicationListener<AbstractSessionEvent> {
private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class);
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
/**
* {@inheritDoc}
*/
@Override
public void onApplicationEvent(AbstractSessionEvent event) {
if (event instanceof SessionDestroyedEvent destroyed) {
this.logger.debug("Received SessionDestroyedEvent");
this.sessionRegistry.removeSessionInformation(destroyed.getId());
return;
}
if (event instanceof SessionIdChangedEvent changed) {
this.logger.debug("Received SessionIdChangedEvent");
OidcSessionInformation information = this.sessionRegistry
.removeSessionInformation(changed.getOldSessionId());
if (information == null) {
this.logger
.debug("Failed to register new session id since old session id was not found in registry");
return;
}
this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId()));
}
}
/**
* The registry where OIDC Provider sessions are linked to the Client session.
* Defaults to in-memory storage.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
}
private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy {
private final Log logger = LogFactory.getLog(getClass());
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
/**
* {@inheritDoc}
*/
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
return;
}
String sessionId = session.getId();
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
Map<String, String> headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken())
: Collections.emptyMap();
OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user);
if (this.logger.isTraceEnabled()) {
this.logger
.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer()));
}
this.sessionRegistry.saveSessionInformation(registration);
}
/**
* The registration for linking OIDC Provider Session information to the Client's
* session. Defaults to in-memory storage.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
}
}
@@ -1,66 +0,0 @@
/*
* 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.annotation.web.configurers.oauth2.client;
import java.util.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
/**
* An {@link org.springframework.security.core.Authentication} implementation that
* represents the result of authenticating an OIDC Logout token for the purposes of
* performing Back-Channel Logout.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
private final OidcLogoutToken logoutToken;
/**
* Construct an {@link OidcBackChannelLogoutAuthentication}
* @param logoutToken a deserialized, verified OIDC Logout Token
*/
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
super(Collections.emptyList());
this.logoutToken = logoutToken;
setAuthenticated(true);
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getPrincipal() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getCredentials() {
return this.logoutToken;
}
}
@@ -1,114 +0,0 @@
/*
* 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.annotation.web.configurers.oauth2.client;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely
* deserializing it, verifying its signature, and validating its claims.
*
* <p>
* Intended to be included in a
* {@link org.springframework.security.authentication.ProviderManager}
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see org.springframework.security.authentication.ProviderManager
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
final class OidcBackChannelLogoutAuthenticationProvider implements AuthenticationProvider {
private JwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory;
/**
* Construct an {@link OidcBackChannelLogoutAuthenticationProvider}
*/
OidcBackChannelLogoutAuthenticationProvider() {
OidcIdTokenDecoderFactory logoutTokenDecoderFactory = new OidcIdTokenDecoderFactory();
logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory());
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
/**
* {@inheritDoc}
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!(authentication instanceof OidcLogoutAuthenticationToken token)) {
return null;
}
String logoutToken = token.getLogoutToken();
ClientRegistration registration = token.getClientRegistration();
Jwt jwt = decode(registration, logoutToken);
OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
.claims((claims) -> claims.putAll(jwt.getClaims()))
.build();
return new OidcBackChannelLogoutAuthentication(oidcLogoutToken);
}
/**
* {@inheritDoc}
*/
@Override
public boolean supports(Class<?> authentication) {
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
}
private Jwt decode(ClientRegistration registration, String token) {
JwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration);
try {
return logoutTokenDecoder.decode(token);
}
catch (BadJwtException failed) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, failed.getMessage(),
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
throw new OAuth2AuthenticationException(error, failed);
}
catch (Exception failed) {
throw new AuthenticationServiceException(failed.getMessage(), failed);
}
}
/**
* Use this {@link JwtDecoderFactory} to generate {@link JwtDecoder}s that correspond
* to the {@link ClientRegistration} associated with the OIDC logout token.
* @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use
*/
void setLogoutTokenDecoderFactory(JwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory) {
Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null");
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
}
@@ -1,139 +0,0 @@
/*
* 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.annotation.web.configurers.oauth2.client;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A filter for the Client-side OIDC Back-Channel Logout endpoint
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
private final Log logger = LogFactory.getLog(getClass());
private final AuthenticationConverter authenticationConverter;
private final AuthenticationManager authenticationManager;
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
/**
* Construct an {@link OidcBackChannelLogoutFilter}
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
* Logout Token authentication
* @param authenticationManager the {@link AuthenticationManager} for authenticating
* Logout Tokens
*/
OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
AuthenticationManager authenticationManager) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationConverter = authenticationConverter;
this.authenticationManager = authenticationManager;
}
/**
* {@inheritDoc}
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
Authentication token;
try {
token = this.authenticationConverter.convert(request);
}
catch (AuthenticationServiceException ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
throw ex;
}
catch (AuthenticationException ex) {
handleAuthenticationFailure(response, ex);
return;
}
if (token == null) {
chain.doFilter(request, response);
return;
}
Authentication authentication;
try {
authentication = this.authenticationManager.authenticate(token);
}
catch (AuthenticationServiceException ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
throw ex;
}
catch (AuthenticationException ex) {
handleAuthenticationFailure(response, ex);
return;
}
this.logoutHandler.logout(request, response, authentication);
}
private void handleAuthenticationFailure(HttpServletResponse response, Exception ex) throws IOException {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
this.errorHttpMessageConverter.write(oauth2Error(ex), null, new ServletServerHttpResponse(response));
}
private OAuth2Error oauth2Error(Exception ex) {
if (ex instanceof OAuth2AuthenticationException oauth2) {
return oauth2.getError();
}
return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(),
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
/**
* The strategy for expiring all Client sessions indicated by the logout request.
* Defaults to {@link OidcBackChannelLogoutHandler}.
* @param logoutHandler the {@link LogoutHandler} to use
*/
void setLogoutHandler(LogoutHandler logoutHandler) {
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
this.logoutHandler = logoutHandler;
}
}
@@ -1,205 +0,0 @@
/*
* 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.configurers.oauth2.client;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A {@link LogoutHandler} that locates the sessions associated with a given OIDC
* Back-Channel Logout Token and invalidates each one.
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
final class OidcBackChannelLogoutHandler implements LogoutHandler {
private final Log logger = LogFactory.getLog(getClass());
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
private RestOperations restOperations = new RestTemplate();
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
private String sessionCookieName = "JSESSIONID";
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
if (this.logger.isDebugEnabled()) {
String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type";
this.logger.debug(String.format(message, authentication.getClass().getSimpleName()));
}
return;
}
Iterable<OidcSessionInformation> sessions = this.sessionRegistry.removeSessionInformation(token.getPrincipal());
Collection<String> errors = new ArrayList<>();
int totalCount = 0;
int invalidatedCount = 0;
for (OidcSessionInformation session : sessions) {
totalCount++;
try {
eachLogout(request, session);
invalidatedCount++;
}
catch (RestClientException ex) {
this.logger.debug("Failed to invalidate session", ex);
errors.add(ex.getMessage());
this.sessionRegistry.saveSessionInformation(session);
}
}
if (this.logger.isTraceEnabled()) {
this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount, totalCount));
}
if (!errors.isEmpty()) {
handleLogoutFailure(response, oauth2Error(errors));
}
}
private void eachLogout(HttpServletRequest request, OidcSessionInformation session) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
headers.add(credential.getKey(), credential.getValue());
}
String logout = computeLogoutEndpoint(request);
HttpEntity<?> entity = new HttpEntity<>(null, headers);
this.restOperations.postForEntity(logout, entity, Object.class);
}
String computeLogoutEndpoint(HttpServletRequest request) {
// @formatter:off
UriComponents uriComponents = UriComponentsBuilder
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.build();
Map<String, String> uriVariables = new HashMap<>();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", (scheme != null) ? scheme : "");
uriVariables.put("baseUrl", uriComponents.toUriString());
String host = uriComponents.getHost();
uriVariables.put("baseHost", (host != null) ? host : "");
String path = uriComponents.getPath();
uriVariables.put("basePath", (path != null) ? path : "");
int port = uriComponents.getPort();
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
return UriComponentsBuilder.fromUriString(this.logoutUri)
.buildAndExpand(uriVariables)
.toUriString();
// @formatter:on
}
private OAuth2Error oauth2Error(Collection<String> errors) {
return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors,
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
private void handleLogoutFailure(HttpServletResponse response, OAuth2Error error) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
try {
this.errorHttpMessageConverter.write(error, null, new ServletServerHttpResponse(response));
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
/**
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
* this class uses
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
* sessions.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
/**
* Use this {@link RestOperations} to perform the per-session back-channel logout
* @param restOperations the {@link RestOperations} to use
*/
void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
/**
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
* since that is the default URI for
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
* @param logoutUri the URI to use
*/
void setLogoutUri(String logoutUri) {
Assert.hasText(logoutUri, "logoutUri cannot be empty");
this.logoutUri = logoutUri;
}
/**
* Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}.
*
* <p>
* Note that if you are using Spring Session, this likely needs to change to SESSION.
* @param sessionCookieName the cookie name to use
*/
void setSessionCookieName(String sessionCookieName) {
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
this.sessionCookieName = sessionCookieName;
}
}
@@ -1,118 +0,0 @@
/*
* 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.annotation.web.configurers.oauth2.client;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
* with the OIDC Back-Channel Logout Spec.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
* Token</a>
* @see <a target="blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation">the OIDC
* Back-Channel Logout spec</a>
*/
final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<Jwt> {
private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation";
private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
private final String audience;
private final String issuer;
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
Collection<OAuth2Error> errors = new ArrayList<>();
LogoutTokenClaimAccessor logoutClaims = jwt::getClaims;
Map<String, Object> events = logoutClaims.getEvents();
if (events == null) {
errors.add(invalidLogoutToken("events claim must not be null"));
}
else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) {
errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key"));
}
String issuer = logoutClaims.getIssuer().toExternalForm();
if (issuer == null) {
errors.add(invalidLogoutToken("iss claim must not be null"));
}
else if (!this.issuer.equals(issuer)) {
errors.add(invalidLogoutToken(
"iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`"));
}
List<String> audience = logoutClaims.getAudience();
if (audience == null) {
errors.add(invalidLogoutToken("aud claim must not be null"));
}
else if (!audience.contains(this.audience)) {
errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`"));
}
Instant issuedAt = logoutClaims.getIssuedAt();
if (issuedAt == null) {
errors.add(invalidLogoutToken("iat claim must not be null"));
}
String jwtId = logoutClaims.getId();
if (jwtId == null) {
errors.add(invalidLogoutToken("jti claim must not be null"));
}
if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) {
errors.add(invalidLogoutToken("sub and sid claims must not both be null"));
}
if (logoutClaims.getClaim("nonce") != null) {
errors.add(invalidLogoutToken("nonce claim must not be present"));
}
return OAuth2TokenValidatorResult.failure(errors);
}
private static OAuth2Error invalidLogoutToken(String description) {
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL);
}
}
@@ -1,85 +0,0 @@
/*
* 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.annotation.web.configurers.oauth2.client;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication
* request
*
* @author Josh Cummings
* @since 6.2
*/
final class OidcLogoutAuthenticationConverter implements AuthenticationConverter {
private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}";
private final Log logger = LogFactory.getLog(getClass());
private final ClientRegistrationRepository clientRegistrationRepository;
private RequestMatcher requestMatcher = new AntPathRequestMatcher(DEFAULT_LOGOUT_URI, "POST");
OidcLogoutAuthenticationConverter(ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public Authentication convert(HttpServletRequest request) {
RequestMatcher.MatchResult result = this.requestMatcher.matcher(request);
if (!result.isMatch()) {
return null;
}
String registrationId = result.getVariables().get("registrationId");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
this.logger.debug("Did not process OIDC Back-Channel Logout since no ClientRegistration was found");
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
String logoutToken = request.getParameter("logout_token");
if (logoutToken == null) {
this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found");
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration);
}
/**
* The logout endpoint. Defaults to
* {@code /logout/connect/back-channel/{registrationId}}.
* @param requestMatcher the {@link RequestMatcher} to use
*/
void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
}
@@ -1,80 +0,0 @@
/*
* 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.annotation.web.configurers.oauth2.client;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* An {@link org.springframework.security.core.Authentication} instance that represents a
* request to authenticate an OIDC Logout Token.
*
* @author Josh Cummings
* @since 6.2
*/
class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken {
private final String logoutToken;
private final ClientRegistration clientRegistration;
/**
* Construct an {@link OidcLogoutAuthenticationToken}
* @param logoutToken a signed, serialized OIDC Logout token
* @param clientRegistration the {@link ClientRegistration client} associated with
* this token; this is usually derived from material in the logout HTTP request
*/
OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) {
super(AuthorityUtils.NO_AUTHORITIES);
this.logoutToken = logoutToken;
this.clientRegistration = clientRegistration;
}
/**
* {@inheritDoc}
*/
@Override
public String getCredentials() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public String getPrincipal() {
return this.logoutToken;
}
/**
* Get the signed, serialized OIDC Logout token
* @return the logout token
*/
String getLogoutToken() {
return this.logoutToken;
}
/**
* Get the {@link ClientRegistration} associated with this logout token
* @return the {@link ClientRegistration}
*/
ClientRegistration getClientRegistration() {
return this.clientRegistration;
}
}
@@ -1,196 +0,0 @@
/*
* 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.configurers.oauth2.client;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.util.Assert;
/**
* An {@link AbstractHttpConfigurer} for OIDC Logout flows
*
* <p>
* OIDC Logout provides an application with the capability to have users log out by using
* their existing account at an OAuth 2.0 or OpenID Connect 1.0 Provider.
*
*
* <h2>Security Filters</h2>
*
* The following {@code Filter} is populated:
*
* <ul>
* <li>{@link OidcBackChannelLogoutFilter}</li>
* </ul>
*
* <h2>Shared Objects Used</h2>
*
* The following shared objects are used:
*
* <ul>
* <li>{@link ClientRegistrationRepository}</li>
* </ul>
*
* @author Josh Cummings
* @since 6.2
* @see HttpSecurity#oidcLogout()
* @see OidcBackChannelLogoutFilter
* @see ClientRegistrationRepository
*/
public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<OidcLogoutConfigurer<B>, B> {
private BackChannelLogoutConfigurer backChannel;
/**
* Sets the repository of client registrations.
* @param clientRegistrationRepository the repository of client registrations
* @return the {@link OAuth2LoginConfigurer} for further configuration
*/
public OidcLogoutConfigurer<B> clientRegistrationRepository(
ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
return this;
}
/**
* Sets the registry for managing the OIDC client-provider session link
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
* @return the {@link OAuth2LoginConfigurer} for further configuration
*/
public OidcLogoutConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
return this;
}
/**
* Configure OIDC Back-Channel Logout using the provided {@link Consumer}
* @return the {@link OidcLogoutConfigurer} for further configuration
*/
public OidcLogoutConfigurer<B> backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
if (this.backChannel == null) {
this.backChannel = new BackChannelLogoutConfigurer();
}
backChannelLogoutConfigurer.customize(this.backChannel);
return this;
}
@Deprecated(forRemoval = true, since = "6.2")
public B and() {
return getBuilder();
}
@Override
public void configure(B builder) throws Exception {
if (this.backChannel != null) {
this.backChannel.configure(builder);
}
}
/**
* A configurer for configuring OIDC Back-Channel Logout
*/
public final class BackChannelLogoutConfigurer {
private AuthenticationConverter authenticationConverter;
private final AuthenticationManager authenticationManager = new ProviderManager(
new OidcBackChannelLogoutAuthenticationProvider());
private Function<B, LogoutHandler> logoutHandler = this::logoutHandler;
private AuthenticationConverter authenticationConverter(B http) {
if (this.authenticationConverter == null) {
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
.getClientRegistrationRepository(http);
this.authenticationConverter = new OidcLogoutAuthenticationConverter(clientRegistrationRepository);
}
return this.authenticationConverter;
}
private AuthenticationManager authenticationManager() {
return this.authenticationManager;
}
private LogoutHandler logoutHandler(B http) {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
return logoutHandler;
}
/**
* Use this endpoint when invoking a back-channel logout.
*
* <p>
* The resulting {@link LogoutHandler} will {@code POST} the session cookie and
* CSRF token to this endpoint to invalidate the corresponding end-user session.
*
* <p>
* Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
* {@code {basePort}}.
*
* <p>
* By default, the URI is set to
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme and
* port of the original back-channel request is preserved, while the host and
* endpoint are changed.
*
* <p>
* If you are using Spring Security for the logout endpoint, the path part of this
* URI should match the value configured there.
*
* <p>
* Otherwise, this is handy in the event that your server configuration means that
* the scheme, server name, or port in the {@code Host} header are different from
* how you would address the same server internally.
* @param logoutUri the URI to request logout on the back-channel
* @return the {@link BackChannelLogoutConfigurer} for further customizations
* @since 6.2.4
*/
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
this.logoutHandler = (http) -> {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
logoutHandler.setLogoutUri(logoutUri);
return logoutHandler;
};
return this;
}
void configure(B http) {
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
authenticationManager());
filter.setLogoutHandler(this.logoutHandler.apply(http));
http.addFilterBefore(filter, CsrfFilter.class);
}
}
}
@@ -52,7 +52,6 @@ import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatchers;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -115,9 +114,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
private Saml2AuthenticationRequestResolver authenticationRequestResolver;
private RequestMatcher loginProcessingUrl = RequestMatchers.anyOf(
new AntPathRequestMatcher(Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI),
new AntPathRequestMatcher("/login/saml2/sso"));
private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
@@ -217,7 +214,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
@Override
public Saml2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");
this.loginProcessingUrl = new AntPathRequestMatcher(loginProcessingUrl);
this.loginProcessingUrl = loginProcessingUrl;
return this;
}
@@ -243,11 +240,12 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
public void init(B http) throws Exception {
registerDefaultCsrfOverride(http);
relyingPartyRegistrationRepository(http);
this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http));
this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http),
this.loginProcessingUrl);
this.saml2WebSsoAuthenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
this.saml2WebSsoAuthenticationFilter.setRequiresAuthenticationRequestMatcher(this.loginProcessingUrl);
setAuthenticationRequestRepository(http, this.saml2WebSsoAuthenticationFilter);
setAuthenticationFilter(this.saml2WebSsoAuthenticationFilter);
super.loginProcessingUrl(this.loginProcessingUrl);
if (StringUtils.hasText(this.loginPage)) {
// Set custom login page
super.loginPage(this.loginPage);
@@ -354,7 +352,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(
this.relyingPartyRegistrationRepository);
converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
converter.setRequestMatcher(this.loginProcessingUrl);
converter.setRequestMatcher(createLoginProcessingUrlMatcher(this.loginProcessingUrl));
return converter;
}
return authenticationConverterBean;
@@ -372,7 +370,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
if (csrf == null) {
return;
}
csrf.ignoringRequestMatchers(this.loginProcessingUrl);
csrf.ignoringRequestMatchers(new AntPathRequestMatcher(this.loginProcessingUrl));
}
private void initDefaultLoginFilter(B http) {
@@ -41,8 +41,6 @@ public class PasswordEncoderParser {
public static final String ATT_HASH = "hash";
static final String ATT_BASE_64 = "base64";
static final String OPT_HASH_BCRYPT = "bcrypt";
private static final Map<String, Class<?>> ENCODER_CLASSES = Collections.singletonMap(OPT_HASH_BCRYPT,
@@ -62,19 +60,17 @@ public class PasswordEncoderParser {
return;
}
String hash = element.getAttribute(ATT_HASH);
boolean useBase64 = StringUtils.hasText(element.getAttribute(ATT_BASE_64))
&& Boolean.parseBoolean(element.getAttribute(ATT_BASE_64));
String ref = element.getAttribute(ATT_REF);
if (StringUtils.hasText(ref)) {
this.passwordEncoder = new RuntimeBeanReference(ref);
}
else {
this.passwordEncoder = createPasswordEncoderBeanDefinition(hash, useBase64);
this.passwordEncoder = createPasswordEncoderBeanDefinition(hash);
((RootBeanDefinition) this.passwordEncoder).setSource(parserContext.extractSource(element));
}
}
public static BeanDefinition createPasswordEncoderBeanDefinition(String hash, boolean useBase64) {
public static BeanDefinition createPasswordEncoderBeanDefinition(String hash) {
Class<?> beanClass = ENCODER_CLASSES.get(hash);
BeanDefinitionBuilder beanBldr = BeanDefinitionBuilder.rootBeanDefinition(beanClass);
return beanBldr.getBeanDefinition();
@@ -68,7 +68,8 @@ public class RsaKeyConversionServicePostProcessor implements BeanFactoryPostProc
return;
}
ConversionService service = beanFactory.getConversionService();
if (service instanceof ConverterRegistry registry) {
if (service instanceof ConverterRegistry) {
ConverterRegistry registry = (ConverterRegistry) service;
registry.addConverter(String.class, RSAPrivateKey.class, this.pkcs8);
registry.addConverter(String.class, RSAPublicKey.class, this.x509);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -421,10 +421,6 @@ final class AuthenticationConfigBuilder {
this.pc.getReaderContext()
.registerWithGeneratedName(new RootBeanDefinition(OAuth2ClientWebMvcSecurityPostProcessor.class));
}
this.pc.getReaderContext()
.getRegistry()
.registerBeanDefinition(OAuth2AuthorizedClientManagerRegistrar.BEAN_NAME,
new RootBeanDefinition(OAuth2AuthorizedClientManagerRegistrar.class));
}
private void createSaml2LoginFilter(BeanReference authenticationManager,
@@ -42,12 +42,19 @@ public final class ChannelAttributeFactory {
}
public static List<ConfigAttribute> createChannelAttributes(String requiredChannel) {
String channelConfigAttribute = switch (requiredChannel) {
case OPT_REQUIRES_HTTPS -> "REQUIRES_SECURE_CHANNEL";
case OPT_REQUIRES_HTTP -> "REQUIRES_INSECURE_CHANNEL";
case OPT_ANY_CHANNEL -> ChannelDecisionManagerImpl.ANY_CHANNEL;
default -> throw new BeanCreationException("Unknown channel attribute " + requiredChannel);
};
String channelConfigAttribute;
if (requiredChannel.equals(OPT_REQUIRES_HTTPS)) {
channelConfigAttribute = "REQUIRES_SECURE_CHANNEL";
}
else if (requiredChannel.equals(OPT_REQUIRES_HTTP)) {
channelConfigAttribute = "REQUIRES_INSECURE_CHANNEL";
}
else if (requiredChannel.equals(OPT_ANY_CHANNEL)) {
channelConfigAttribute = ChannelDecisionManagerImpl.ANY_CHANNEL;
}
else {
throw new BeanCreationException("Unknown channel attribute " + requiredChannel);
}
return SecurityConfig.createList(channelConfigAttribute);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@ import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfLogoutHandler;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
@@ -108,12 +109,13 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
this.requestHandlerRef = element.getAttribute(ATT_REQUEST_HANDLER);
}
if (!StringUtils.hasText(this.csrfRepositoryRef)) {
BeanDefinitionBuilder httpSessionCsrfTokenRepository = BeanDefinitionBuilder
.rootBeanDefinition(HttpSessionCsrfTokenRepository.class);
this.csrfRepositoryRef = pc.getReaderContext()
.generateBeanName(httpSessionCsrfTokenRepository.getBeanDefinition());
pc.registerBeanComponent(new BeanComponentDefinition(httpSessionCsrfTokenRepository.getBeanDefinition(),
this.csrfRepositoryRef));
RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class);
BeanDefinitionBuilder lazyTokenRepository = BeanDefinitionBuilder
.rootBeanDefinition(LazyCsrfTokenRepository.class);
lazyTokenRepository.addConstructorArgValue(csrfTokenRepository);
this.csrfRepositoryRef = pc.getReaderContext().generateBeanName(lazyTokenRepository.getBeanDefinition());
pc.registerBeanComponent(
new BeanComponentDefinition(lazyTokenRepository.getBeanDefinition(), this.csrfRepositoryRef));
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class);
builder.addConstructorArgReference(this.csrfRepositoryRef);
@@ -1,291 +0,0 @@
/*
* 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.http;
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.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
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.core.ResolvableType;
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider;
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.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.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
/**
* A registrar for registering the default {@link OAuth2AuthorizedClientManager} bean
* definition, if not already present.
* <p>
* Note: This class is a direct copy of
* {@link org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerRegistrar}.
*
* @author Joe Grandja
* @author Steve Riesenberg
* @since 6.2.0
*/
final class OAuth2AuthorizedClientManagerRegistrar 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(
AuthorizationCodeOAuth2AuthorizedClientProvider.class,
RefreshTokenOAuth2AuthorizedClientProvider.class,
ClientCredentialsOAuth2AuthorizedClientProvider.class,
PasswordOAuth2AuthorizedClientProvider.class,
JwtBearerOAuth2AuthorizedClientProvider.class
);
// @formatter:on
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
private ListableBeanFactory beanFactory;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (getBeanNamesForType(OAuth2AuthorizedClientManager.class).length != 0
|| getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
return;
}
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(OAuth2AuthorizedClientManager.class)
.setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME)
.getBeanDefinition();
registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry),
beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ListableBeanFactory) beanFactory;
}
private OAuth2AuthorizedClientManager getAuthorizedClientManager() {
ClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils
.beanOfTypeIncludingAncestors(this.beanFactory, ClientRegistrationRepository.class, true, true);
OAuth2AuthorizedClientRepository authorizedClientRepository = BeanFactoryUtils
.beanOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientRepository.class, true, true);
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviderBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientProvider.class, true, true)
.values();
OAuth2AuthorizedClientProvider authorizedClientProvider;
if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) {
authorizedClientProvider = authorizedClientProviderBeans.iterator().next();
}
else {
List<OAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
authorizedClientProviders.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans));
authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans));
OAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider(
authorizedClientProviderBeans);
if (jwtBearerAuthorizedClientProvider != null) {
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
}
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
}
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
Consumer<DefaultOAuth2AuthorizedClientManager> authorizedClientManagerConsumer = getBeanOfType(
ResolvableType.forClassWithGenerics(Consumer.class, DefaultOAuth2AuthorizedClientManager.class));
if (authorizedClientManagerConsumer != null) {
authorizedClientManagerConsumer.accept(authorizedClientManager);
}
return authorizedClientManager;
}
private boolean hasDelegatingAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
if (authorizedClientProviders.size() != 1) {
return false;
}
return authorizedClientProviders.iterator().next() instanceof DelegatingOAuth2AuthorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
AuthorizationCodeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, AuthorizationCodeOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new AuthorizationCodeOAuth2AuthorizedClientProvider();
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, RefreshTokenOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
}
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2RefreshTokenGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, ClientCredentialsOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
}
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2ClientCredentialsGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, PasswordOAuth2AuthorizedClientProvider.class);
if (authorizedClientProvider == null) {
authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
}
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = getBeanOfType(
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
OAuth2PasswordGrantRequest.class));
if (accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private OAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
authorizedClientProviders, JwtBearerOAuth2AuthorizedClientProvider.class);
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = getBeanOfType(ResolvableType
.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, JwtBearerGrantRequest.class));
if (accessTokenResponseClient != null) {
if (authorizedClientProvider == null) {
authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
}
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
}
return authorizedClientProvider;
}
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
authorizedClientProviders);
additionalAuthorizedClientProviders
.removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass()));
return additionalAuthorizedClientProviders;
}
private <T extends OAuth2AuthorizedClientProvider> T getAuthorizedClientProviderByType(
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders, Class<T> providerClass) {
T authorizedClientProvider = null;
for (OAuth2AuthorizedClientProvider current : authorizedClientProviders) {
if (providerClass.isInstance(current)) {
assertAuthorizedClientProviderIsNull(authorizedClientProvider);
authorizedClientProvider = providerClass.cast(current);
}
}
return authorizedClientProvider;
}
private static void assertAuthorizedClientProviderIsNull(OAuth2AuthorizedClientProvider authorizedClientProvider) {
if (authorizedClientProvider != null) {
// @formatter:off
throw new BeanInitializationException(String.format(
"Unable to create an %s bean. Expected one bean of type %s, but found multiple. " +
"Please consider defining only a single bean of this type, or define an %s bean yourself.",
OAuth2AuthorizedClientManager.class.getName(),
authorizedClientProvider.getClass().getName(),
OAuth2AuthorizedClientManager.class.getName()));
// @formatter:on
}
}
private <T> String[] getBeanNamesForType(Class<T> beanClass) {
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, false, false);
}
private <T> T getBeanOfType(ResolvableType resolvableType) {
ObjectProvider<T> objectProvider = this.beanFactory.getBeanProvider(resolvableType, true);
return objectProvider.getIfAvailable();
}
}
@@ -94,7 +94,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
.rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class);
String authorizationRequestResolverRef = (authorizationCodeGrantElt != null)
? authorizationCodeGrantElt.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF) : null;
if (StringUtils.hasLength(authorizationRequestResolverRef)) {
if (!StringUtils.isEmpty(authorizationRequestResolverRef)) {
authorizationRequestRedirectFilterBuilder.addConstructorArgReference(authorizationRequestResolverRef);
}
else {
@@ -125,7 +125,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
private BeanMetadataElement getAuthorizationRequestRepository(Element element) {
String authorizationRequestRepositoryRef = (element != null)
? element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF) : null;
if (StringUtils.hasLength(authorizationRequestRepositoryRef)) {
if (!StringUtils.isEmpty(authorizationRequestRepositoryRef)) {
return new RuntimeBeanReference(authorizationRequestRepositoryRef);
}
return BeanDefinitionBuilder
@@ -147,7 +147,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser {
private BeanMetadataElement getAccessTokenResponseClient(Element element) {
String accessTokenResponseClientRef = (element != null)
? element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF) : null;
if (StringUtils.hasLength(accessTokenResponseClientRef)) {
if (!StringUtils.isEmpty(accessTokenResponseClientRef)) {
return new RuntimeBeanReference(accessTokenResponseClientRef);
}
return BeanDefinitionBuilder
@@ -42,7 +42,7 @@ final class OAuth2ClientBeanDefinitionParserUtils {
static BeanMetadataElement getClientRegistrationRepository(Element element) {
String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF);
if (StringUtils.hasLength(clientRegistrationRepositoryRef)) {
if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) {
return new RuntimeBeanReference(clientRegistrationRepositoryRef);
}
return new RuntimeBeanReference(ClientRegistrationRepository.class);
@@ -50,7 +50,7 @@ final class OAuth2ClientBeanDefinitionParserUtils {
static BeanMetadataElement getAuthorizedClientRepository(Element element) {
String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF);
if (StringUtils.hasLength(authorizedClientRepositoryRef)) {
if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) {
return new RuntimeBeanReference(authorizedClientRepositoryRef);
}
return null;
@@ -58,7 +58,7 @@ final class OAuth2ClientBeanDefinitionParserUtils {
static BeanMetadataElement getAuthorizedClientService(Element element) {
String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF);
if (StringUtils.hasLength(authorizedClientServiceRef)) {
if (!StringUtils.isEmpty(authorizedClientServiceRef)) {
return new RuntimeBeanReference(authorizedClientServiceRef);
}
return null;
@@ -176,7 +176,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
}
Object source = parserContext.extractSource(element);
String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL);
if (StringUtils.hasLength(loginProcessingUrl)) {
if (!StringUtils.isEmpty(loginProcessingUrl)) {
WebConfigUtils.validateHttpRedirect(loginProcessingUrl, parserContext, source);
oauth2LoginAuthenticationFilterBuilder.addConstructorArgValue(loginProcessingUrl);
}
@@ -189,7 +189,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
.addConstructorArgValue(accessTokenResponseClient)
.addConstructorArgValue(oauth2UserService);
String userAuthoritiesMapperRef = element.getAttribute(ATT_USER_AUTHORITIES_MAPPER_REF);
if (StringUtils.hasLength(userAuthoritiesMapperRef)) {
if (!StringUtils.isEmpty(userAuthoritiesMapperRef)) {
oauth2LoginAuthenticationProviderBuilder.addPropertyReference("authoritiesMapper",
userAuthoritiesMapperRef);
}
@@ -199,7 +199,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
BeanDefinitionBuilder oauth2AuthorizationRequestRedirectFilterBuilder = BeanDefinitionBuilder
.rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class);
String authorizationRequestResolverRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF);
if (StringUtils.hasLength(authorizationRequestResolverRef)) {
if (!StringUtils.isEmpty(authorizationRequestResolverRef)) {
oauth2AuthorizationRequestRedirectFilterBuilder.addConstructorArgReference(authorizationRequestResolverRef);
}
else {
@@ -212,7 +212,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
this.oauth2AuthorizationRequestRedirectFilter = oauth2AuthorizationRequestRedirectFilterBuilder
.getBeanDefinition();
String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF);
if (StringUtils.hasLength(authenticationSuccessHandlerRef)) {
if (!StringUtils.isEmpty(authenticationSuccessHandlerRef)) {
oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler",
authenticationSuccessHandlerRef);
}
@@ -225,7 +225,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
successHandlerBuilder.getBeanDefinition());
}
String loginPage = element.getAttribute(ATT_LOGIN_PAGE);
if (StringUtils.hasLength(loginPage)) {
if (!StringUtils.isEmpty(loginPage)) {
WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source);
this.oauth2LoginAuthenticationEntryPoint = BeanDefinitionBuilder
.rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class)
@@ -245,7 +245,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
}
}
String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF);
if (StringUtils.hasLength(authenticationFailureHandlerRef)) {
if (!StringUtils.isEmpty(authenticationFailureHandlerRef)) {
oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler",
authenticationFailureHandlerRef);
}
@@ -269,7 +269,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
private BeanMetadataElement getAuthorizationRequestRepository(Element element) {
String authorizationRequestRepositoryRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF);
if (StringUtils.hasLength(authorizationRequestRepositoryRef)) {
if (!StringUtils.isEmpty(authorizationRequestRepositoryRef)) {
return new RuntimeBeanReference(authorizationRequestRepositoryRef);
}
return BeanDefinitionBuilder
@@ -299,11 +299,11 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
"org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider")
.addConstructorArgValue(accessTokenResponseClient)
.addConstructorArgValue(oidcUserService);
if (StringUtils.hasLength(userAuthoritiesMapperRef)) {
if (!StringUtils.isEmpty(userAuthoritiesMapperRef)) {
oidcAuthProviderBuilder.addPropertyReference("authoritiesMapper", userAuthoritiesMapperRef);
}
String jwtDecoderFactoryRef = element.getAttribute(ATT_JWT_DECODER_FACTORY_REF);
if (StringUtils.hasLength(jwtDecoderFactoryRef)) {
if (!StringUtils.isEmpty(jwtDecoderFactoryRef)) {
oidcAuthProviderBuilder.addPropertyReference("jwtDecoderFactory", jwtDecoderFactoryRef);
}
return oidcAuthProviderBuilder.getBeanDefinition();
@@ -311,7 +311,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
private BeanMetadataElement getOidcUserService(Element element) {
String oidcUserServiceRef = element.getAttribute(ATT_OIDC_USER_SERVICE_REF);
if (StringUtils.hasLength(oidcUserServiceRef)) {
if (!StringUtils.isEmpty(oidcUserServiceRef)) {
return new RuntimeBeanReference(oidcUserServiceRef);
}
return BeanDefinitionBuilder
@@ -321,7 +321,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
private BeanMetadataElement getOAuth2UserService(Element element) {
String oauth2UserServiceRef = element.getAttribute(ATT_USER_SERVICE_REF);
if (StringUtils.hasLength(oauth2UserServiceRef)) {
if (!StringUtils.isEmpty(oauth2UserServiceRef)) {
return new RuntimeBeanReference(oauth2UserServiceRef);
}
return BeanDefinitionBuilder
@@ -331,7 +331,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
private BeanMetadataElement getAccessTokenResponseClient(Element element) {
String accessTokenResponseClientRef = element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF);
if (StringUtils.hasLength(accessTokenResponseClientRef)) {
if (!StringUtils.isEmpty(accessTokenResponseClientRef)) {
return new RuntimeBeanReference(accessTokenResponseClientRef);
}
return BeanDefinitionBuilder
@@ -166,7 +166,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
BeanMetadataElement getAuthenticationManagerResolver(Element element) {
String authenticationManagerResolverRef = element.getAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF);
if (StringUtils.hasLength(authenticationManagerResolverRef)) {
if (!StringUtils.isEmpty(authenticationManagerResolverRef)) {
return new RuntimeBeanReference(authenticationManagerResolverRef);
}
BeanDefinitionBuilder authenticationManagerResolver = BeanDefinitionBuilder
@@ -177,7 +177,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
BeanMetadataElement getBearerTokenResolver(Element element) {
String bearerTokenResolverRef = element.getAttribute(BEARER_TOKEN_RESOLVER_REF);
if (!StringUtils.hasLength(bearerTokenResolverRef)) {
if (StringUtils.isEmpty(bearerTokenResolverRef)) {
return new RootBeanDefinition(DefaultBearerTokenResolver.class);
}
return new RuntimeBeanReference(bearerTokenResolverRef);
@@ -185,7 +185,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
BeanMetadataElement getEntryPoint(Element element) {
String entryPointRef = element.getAttribute(ENTRY_POINT_REF);
if (!StringUtils.hasLength(entryPointRef)) {
if (StringUtils.isEmpty(entryPointRef)) {
return this.authenticationEntryPoint;
}
return new RuntimeBeanReference(entryPointRef);
@@ -224,7 +224,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
Object getDecoder(Element element) {
String decoderRef = element.getAttribute(DECODER_REF);
if (StringUtils.hasLength(decoderRef)) {
if (!StringUtils.isEmpty(decoderRef)) {
return new RuntimeBeanReference(decoderRef);
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder
@@ -235,7 +235,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
Object getJwtAuthenticationConverter(Element element) {
String jwtDecoderRef = element.getAttribute(JWT_AUTHENTICATION_CONVERTER_REF);
return (StringUtils.hasLength(jwtDecoderRef)) ? new RuntimeBeanReference(jwtDecoderRef)
return (!StringUtils.isEmpty(jwtDecoderRef)) ? new RuntimeBeanReference(jwtDecoderRef)
: new JwtAuthenticationConverter();
}
@@ -293,7 +293,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
BeanMetadataElement getIntrospector(Element element) {
String introspectorRef = element.getAttribute(INTROSPECTOR_REF);
if (StringUtils.hasLength(introspectorRef)) {
if (!StringUtils.isEmpty(introspectorRef)) {
return new RuntimeBeanReference(introspectorRef);
}
String introspectionUri = element.getAttribute(INTROSPECTION_URI);
@@ -98,7 +98,7 @@ public class LdapProviderBeanDefinitionParser implements BeanDefinitionParser {
}
else if (StringUtils.hasText(hash)) {
authenticatorBuilder.addPropertyValue("passwordEncoder",
PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash, false));
PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash));
}
}
authenticatorBuilder.addConstructorArgValue(contextSource);
@@ -174,7 +174,7 @@ public final class ClientRegistrationsBeanDefinitionParser implements BeanDefini
if (providers.containsKey(providerId)) {
Map<String, String> provider = providers.get(providerId);
String issuer = provider.get(ATT_ISSUER_URI);
if (StringUtils.hasLength(issuer)) {
if (!StringUtils.isEmpty(issuer)) {
ClientRegistration.Builder builder = ClientRegistrations.fromIssuerLocation(issuer)
.registrationId(registrationId);
return getBuilder(parserContext, builder, provider);
@@ -1,35 +0,0 @@
/*
* 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 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;
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new OidcBackChannelLogoutTokenValidator(clientRegistration));
}
}
@@ -1,66 +0,0 @@
/*
* 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 java.util.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
/**
* An {@link org.springframework.security.core.Authentication} implementation that
* represents the result of authenticating an OIDC Logout token for the purposes of
* performing Back-Channel Logout.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
private final OidcLogoutToken logoutToken;
/**
* Construct an {@link OidcBackChannelLogoutAuthentication}
* @param logoutToken a deserialized, verified OIDC Logout Token
*/
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
super(Collections.emptyList());
this.logoutToken = logoutToken;
setAuthenticated(true);
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getPrincipal() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getCredentials() {
return this.logoutToken;
}
}
@@ -1,108 +0,0 @@
/*
* 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 reactor.core.publisher.Mono;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely
* deserializing it, verifying its signature, and validating its claims.
*
* <p>
* Intended to be included in a
* {@link org.springframework.security.authentication.ProviderManager}
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see org.springframework.security.authentication.ProviderManager
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
final class OidcBackChannelLogoutReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private ReactiveJwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory;
/**
* Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager}
*/
OidcBackChannelLogoutReactiveAuthenticationManager() {
ReactiveOidcIdTokenDecoderFactory logoutTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory());
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Authentication> authenticate(Authentication authentication) throws AuthenticationException {
if (!(authentication instanceof OidcLogoutAuthenticationToken token)) {
return Mono.empty();
}
String logoutToken = token.getLogoutToken();
ClientRegistration registration = token.getClientRegistration();
return decode(registration, logoutToken)
.map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken)
.claims((claims) -> claims.putAll(jwt.getClaims()))
.build())
.map(OidcBackChannelLogoutAuthentication::new);
}
private Mono<Jwt> decode(ClientRegistration registration, String token) {
ReactiveJwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration);
return logoutTokenDecoder.decode(token).onErrorResume(Exception.class, (ex) -> {
if (ex instanceof BadJwtException) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(),
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
return Mono.error(new OAuth2AuthenticationException(error, ex));
}
return Mono.error(new AuthenticationServiceException(ex.getMessage(), ex));
});
}
/**
* Use this {@link ReactiveJwtDecoderFactory} to generate {@link JwtDecoder}s that
* correspond to the {@link ClientRegistration} associated with the OIDC logout token.
* @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use
*/
void setLogoutTokenDecoderFactory(ReactiveJwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory) {
Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null");
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
}
@@ -1,118 +0,0 @@
/*
* 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 java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
* with the OIDC Back-Channel Logout Spec.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
* Token</a>
* @see <a target="blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation">the OIDC
* Back-Channel Logout spec</a>
*/
final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<Jwt> {
private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation";
private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
private final String audience;
private final String issuer;
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
Collection<OAuth2Error> errors = new ArrayList<>();
LogoutTokenClaimAccessor logoutClaims = jwt::getClaims;
Map<String, Object> events = logoutClaims.getEvents();
if (events == null) {
errors.add(invalidLogoutToken("events claim must not be null"));
}
else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) {
errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key"));
}
String issuer = logoutClaims.getIssuer().toExternalForm();
if (issuer == null) {
errors.add(invalidLogoutToken("iss claim must not be null"));
}
else if (!this.issuer.equals(issuer)) {
errors.add(invalidLogoutToken(
"iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`"));
}
List<String> audience = logoutClaims.getAudience();
if (audience == null) {
errors.add(invalidLogoutToken("aud claim must not be null"));
}
else if (!audience.contains(this.audience)) {
errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`"));
}
Instant issuedAt = logoutClaims.getIssuedAt();
if (issuedAt == null) {
errors.add(invalidLogoutToken("iat claim must not be null"));
}
String jwtId = logoutClaims.getId();
if (jwtId == null) {
errors.add(invalidLogoutToken("jti claim must not be null"));
}
if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) {
errors.add(invalidLogoutToken("sub and sid claims must not both be null"));
}
if (logoutClaims.getClaim("nonce") != null) {
errors.add(invalidLogoutToken("nonce claim must not be present"));
}
return OAuth2TokenValidatorResult.failure(errors);
}
private static OAuth2Error invalidLogoutToken(String description) {
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL);
}
}
@@ -1,137 +0,0 @@
/*
* 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 java.nio.charset.StandardCharsets;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
/**
* A filter for the Client-side OIDC Back-Channel Logout endpoint
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
class OidcBackChannelLogoutWebFilter implements WebFilter {
private final Log logger = LogFactory.getLog(getClass());
private final ServerAuthenticationConverter authenticationConverter;
private final ReactiveAuthenticationManager authenticationManager;
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
/**
* Construct an {@link OidcBackChannelLogoutWebFilter}
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
* Logout Token authentication
* @param authenticationManager the {@link AuthenticationManager} for authenticating
* Logout Tokens
*/
OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter,
ReactiveAuthenticationManager authenticationManager) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationConverter = authenticationConverter;
this.authenticationManager = authenticationManager;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.authenticationConverter.convert(exchange).onErrorResume(AuthenticationException.class, (ex) -> {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
})
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap(this.authenticationManager::authenticate)
.onErrorResume(AuthenticationException.class, (ex) -> {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
})
.flatMap((authentication) -> {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return this.logoutHandler.logout(webFilterExchange, authentication);
});
}
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
OAuth2Error error = oauth2Error(ex);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
}
private OAuth2Error oauth2Error(Exception ex) {
if (ex instanceof OAuth2AuthenticationException oauth2) {
return oauth2.getError();
}
return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(),
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
/**
* The strategy for expiring all Client sessions indicated by the logout request.
* Defaults to {@link OidcBackChannelServerLogoutHandler}.
* @param logoutHandler the {@link LogoutHandler} to use
*/
void setLogoutHandler(ServerLogoutHandler logoutHandler) {
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
this.logoutHandler = logoutHandler;
}
}
@@ -1,208 +0,0 @@
/*
* 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 java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A {@link ServerLogoutHandler} that locates the sessions associated with a given OIDC
* Back-Channel Logout Token and invalidates each one.
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
private final Log logger = LogFactory.getLog(getClass());
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
private WebClient web = WebClient.create();
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
private String sessionCookieName = "SESSION";
@Override
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
return Mono.defer(() -> {
if (this.logger.isDebugEnabled()) {
String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type";
this.logger.debug(String.format(message, authentication.getClass().getSimpleName()));
}
return Mono.empty();
});
}
AtomicInteger totalCount = new AtomicInteger(0);
AtomicInteger invalidatedCount = new AtomicInteger(0);
return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> {
totalCount.incrementAndGet();
return eachLogout(exchange, session).flatMap((response) -> {
invalidatedCount.incrementAndGet();
return Mono.empty();
}).onErrorResume((ex) -> {
this.logger.debug("Failed to invalidate session", ex);
return this.sessionRegistry.saveSessionInformation(session).then(Mono.just(ex.getMessage()));
});
}).collectList().flatMap((list) -> {
if (this.logger.isTraceEnabled()) {
this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount.intValue(),
totalCount.intValue()));
}
if (!list.isEmpty()) {
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
}
else {
return Mono.empty();
}
});
}
private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
headers.add(credential.getKey(), credential.getValue());
}
String logout = computeLogoutEndpoint(exchange.getExchange().getRequest());
return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
}
String computeLogoutEndpoint(ServerHttpRequest request) {
// @formatter:off
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
.replacePath(request.getPath().contextPath().value())
.replaceQuery(null)
.fragment(null)
.build();
Map<String, String> uriVariables = new HashMap<>();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", (scheme != null) ? scheme : "");
uriVariables.put("baseUrl", uriComponents.toUriString());
String host = uriComponents.getHost();
uriVariables.put("baseHost", (host != null) ? host : "");
String path = uriComponents.getPath();
uriVariables.put("basePath", (path != null) ? path : "");
int port = uriComponents.getPort();
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
return UriComponentsBuilder.fromUriString(this.logoutUri)
.buildAndExpand(uriVariables)
.toUriString();
// @formatter:on
}
private OAuth2Error oauth2Error(Collection<?> errors) {
return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors,
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
}
/**
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
* this class uses
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
* sessions.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
/**
* Use this {@link WebClient} to perform the per-session back-channel logout
* @param web the {@link WebClient} to use
*/
void setWebClient(WebClient web) {
Assert.notNull(web, "web cannot be null");
this.web = web;
}
/**
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
* since that is the default URI for
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
* @param logoutUri the URI to use
*/
void setLogoutUri(String logoutUri) {
Assert.hasText(logoutUri, "logoutUri cannot be empty");
this.logoutUri = logoutUri;
}
/**
* Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}.
*
* <p>
* Note that if you are using Spring Session, this likely needs to change to SESSION.
* @param sessionCookieName the cookie name to use
*/
void setSessionCookieName(String sessionCookieName) {
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
this.sessionCookieName = sessionCookieName;
}
}
@@ -1,80 +0,0 @@
/*
* 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.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* An {@link org.springframework.security.core.Authentication} instance that represents a
* request to authenticate an OIDC Logout Token.
*
* @author Josh Cummings
* @since 6.2
*/
class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken {
private final String logoutToken;
private final ClientRegistration clientRegistration;
/**
* Construct an {@link OidcLogoutAuthenticationToken}
* @param logoutToken a signed, serialized OIDC Logout token
* @param clientRegistration the {@link ClientRegistration client} associated with
* this token; this is usually derived from material in the logout HTTP request
*/
OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) {
super(AuthorityUtils.NO_AUTHORITIES);
this.logoutToken = logoutToken;
this.clientRegistration = clientRegistration;
}
/**
* {@inheritDoc}
*/
@Override
public String getCredentials() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public String getPrincipal() {
return this.logoutToken;
}
/**
* Get the signed, serialized OIDC Logout token
* @return the logout token
*/
String getLogoutToken() {
return this.logoutToken;
}
/**
* Get the {@link ClientRegistration} associated with this logout token
* @return the {@link ClientRegistration}
*/
ClientRegistration getClientRegistration() {
return this.clientRegistration;
}
}
@@ -1,90 +0,0 @@
/*
* 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication
* request
*
* @author Josh Cummings
* @since 6.2
*/
final class OidcLogoutServerAuthenticationConverter implements ServerAuthenticationConverter {
private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}";
private final Log logger = LogFactory.getLog(getClass());
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private ServerWebExchangeMatcher exchangeMatcher = new PathPatternParserServerWebExchangeMatcher(DEFAULT_LOGOUT_URI,
HttpMethod.POST);
OidcLogoutServerAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return this.exchangeMatcher.matches(exchange)
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap((match) -> {
String registrationId = (String) match.getVariables().get("registrationId");
return this.clientRegistrationRepository.findByRegistrationId(registrationId)
.switchIfEmpty(Mono.error(() -> {
this.logger
.debug("Did not process OIDC Back-Channel Logout since no ClientRegistration was found");
return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}));
})
.flatMap((clientRegistration) -> exchange.getFormData().map((data) -> {
String logoutToken = data.getFirst("logout_token");
return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration);
}).switchIfEmpty(Mono.error(() -> {
this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found");
return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
})));
}
/**
* The logout endpoint. Defaults to
* {@code /logout/connect/back-channel/{registrationId}}.
* @param exchangeMatcher the {@link ServerWebExchangeMatcher} to use
*/
void setExchangeMatcher(ServerWebExchangeMatcher exchangeMatcher) {
Assert.notNull(exchangeMatcher, "exchangeMatcher cannot be null");
this.exchangeMatcher = exchangeMatcher;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -21,7 +21,6 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -29,13 +28,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@@ -58,7 +54,6 @@ import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
@@ -72,9 +67,6 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -111,7 +103,6 @@ import org.springframework.security.oauth2.server.resource.web.access.server.Bea
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.PortMapper;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
@@ -122,7 +113,6 @@ import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.ServerRedirectStrategy;
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;
@@ -157,7 +147,6 @@ import org.springframework.security.web.server.context.SecurityContextServerWebE
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestHandler;
@@ -204,10 +193,8 @@ import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
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.util.pattern.PathPatternParser;
/**
@@ -308,8 +295,6 @@ public class ServerHttpSecurity {
private OAuth2ClientSpec client;
private OidcLogoutSpec oidcLogout;
private LogoutSpec logout = new LogoutSpec();
private LoginPageSpec loginPage = new LoginPageSpec();
@@ -1108,33 +1093,6 @@ public class ServerHttpSecurity {
return this;
}
/**
* Configures OIDC Connect 1.0 Logout support.
*
* <pre class="code">
* &#064;Bean
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
* http
* // ...
* .oidcLogout((logout) -&gt; logout
* .backChannel(Customizer.withDefaults())
* );
* return http.build();
* }
* </pre>
* @param oidcLogoutCustomizer the {@link Customizer} to provide more options for the
* {@link OidcLogoutSpec}
* @return the {@link ServerHttpSecurity} to customize
* @since 6.2
*/
public ServerHttpSecurity oidcLogout(Customizer<OidcLogoutSpec> oidcLogoutCustomizer) {
if (this.oidcLogout == null) {
this.oidcLogout = new OidcLogoutSpec();
}
oidcLogoutCustomizer.customize(this.oidcLogout);
return this;
}
/**
* Configures HTTP Response Headers. The default headers are:
*
@@ -1579,9 +1537,6 @@ public class ServerHttpSecurity {
if (this.resourceServer != null) {
this.resourceServer.configure(this);
}
if (this.oidcLogout != null) {
this.oidcLogout.configure(this);
}
if (this.client != null) {
this.client.configure(this);
}
@@ -3735,8 +3690,6 @@ public class ServerHttpSecurity {
private ServerWebExchangeMatcher authenticationMatcher;
private ReactiveOidcSessionRegistry oidcSessionRegistry;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private ServerAuthenticationFailureHandler authenticationFailureHandler;
@@ -3768,20 +3721,6 @@ public class ServerHttpSecurity {
return this;
}
/**
* Configures the {@link ReactiveOidcSessionRegistry} to use when logins use OIDC.
* Default is to look the value up as a Bean, or else use an
* {@link InMemoryReactiveOidcSessionRegistry}.
* @param oidcSessionRegistry the registry to use
* @return the {@link OidcLogoutSpec} to customize
* @since 6.2
*/
public OAuth2LoginSpec oidcSessionRegistry(ReactiveOidcSessionRegistry oidcSessionRegistry) {
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
this.oidcSessionRegistry = oidcSessionRegistry;
return this;
}
/**
* The {@link ServerAuthenticationSuccessHandler} used after authentication
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}
@@ -3975,11 +3914,8 @@ public class ServerHttpSecurity {
oauthRedirectFilter.setRequestCache(http.requestCache.requestCache);
ReactiveAuthenticationManager manager = getAuthenticationManager();
ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry();
AuthenticationWebFilter authenticationFilter = (sessionRegistry != null)
? new OidcSessionRegistryAuthenticationWebFilter(manager, authorizedClientRepository,
sessionRegistry)
: new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository);
AuthenticationWebFilter authenticationFilter = new OAuth2LoginAuthenticationWebFilter(manager,
authorizedClientRepository);
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
authenticationFilter
.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
@@ -3988,10 +3924,6 @@ public class ServerHttpSecurity {
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
setDefaultEntryPoints(http);
if (sessionRegistry != null) {
http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry),
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
}
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
}
@@ -4036,19 +3968,6 @@ public class ServerHttpSecurity {
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
}
private ReactiveOidcSessionRegistry getOidcSessionRegistry() {
if (ServerHttpSecurity.this.oidcLogout == null && this.oidcSessionRegistry == null) {
return null;
}
if (this.oidcSessionRegistry == null) {
this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class);
}
if (this.oidcSessionRegistry == null) {
this.oidcSessionRegistry = new InMemoryReactiveOidcSessionRegistry();
}
return this.oidcSessionRegistry;
}
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
if (this.authenticationSuccessHandler == null) {
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
@@ -4165,156 +4084,6 @@ public class ServerHttpSecurity {
return new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository());
}
private static final class OidcSessionRegistryWebFilter implements WebFilter {
private final ReactiveOidcSessionRegistry oidcSessionRegistry;
OidcSessionRegistryWebFilter(ReactiveOidcSessionRegistry oidcSessionRegistry) {
this.oidcSessionRegistry = oidcSessionRegistry;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(new OidcSessionRegistryServerWebExchange(exchange));
}
private final class OidcSessionRegistryServerWebExchange extends ServerWebExchangeDecorator {
private final Mono<WebSession> sessionMono;
protected OidcSessionRegistryServerWebExchange(ServerWebExchange delegate) {
super(delegate);
this.sessionMono = delegate.getSession().map(OidcSessionRegistryWebSession::new);
}
@Override
public Mono<WebSession> getSession() {
return this.sessionMono;
}
private final class OidcSessionRegistryWebSession implements WebSession {
private final WebSession session;
OidcSessionRegistryWebSession(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(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
.removeSessionInformation(currentId)
.flatMap((information) -> {
information = information.withSessionId(this.session.getId());
return OidcSessionRegistryWebFilter.this.oidcSessionRegistry
.saveSessionInformation(information);
})));
}
@Override
public Mono<Void> invalidate() {
String currentId = this.session.getId();
return this.session.invalidate()
.then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
.removeSessionInformation(currentId)
.then(Mono.empty())));
}
@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();
}
}
}
}
static final class OidcSessionRegistryAuthenticationWebFilter extends OAuth2LoginAuthenticationWebFilter {
private final Log logger = LogFactory.getLog(getClass());
private final ReactiveOidcSessionRegistry oidcSessionRegistry;
OidcSessionRegistryAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
ReactiveOidcSessionRegistry oidcSessionRegistry) {
super(authenticationManager, authorizedClientRepository);
this.oidcSessionRegistry = oidcSessionRegistry;
}
@Override
protected Mono<Void> onAuthenticationSuccess(Authentication authentication,
WebFilterExchange webFilterExchange) {
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
return super.onAuthenticationSuccess(authentication, webFilterExchange);
}
return webFilterExchange.getExchange().getSession().doOnNext((session) -> {
if (this.logger.isTraceEnabled()) {
this.logger.trace(String.format("Linking a provider [%s] session to this client's session",
user.getIssuer()));
}
}).flatMap((session) -> {
Mono<CsrfToken> csrfToken = webFilterExchange.getExchange().getAttribute(CsrfToken.class.getName());
return (csrfToken != null)
? csrfToken.map((token) -> new OidcSessionInformation(session.getId(),
Map.of(token.getHeaderName(), token.getToken()), user))
: Mono.just(new OidcSessionInformation(session.getId(), Map.of(), user));
})
.flatMap(this.oidcSessionRegistry::saveSessionInformation)
.then(super.onAuthenticationSuccess(authentication, webFilterExchange));
}
}
}
public final class OAuth2ClientSpec {
@@ -4987,167 +4756,6 @@ public class ServerHttpSecurity {
}
/**
* Configures OIDC 1.0 Logout support
*
* @author Josh Cummings
* @since 6.2
*/
public final class OidcLogoutSpec {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
private ReactiveOidcSessionRegistry sessionRegistry;
private BackChannelLogoutConfigurer backChannel;
/**
* Configures the {@link ReactiveClientRegistrationRepository}. Default is to look
* the value up as a Bean.
* @param clientRegistrationRepository the repository to use
* @return the {@link OidcLogoutSpec} to customize
*/
public OidcLogoutSpec clientRegistrationRepository(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
return this;
}
/**
* Configures the {@link ReactiveOidcSessionRegistry}. Default is to use the value
* from {@link OAuth2LoginSpec#oidcSessionRegistry}, then look the value up as a
* Bean, or else use an {@link InMemoryReactiveOidcSessionRegistry}.
* @param sessionRegistry the registry to use
* @return the {@link OidcLogoutSpec} to customize
*/
public OidcLogoutSpec oidcSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
return this;
}
/**
* Configure OIDC Back-Channel Logout using the provided {@link Consumer}
* @return the {@link OidcLogoutSpec} for further configuration
*/
public OidcLogoutSpec backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
if (this.backChannel == null) {
this.backChannel = new OidcLogoutSpec.BackChannelLogoutConfigurer();
}
backChannelLogoutConfigurer.customize(this.backChannel);
return this;
}
@Deprecated(forRemoval = true, since = "6.2")
public ServerHttpSecurity and() {
return ServerHttpSecurity.this;
}
void configure(ServerHttpSecurity http) {
if (this.backChannel != null) {
this.backChannel.configure(http);
}
}
private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
if (this.clientRegistrationRepository == null) {
this.clientRegistrationRepository = getBeanOrNull(ReactiveClientRegistrationRepository.class);
}
return this.clientRegistrationRepository;
}
private ReactiveOidcSessionRegistry getSessionRegistry() {
if (this.sessionRegistry == null && ServerHttpSecurity.this.oauth2Login == null) {
return new InMemoryReactiveOidcSessionRegistry();
}
if (this.sessionRegistry == null) {
return ServerHttpSecurity.this.oauth2Login.oidcSessionRegistry;
}
return this.sessionRegistry;
}
/**
* A configurer for configuring OIDC Back-Channel Logout
*/
public final class BackChannelLogoutConfigurer {
private ServerAuthenticationConverter authenticationConverter;
private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager();
private Supplier<ServerLogoutHandler> logoutHandler = this::logoutHandler;
private ServerAuthenticationConverter authenticationConverter() {
if (this.authenticationConverter == null) {
this.authenticationConverter = new OidcLogoutServerAuthenticationConverter(
OidcLogoutSpec.this.getClientRegistrationRepository());
}
return this.authenticationConverter;
}
private ReactiveAuthenticationManager authenticationManager() {
return this.authenticationManager;
}
private ServerLogoutHandler logoutHandler() {
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
return logoutHandler;
}
/**
* Use this endpoint when invoking a back-channel logout.
*
* <p>
* The resulting {@link LogoutHandler} will {@code POST} the session cookie
* and CSRF token to this endpoint to invalidate the corresponding end-user
* session.
*
* <p>
* Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
* {@code {basePort}}.
*
* <p>
* By default, the URI is set to
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme
* and port of the original back-channel request is preserved, while the host
* and endpoint are changed.
*
* <p>
* If you are using Spring Security for the logout endpoint, the path part of
* this URI should match the value configured there.
*
* <p>
* Otherwise, this is handy in the event that your server configuration means
* that the scheme, server name, or port in the {@code Host} header are
* different from how you would address the same server internally.
* @param logoutUri the URI to request logout on the back-channel
* @return the {@link OidcLogoutConfigurer.BackChannelLogoutConfigurer} for
* further customizations
* @since 6.2.4
*/
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
this.logoutHandler = () -> {
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
logoutHandler.setLogoutUri(logoutUri);
return logoutHandler;
};
return this;
}
void configure(ServerHttpSecurity http) {
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
authenticationManager());
filter.setLogoutHandler(this.logoutHandler.get());
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
}
}
}
/**
* Configures anonymous authentication
*
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,6 @@
package org.springframework.security.config.annotation.web
import jakarta.servlet.Filter
import jakarta.servlet.http.HttpServletRequest
import org.checkerframework.checker.units.qual.C
import org.springframework.context.ApplicationContext
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
@@ -27,6 +24,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.util.matcher.RequestMatcher
import org.springframework.util.ClassUtils
import jakarta.servlet.Filter
import jakarta.servlet.http.HttpServletRequest
/**
* Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl].
@@ -107,36 +107,6 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
return this.http.apply(configurer).apply(configuration)
}
/**
* Applies a [SecurityConfigurerAdapter] to this [HttpSecurity]
*
* Example:
*
* ```
* @Configuration
* @EnableWebSecurity
* class SecurityConfig {
*
* @Bean
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* with(CustomSecurityConfigurer<HttpSecurity>()) {
* customProperty = "..."
* }
* }
* return http.build()
* }
* }
* ```
*
* @param configurer
* the [HttpSecurity] for further customizations
* @since 6.2
*/
fun <C : SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> with(configurer: C, configuration: C.() -> Unit = { }): HttpSecurity? {
return this.http.with(configurer, configuration)
}
/**
* Allows configuring the [HttpSecurity] to only be invoked when matching the
* provided pattern.
@@ -868,38 +838,6 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
}
/**
* Configures OIDC 1.0 logout support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebSecurity
* class SecurityConfig {
*
* @Bean
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* return http.build()
* }
* }
* ```
*
* @param oidcLogoutConfiguration custom configuration to configure the
* OIDC 1.0 logout support
* @see [OidcLogoutDsl]
*/
fun oidcLogout(oidcLogoutConfiguration: OidcLogoutDsl.() -> Unit) {
val oidcLogoutCustomizer = OidcLogoutDsl().apply(oidcLogoutConfiguration).get()
this.http.oidcLogout(oidcLogoutCustomizer)
}
/**
* Configures Remember Me authentication.
*
@@ -1,75 +0,0 @@
/*
* 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.annotation.web
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
import org.springframework.security.config.annotation.web.oauth2.login.OidcBackChannelLogoutDsl
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
/**
* A Kotlin DSL to configure [HttpSecurity] OAuth 1.0 Logout using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@SecurityMarker
class OidcLogoutDsl {
var clientRegistrationRepository: ClientRegistrationRepository? = null
var oidcSessionRegistry: OidcSessionRegistry? = null
private var backChannel: ((OidcLogoutConfigurer<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit)? = null
/**
* Configures the OIDC 1.0 Back-Channel endpoint.
*
* Example:
*
* ```
* @Configuration
* @EnableWebSecurity
* class SecurityConfig {
*
* @Bean
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* return http.build()
* }
* }
* ```
*
* @param backChannelConfig custom configurations to configure the back-channel endpoint
* @see [OidcBackChannelLogoutDsl]
*/
fun backChannel(backChannelConfig: OidcBackChannelLogoutDsl.() -> Unit) {
this.backChannel = OidcBackChannelLogoutDsl().apply(backChannelConfig).get()
}
internal fun get(): (OidcLogoutConfigurer<HttpSecurity>) -> Unit {
return { oidcLogout ->
clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) }
oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) }
backChannel?.also { oidcLogout.backChannel(backChannel) }
}
}
}
@@ -1,34 +0,0 @@
/*
* 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.annotation.web.oauth2.login
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
/**
* A Kotlin DSL to configure the OIDC 1.0 Back-Channel configuration using
* idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@OAuth2LoginSecurityMarker
class OidcBackChannelLogoutDsl {
internal fun get(): (OidcLogoutConfigurer<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit {
return { backChannel -> }
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* 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.
@@ -650,38 +650,6 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
}
/**
* Configures logout support using an OpenID Connect 1.0 Provider.
* A [ReactiveClientRegistrationRepository] is required and must be registered as a Bean or
* configured via [ServerOidcLogoutDsl.clientRegistrationRepository].
*
* Example:
*
* ```
* @Configuration
* @EnableWebFluxSecurity
* class SecurityConfig {
*
* @Bean
* fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
* return http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* }
* }
* ```
*
* @param oidcLogoutConfiguration custom configuration to configure the OIDC 1.0 Logout
* @see [ServerOidcLogoutDsl]
*/
fun oidcLogout(oidcLogoutConfiguration: ServerOidcLogoutDsl.() -> Unit) {
val oidcLogoutCustomizer = ServerOidcLogoutDsl().apply(oidcLogoutConfiguration).get()
this.http.oidcLogout(oidcLogoutCustomizer)
}
/**
* Apply all configurations to the provided [ServerHttpSecurity]
*/
@@ -1,30 +0,0 @@
/*
* 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] OIDC 1.0 Back-Channel Logout support using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@ServerSecurityMarker
class ServerOidcBackChannelLogoutDsl {
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit {
return { backChannel -> }
}
}
@@ -1,71 +0,0 @@
/*
* 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.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
/**
* A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 login using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@ServerSecurityMarker
class ServerOidcLogoutDsl {
var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null
var oidcSessionRegistry: ReactiveOidcSessionRegistry? = null
private var backChannel: ((ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit)? = null
/**
* Enables OIDC 1.0 Back-Channel Logout support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebFluxSecurity
* class SecurityConfig {
*
* @Bean
* fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
* return http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* }
* }
* ```
*
* @param backChannelConfig custom configurations to configure OIDC 1.0 Back-Channel Logout support
* @see [ServerOidcBackChannelLogoutDsl]
*/
fun backChannel(backChannelConfig: ServerOidcBackChannelLogoutDsl.() -> Unit) {
this.backChannel = ServerOidcBackChannelLogoutDsl().apply(backChannelConfig).get()
}
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec) -> Unit {
return { oidcLogout ->
clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) }
oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) }
backChannel?.also { oidcLogout.backChannel(backChannel) }
}
}
}
@@ -1,5 +1,4 @@
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-6.2.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.1.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
http\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd
@@ -22,8 +21,7 @@ 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-6.2.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.1.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
https\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd
@@ -1,318 +0,0 @@
/*
* 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;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.apereo.cas.client.validation.AssertionImpl;
import org.instancio.Instancio;
import org.instancio.InstancioApi;
import org.instancio.Select;
import org.instancio.generator.Generator;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.security.access.intercept.RunAsUserToken;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.jaas.JaasAuthenticationToken;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationToken;
import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens;
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* Tests that Spring Security classes that implements {@link Serializable} and have the
* same serial version as {@link SpringSecurityCoreVersion#SERIAL_VERSION_UID} can be
* deserialized from a previous minor version.
* <p>
* For example, all classes from version 6.2.x that matches the previous requirement
* should be serialized and saved to a folder, and then later on, in 6.3.x, it is verified
* if they can be deserialized
*
* @author Marcus da Coregio
* @since 6.2.2
* @see <a href="https://github.com/spring-projects/spring-security/issues/3737">GitHub
* Issue #3737</a>
*/
class SpringSecurityCoreVersionSerializableTests {
private static final Map<Class<?>, Generator<?>> generatorByClassName = new HashMap<>();
static final long securitySerialVersionUid = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
static Path currentVersionFolder = Paths.get("src/test/resources/serialized/" + getCurrentVersion());
static Path previousVersionFolder = Paths.get("src/test/resources/serialized/" + getPreviousVersion());
static {
ClientRegistration.Builder clientRegistrationBuilder = TestClientRegistrations.clientRegistration();
ClientRegistration clientRegistration = clientRegistrationBuilder.build();
UserDetails user = TestAuthentication.user();
WebAuthenticationDetails details = new WebAuthenticationDetails("remote", "sessionId");
generatorByClassName.put(DefaultOAuth2User.class, (r) -> TestOAuth2Users.create());
generatorByClassName.put(ClientRegistration.class, (r) -> clientRegistration);
generatorByClassName.put(ClientRegistration.ProviderDetails.class,
(r) -> clientRegistration.getProviderDetails());
generatorByClassName.put(ClientRegistration.ProviderDetails.UserInfoEndpoint.class,
(r) -> clientRegistration.getProviderDetails().getUserInfoEndpoint());
generatorByClassName.put(ClientRegistration.Builder.class, (r) -> clientRegistrationBuilder);
generatorByClassName.put(OAuth2AuthorizationRequest.class,
(r) -> TestOAuth2AuthorizationRequests.request().build());
generatorByClassName.put(OAuth2AuthorizationResponse.class,
(r) -> TestOAuth2AuthorizationResponses.success().build());
generatorByClassName.put(OAuth2AuthorizedClient.class,
(r) -> new OAuth2AuthorizedClient(clientRegistration, "principal", TestOAuth2AccessTokens.noScopes()));
generatorByClassName.put(OAuth2UserAuthority.class, (r) -> new OAuth2UserAuthority(Map.of("username", "user")));
generatorByClassName.put(OAuth2AuthorizationExchange.class, (r) -> TestOAuth2AuthorizationExchanges.success());
generatorByClassName.put(OidcUserInfo.class, (r) -> OidcUserInfo.builder().email("email@example.com").build());
generatorByClassName.put(SessionInformation.class,
(r) -> new SessionInformation(user, r.alphanumeric(4), new Date(1704378933936L)));
generatorByClassName.put(OAuth2LoginAuthenticationToken.class, (r) -> {
var token = new OAuth2LoginAuthenticationToken(clientRegistration,
TestOAuth2AuthorizationExchanges.success());
token.setDetails(details);
return token;
});
generatorByClassName.put(OAuth2AuthorizationCodeAuthenticationToken.class, (r) -> {
var token = TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated();
token.setDetails(details);
return token;
});
generatorByClassName
.put(org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken.class, (r) -> {
var token = new org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken(
"token");
token.setDetails(details);
return token;
});
generatorByClassName.put(BearerTokenAuthenticationToken.class, (r) -> {
var token = new BearerTokenAuthenticationToken("token");
token.setDetails(details);
return token;
});
generatorByClassName.put(BearerTokenAuthentication.class, (r) -> {
var token = new BearerTokenAuthentication(TestOAuth2AuthenticatedPrincipals.active(),
TestOAuth2AccessTokens.noScopes(), user.getAuthorities());
token.setDetails(details);
return token;
});
generatorByClassName.put(OAuth2AuthenticationToken.class, (r) -> {
var token = TestOAuth2AuthenticationTokens.authenticated();
token.setDetails(details);
return token;
});
generatorByClassName.put(JwtAuthenticationToken.class, (r) -> {
var token = new JwtAuthenticationToken(TestJwts.user());
token.setDetails(details);
return token;
});
generatorByClassName.put(RunAsUserToken.class, (r) -> {
RunAsUserToken token = new RunAsUserToken("key", user, "creds", user.getAuthorities(),
AnonymousAuthenticationToken.class);
token.setDetails(details);
return token;
});
generatorByClassName.put(CasServiceTicketAuthenticationToken.class, (r) -> {
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("creds");
token.setDetails(details);
return token;
});
generatorByClassName.put(CasAuthenticationToken.class, (r) -> {
var token = new CasAuthenticationToken("key", user, "Password", user.getAuthorities(), user,
new AssertionImpl("test"));
token.setDetails(details);
return token;
});
generatorByClassName.put(CasAssertionAuthenticationToken.class, (r) -> {
var token = new CasAssertionAuthenticationToken(new AssertionImpl("test"), "ticket");
token.setDetails(details);
return token;
});
generatorByClassName.put(RememberMeAuthenticationToken.class, (r) -> {
RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("key", user, user.getAuthorities());
token.setDetails(details);
return token;
});
generatorByClassName.put(PreAuthenticatedAuthenticationToken.class, (r) -> {
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(user, "creds",
user.getAuthorities());
token.setDetails(details);
return token;
});
generatorByClassName.put(UsernamePasswordAuthenticationToken.class, (r) -> {
var token = UsernamePasswordAuthenticationToken.unauthenticated(user, "creds");
token.setDetails(details);
return token;
});
generatorByClassName.put(JaasAuthenticationToken.class, (r) -> {
var token = new JaasAuthenticationToken(user, "creds", null);
token.setDetails(details);
return token;
});
}
@ParameterizedTest
@MethodSource("getClassesToSerialize")
@Disabled("This method should only be used to serialize the classes once")
void serializeCurrentVersionClasses(Class<?> clazz) throws Exception {
Files.createDirectories(currentVersionFolder);
Path filePath = Paths.get(currentVersionFolder.toAbsolutePath() + "/" + clazz.getName() + ".serialized");
File file = filePath.toFile();
if (file.exists()) {
return;
}
Files.createFile(filePath);
Object instance = instancioWithDefaults(clazz).create();
assertThat(instance).isInstanceOf(clazz);
try (FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
objectOutputStream.writeObject(instance);
objectOutputStream.flush();
}
catch (NotSerializableException ex) {
Files.delete(filePath);
fail("Could not serialize " + clazz.getName(), ex);
}
}
@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)) {
Object obj = objectInputStream.readObject();
Class<?> clazz = Class.forName(filePath.getFileName().toString().replace(".serialized", ""));
assertThat(obj).isInstanceOf(clazz);
}
catch (IOException | ClassNotFoundException ex) {
fail("Could not deserialize " + filePath, ex);
}
}
static Stream<Path> getFilesToDeserialize() throws IOException {
assertThat(previousVersionFolder.toFile().exists())
.as("Make sure that the " + previousVersionFolder + " exists and is not empty")
.isTrue();
try (Stream<Path> files = Files.list(previousVersionFolder)) {
if (files.findFirst().isEmpty()) {
fail("Please make sure to run SpringSecurityCoreVersionSerializableTests#serializeCurrentVersionClasses for the "
+ getPreviousVersion() + " version");
}
}
return Files.list(previousVersionFolder);
}
static Stream<Class<?>> getClassesToSerialize() throws Exception {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(Serializable.class));
List<Class<?>> classes = new ArrayList<>();
Set<BeanDefinition> components = provider.findCandidateComponents("org/springframework/security");
for (BeanDefinition component : components) {
Class<?> clazz = Class.forName(component.getBeanClassName());
boolean isAbstract = Modifier.isAbstract(clazz.getModifiers());
boolean matchesExpectedSerialVersion = ObjectStreamClass.lookup(clazz)
.getSerialVersionUID() == securitySerialVersionUid;
if (!isAbstract && matchesExpectedSerialVersion) {
classes.add(clazz);
}
}
return classes.stream();
}
private static InstancioApi<?> instancioWithDefaults(Class<?> clazz) {
InstancioApi<?> instancio = Instancio.of(clazz);
if (generatorByClassName.containsKey(clazz)) {
instancio.supply(Select.all(clazz), generatorByClassName.get(clazz));
}
return instancio;
}
private static String getCurrentVersion() {
String version = System.getProperty("springSecurityVersion");
String[] parts = version.split("\\.");
parts[2] = "x";
return String.join(".", parts);
}
private static String getPreviousVersion() {
String version = System.getProperty("springSecurityVersion");
String[] parts = version.split("\\.");
parts[1] = String.valueOf(Integer.parseInt(parts[1]) - 1);
parts[2] = "x";
return String.join(".", parts);
}
}
@@ -17,13 +17,11 @@
package org.springframework.security.config.annotation.authentication.configurers.provisioning;
import java.util.Arrays;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@@ -75,7 +73,7 @@ public class UserDetailsManagerConfigurerTests {
.authorities(authority)
.build();
// @formatter:on
assertThat((Optional<GrantedAuthority>) userDetails.getAuthorities().stream().findFirst()).contains(authority);
assertThat(userDetails.getAuthorities().stream().findFirst().get()).isEqualTo(authority);
}
@Test
@@ -101,7 +99,7 @@ public class UserDetailsManagerConfigurerTests {
.authorities(Arrays.asList(authority))
.build();
// @formatter:on
assertThat((Optional<GrantedAuthority>) userDetails.getAuthorities().stream().findFirst()).contains(authority);
assertThat(userDetails.getAuthorities().stream().findFirst().get()).isEqualTo(authority);
}
private UserDetailsManagerConfigurer<AuthenticationManagerBuilder, InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder>> configurer() {
@@ -66,7 +66,7 @@ public class SecurityConfig {
@Bean
public AuthenticationProvider authenticationProvider() {
Assert.notNull(this.myUserRepository, "myUserRepository cannot be null");
Assert.notNull(this.myUserRepository);
return new AuthenticationProvider() {
@Override
public boolean supports(Class<?> authentication) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -80,7 +80,7 @@ public class EnableAuthorizationManagerReactiveMethodSecurityTests {
.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
+ ".Publisher (for example, a Mono or Flux) or the function must be a Kotlin coroutine in order to support Reactor Context");
+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
}
@Test
@@ -1,41 +0,0 @@
/*
* 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.annotation.method.configuration;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.core.annotation.AliasFor;
/**
* @author Evgeniy Cheban
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@EnableMethodSecurity
public @interface EnableCustomMethodSecurity {
@AliasFor(annotation = EnableMethodSecurity.class, attribute = "proxyTargetClass")
boolean proxyTargetClass() default false;
@AliasFor(annotation = EnableMethodSecurity.class, attribute = "mode")
AdviceMode mode() default AdviceMode.PROXY;
}
@@ -78,7 +78,7 @@ public class EnableReactiveMethodSecurityTests {
.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
+ ".Publisher (for example, a Mono or Flux) or the function must be a Kotlin coroutine in order to support Reactor Context");
+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
}
@Test
@@ -95,21 +95,6 @@ public class PrePostMethodSecurityConfigurationTests {
@Autowired(required = false)
BusinessService businessService;
@WithMockUser
@Test
public void customMethodSecurityPreAuthorizeAdminWhenRoleUserThenAccessDeniedException() {
this.spring.register(CustomMethodSecurityServiceConfig.class).autowire();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorizeAdmin)
.withMessage("Access Denied");
}
@WithMockUser(roles = "ADMIN")
@Test
public void customMethodSecurityPreAuthorizeAdminWhenRoleAdminThenPasses() {
this.spring.register(CustomMethodSecurityServiceConfig.class).autowire();
this.methodSecurityService.preAuthorizeAdmin();
}
@WithMockUser(roles = "ADMIN")
@Test
public void preAuthorizeWhenRoleAdminThenAccessDeniedException() {
@@ -451,17 +436,6 @@ public class PrePostMethodSecurityConfigurationTests {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
@Configuration
@EnableCustomMethodSecurity
static class CustomMethodSecurityServiceConfig {
@Bean
MethodSecurityService methodSecurityService() {
return new MethodSecurityServiceImpl();
}
}
@Configuration
@EnableMethodSecurity
static class MethodSecurityServiceConfig {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@ import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.SecurityConfigurer;
@@ -150,19 +149,6 @@ public class AbstractConfiguredSecurityBuilderTests {
assertThat(builder.getConfigurers(DelegateSecurityConfigurer.class)).hasSize(2);
}
@Test
public void withWhenConfigurerThenConfigurerAdded() throws Exception {
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
}
@Test
public void withWhenDuplicateConfigurerAddedThenDuplicateConfigurerRemoved() throws Exception {
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
}
private static class ApplyAndRemoveSecurityConfigurer
extends SecurityConfigurerAdapter<Object, TestConfiguredSecurityBuilder> {
@@ -47,7 +47,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
public void requestMatchersWhenPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@@ -55,7 +55,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
public void requestMatchersWhenHttpMethodAndPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@@ -63,7 +63,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
public void requestMatchersWhenHttpMethodAndMvcNotPresentThenReturnAntPathMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET);
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@@ -21,6 +21,7 @@ import java.util.List;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Servlet;
import jakarta.servlet.http.HttpServletMapping;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -82,7 +83,7 @@ public class AbstractRequestMatcherRegistryTests {
List<RequestMatcher> requestMatchers = this.matcherRegistry
.requestMatchers(new RegexRequestMatcher("/a.*", HttpMethod.GET.name()));
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(RegexRequestMatcher.class);
}
@@ -91,7 +92,7 @@ public class AbstractRequestMatcherRegistryTests {
List<RequestMatcher> requestMatchers = this.matcherRegistry
.requestMatchers(new RegexRequestMatcher("/a.*", null));
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(RegexRequestMatcher.class);
}
@@ -100,7 +101,7 @@ public class AbstractRequestMatcherRegistryTests {
List<RequestMatcher> requestMatchers = this.matcherRegistry
.requestMatchers(new AntPathRequestMatcher("/a.*", HttpMethod.GET.name()));
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@@ -108,7 +109,7 @@ public class AbstractRequestMatcherRegistryTests {
public void antMatchersWhenPatternParamThenReturnAntPathRequestMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(new AntPathRequestMatcher("/a.*"));
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@@ -117,7 +118,7 @@ public class AbstractRequestMatcherRegistryTests {
List<RequestMatcher> requestMatchers = this.matcherRegistry.dispatcherTypeMatchers(HttpMethod.GET,
DispatcherType.ASYNC);
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(DispatcherTypeRequestMatcher.class);
}
@@ -125,7 +126,7 @@ public class AbstractRequestMatcherRegistryTests {
public void dispatcherMatchersWhenPatternParamThenReturnAntPathRequestMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.dispatcherTypeMatchers(DispatcherType.INCLUDE);
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(DispatcherTypeRequestMatcher.class);
}
@@ -133,7 +134,7 @@ public class AbstractRequestMatcherRegistryTests {
public void requestMatchersWhenPatternAndMvcPresentThenReturnMvcRequestMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
}
@@ -141,7 +142,7 @@ public class AbstractRequestMatcherRegistryTests {
public void requestMatchersWhenHttpMethodAndPatternAndMvcPresentThenReturnMvcRequestMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
}
@@ -149,7 +150,7 @@ public class AbstractRequestMatcherRegistryTests {
public void requestMatchersWhenHttpMethodAndMvcPresentThenReturnMvcRequestMatcherType() {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET);
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
}
@@ -227,6 +228,7 @@ public class AbstractRequestMatcherRegistryTests {
@Test
public void requestMatchersWhenOnlyDispatcherServletThenAllows() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
@@ -250,6 +252,7 @@ public class AbstractRequestMatcherRegistryTests {
@Test
public void requestMatchersWhenPathBasedNonDispatcherServletThenAllows() {
mockMvcIntrospector(true);
MockServletContext servletContext = new MockServletContext();
given(this.context.getServletContext()).willReturn(servletContext);
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
@@ -257,10 +260,19 @@ public class AbstractRequestMatcherRegistryTests {
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/services/*");
assertThat(requestMatchers).hasSize(1);
assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint");
request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping());
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.defaultMapping();
}
};
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/services"));
request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.path(this, "/services");
}
};
request.setServletPath("/services");
request.setPathInfo("/endpoint");
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
@@ -275,12 +287,21 @@ public class AbstractRequestMatcherRegistryTests {
AntPathRequestMatcher ant = mock(AntPathRequestMatcher.class);
DispatcherServletDelegatingRequestMatcher requestMatcher = new DispatcherServletDelegatingRequestMatcher(ant,
mvc, servletContext);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint");
request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping());
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.defaultMapping();
}
};
assertThat(requestMatcher.matches(request)).isFalse();
verify(mvc).matches(request);
verifyNoInteractions(ant);
request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/services"));
request = new MockHttpServletRequest("GET", "/services/endpoint") {
@Override
public HttpServletMapping getHttpServletMapping() {
return TestMockHttpServletMappings.path(this, "/services");
}
};
assertThat(requestMatcher.matches(request)).isFalse();
verify(ant).matches(request);
verifyNoMoreInteractions(mvc);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,7 +47,6 @@ 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.config.Customizer;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -64,11 +63,9 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -76,10 +73,6 @@ import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -92,8 +85,6 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -361,40 +352,6 @@ public class HttpSecurityConfigurationTests {
DefaultLogoutPageGeneratingFilter.class);
}
@Test
public void configureWhenCorsConfigurationSourceThenApplyCors() {
this.spring.register(CorsConfigurationSourceConfig.class, DefaultWithFilterChainConfig.class).autowire();
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
CorsFilter corsFilter = (CorsFilter) filterChain.getFilters()
.stream()
.filter((f) -> f instanceof CorsFilter)
.findFirst()
.get();
Object configSource = ReflectionTestUtils.getField(corsFilter, "configSource");
assertThat(configSource).isInstanceOf(UrlBasedCorsConfigurationSource.class);
}
@Test
public void configureWhenAddingCustomDslUsingWithThenApplied() throws Exception {
this.spring.register(WithCustomDslConfig.class, UserDetailsConfig.class).autowire();
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
List<Filter> filters = filterChain.getFilters();
assertThat(filters).hasAtLeastOneElementOfType(UsernamePasswordAuthenticationFilter.class);
this.mockMvc.perform(formLogin()).andExpectAll(redirectedUrl("/"), authenticated());
}
@Test
public void configureWhenCustomDslAddedFromFactoriesAndDisablingUsingWithThenNotApplied() throws Exception {
this.springFactoriesLoader
.when(() -> SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, getClass().getClassLoader()))
.thenReturn(List.of(new WithCustomDsl()));
this.spring.register(WithCustomDslDisabledConfig.class, UserDetailsConfig.class).autowire();
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
List<Filter> filters = filterChain.getFilters();
assertThat(filters).doesNotHaveAnyElementsOfTypes(UsernamePasswordAuthenticationFilter.class);
this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated());
}
@RestController
static class NameController {
@@ -659,20 +616,6 @@ public class HttpSecurityConfigurationTests {
}
@Configuration
static class CorsConfigurationSourceConfig {
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(List.of("http://localhost:8080"));
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
static class DefaultConfigurer extends AbstractHttpConfigurer<DefaultConfigurer, HttpSecurity> {
boolean init;
@@ -691,45 +634,4 @@ public class HttpSecurityConfigurationTests {
}
@Configuration
@EnableWebSecurity
static class WithCustomDslConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.with(new WithCustomDsl(), Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
static class WithCustomDslDisabledConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.with(new WithCustomDsl(), (dsl) -> dsl.disable())
.httpBasic(Customizer.withDefaults());
// @formatter:on
return http.build();
}
}
static class WithCustomDsl extends AbstractHttpConfigurer<WithCustomDsl, HttpSecurity> {
@Override
public void init(HttpSecurity builder) throws Exception {
builder.formLogin(Customizer.withDefaults());
}
}
}
@@ -1,547 +0,0 @@
/*
* 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.annotation.web.configuration;
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 jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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;
import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider;
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.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
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.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
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.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.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;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
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 OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerConfiguration}.
*
* @author Joe Grandja
* @author Steve Riesenberg
*/
public class OAuth2AuthorizedClientManagerConfigurationTests {
private static OAuth2AccessTokenResponseClient<? super AbstractOAuth2AuthorizationGrantRequest> MOCK_RESPONSE_CLIENT;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Autowired
private OAuth2AuthorizedClientRepository authorizedClientRepository;
@Autowired(required = false)
private AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
@BeforeEach
@SuppressWarnings("unchecked")
public void setUp() {
MOCK_RESPONSE_CLIENT = mock(OAuth2AccessTokenResponseClient.class);
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
}
@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);
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("google")
.principal(authentication)
.attribute(HttpServletRequest.class.getName(), this.request)
.attribute(HttpServletResponse.class.getName(), this.response)
.build();
assertThatExceptionOfType(ClientAuthorizationRequiredException.class)
.isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest))
.extracting(OAuth2AuthorizationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo("client_authorization_required");
// @formatter:on
verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class));
}
@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(accessTokenResponse);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null);
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google");
OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken());
this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.request,
this.response);
// @formatter:off
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withAuthorizedClient(existingAuthorizedClient)
.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<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(accessTokenResponse);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null);
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github");
// @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<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(accessTokenResponse);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook");
// @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
this.request.setParameter(OAuth2ParameterNames.USERNAME, "user");
this.request.setParameter(OAuth2ParameterNames.PASSWORD, "password");
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
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(accessTokenResponse);
JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt());
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta");
// @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<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");
}
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
@EnableWebSecurity
static class MinimalOAuth2ClientConfig extends OAuth2ClientBaseConfig {
}
@Configuration
@EnableWebSecurity
static class CustomAccessTokenResponseClientsConfig extends OAuth2ClientBaseConfig {
@Bean
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
return new MockAuthorizationCodeClient();
}
@Bean
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient() {
return new MockRefreshTokenClient();
}
@Bean
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient() {
return new MockClientCredentialsClient();
}
@Bean
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient() {
return new MockPasswordClient();
}
@Bean
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerTokenResponseClient() {
return new MockJwtBearerClient();
}
@Bean
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
return mock(DefaultOAuth2UserService.class);
}
@Bean
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
return mock(OidcUserService.class);
}
}
@Configuration
@EnableWebSecurity
static class CustomAuthorizedClientProvidersConfig extends OAuth2ClientBaseConfig {
@Bean
AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeProvider() {
return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider());
}
@Bean
RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider() {
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockRefreshTokenClient());
return authorizedClientProvider;
}
@Bean
ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockClientCredentialsClient());
return authorizedClientProvider;
}
@Bean
PasswordOAuth2AuthorizedClientProvider passwordProvider() {
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockPasswordClient());
return authorizedClientProvider;
}
@Bean
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() {
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
authorizedClientProvider.setAccessTokenResponseClient(new MockJwtBearerClient());
return authorizedClientProvider;
}
}
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(
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()));
// @formatter:on
}
@Bean
OAuth2AuthorizedClientRepository authorizedClientRepository() {
return mock(OAuth2AuthorizedClientRepository.class);
}
@Bean
Consumer<DefaultOAuth2AuthorizedClientManager> authorizedClientManagerConsumer() {
return (authorizedClientManager) -> authorizedClientManager
.setContextAttributesMapper((authorizeRequest) -> {
HttpServletRequest request = Objects
.requireNonNull(authorizeRequest.getAttribute(HttpServletRequest.class.getName()));
String username = request.getParameter(OAuth2ParameterNames.USERNAME);
String password = request.getParameter(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 MockAuthorizationCodeClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
@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) {
return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest);
}
}
}
@@ -180,10 +180,9 @@ public class OAuth2ClientConfigurationTests {
@Test
public void loadContextWhenAccessTokenResponseClientRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() {
// @formatter:off
assertThatExceptionOfType(BeanCreationException.class)
assertThatExceptionOfType(Exception.class)
.isThrownBy(() -> this.spring.register(AccessTokenResponseClientRegisteredTwiceConfig.class).autowire())
.havingRootCause()
.isInstanceOf(NoUniqueBeanDefinitionException.class)
.withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class)
.withMessageContaining(
"expected single matching bean but found 2: accessTokenResponseClient1,accessTokenResponseClient2");
// @formatter:on
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,20 +17,14 @@
package org.springframework.security.config.annotation.web.configuration;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnJre;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.BaseSubscriber;
@@ -41,8 +35,6 @@ import reactor.util.context.Context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -54,7 +46,6 @@ import org.springframework.security.config.annotation.web.configuration.Security
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.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.oauth2.client.web.reactive.function.client.MockExchangeFunction;
@@ -280,58 +271,6 @@ public class SecurityReactorContextConfigurationTests {
verify(strategy, times(2)).getContext();
}
@Test
public void createPublisherWhenThreadFactoryIsPlatformThenSecurityContextAttributesAvailable() throws Exception {
this.spring.register(SecurityConfig.class).autowire();
ThreadFactory threadFactory = Executors.defaultThreadFactory();
assertContextAttributesAvailable(threadFactory);
}
@Test
@DisabledOnJre(JRE.JAVA_17)
public void createPublisherWhenThreadFactoryIsVirtualThenSecurityContextAttributesAvailable() throws Exception {
this.spring.register(SecurityConfig.class).autowire();
ThreadFactory threadFactory = new VirtualThreadTaskExecutor().getVirtualThreadFactory();
assertContextAttributesAvailable(threadFactory);
}
private void assertContextAttributesAvailable(ThreadFactory threadFactory) throws Exception {
Map<Object, Object> expectedContextAttributes = new HashMap<>();
expectedContextAttributes.put(HttpServletRequest.class, this.servletRequest);
expectedContextAttributes.put(HttpServletResponse.class, this.servletResponse);
expectedContextAttributes.put(Authentication.class, this.authentication);
try (SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(threadFactory)) {
Future<Map<Object, Object>> future = taskExecutor.submit(this::propagateRequestAttributes);
assertThat(future.get()).isEqualTo(expectedContextAttributes);
}
}
private Map<Object, Object> propagateRequestAttributes() {
RequestAttributes requestAttributes = new ServletRequestAttributes(this.servletRequest, this.servletResponse);
RequestContextHolder.setRequestAttributes(requestAttributes);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(this.authentication);
SecurityContextHolder.setContext(securityContext);
// @formatter:off
return Mono.deferContextual(Mono::just)
.filter((ctx) -> ctx.hasKey(SecurityReactorContextSubscriber.SECURITY_CONTEXT_ATTRIBUTES))
.map((ctx) -> ctx.<Map<Object, Object>>get(SecurityReactorContextSubscriber.SECURITY_CONTEXT_ATTRIBUTES))
.map((attributes) -> {
Map<Object, Object> map = new HashMap<>();
// Copy over items from lazily loaded map
Arrays.asList(HttpServletRequest.class, HttpServletResponse.class, Authentication.class)
.forEach((key) -> map.put(key, attributes.get(key)));
return map;
})
.block();
// @formatter:on
}
@Configuration
@EnableWebSecurity
static class SecurityConfig {
@@ -37,7 +37,6 @@ import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.authority.AuthorityUtils;
@@ -477,43 +476,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.mvc.perform(requestWithRoleOther).andExpect(status().isForbidden());
}
@Test
public void getWhenCustomRolePrefixAndRoleHasDifferentPrefixThenRespondsWithForbidden() throws Exception {
this.spring.register(GrantedAuthorityDefaultHasRoleConfig.class, BasicController.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder requestWithUser = get("/")
.with(user("user")
.authorities(new SimpleGrantedAuthority("ROLE_USER")));
// @formatter:on
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
}
@Test
public void getWhenCustomRolePrefixAndHasRoleThenRespondsWithOk() throws Exception {
this.spring.register(GrantedAuthorityDefaultHasRoleConfig.class, BasicController.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder requestWithUser = get("/")
.with(user("user")
.authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_USER")));
// @formatter:on
this.mvc.perform(requestWithUser).andExpect(status().isOk());
}
@Test
public void getWhenCustomRolePrefixAndHasAnyRoleThenRespondsWithOk() throws Exception {
this.spring.register(GrantedAuthorityDefaultHasAnyRoleConfig.class, BasicController.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder requestWithUser = get("/")
.with(user("user")
.authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_USER")));
MockHttpServletRequestBuilder requestWithAdmin = get("/")
.with(user("user")
.authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_ADMIN")));
// @formatter:on
this.mvc.perform(requestWithUser).andExpect(status().isOk());
this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
}
@Test
public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsLocalhostThenRespondsWithOk()
throws Exception {
@@ -596,38 +558,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
}
@Configuration
@EnableWebSecurity
static class GrantedAuthorityDefaultHasRoleConfig {
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("CUSTOM_PREFIX_");
}
@Bean
SecurityFilterChain myFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests((c) -> c.anyRequest().hasRole("USER")).build();
}
}
@Configuration
@EnableWebSecurity
static class GrantedAuthorityDefaultHasAnyRoleConfig {
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("CUSTOM_PREFIX_");
}
@Bean
SecurityFilterChain myFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests((c) -> c.anyRequest().hasAnyRole("USER", "ADMIN")).build();
}
}
@Configuration
@EnableWebSecurity
static class NoRequestsConfig {
@@ -85,10 +85,10 @@ public class DefaultFiltersTests {
List<SecurityFilterChain> filterChains = this.spring.getContext()
.getBean(FilterChainProxy.class)
.getFilterChains();
assertThat(filterChains).hasSize(1);
assertThat(filterChains.size()).isEqualTo(1);
DefaultSecurityFilterChain filterChain = (DefaultSecurityFilterChain) filterChains.get(0);
assertThat(filterChain.getRequestMatcher()).isInstanceOf(AnyRequestMatcher.class);
assertThat(filterChain.getFilters()).hasSize(1);
assertThat(filterChain.getFilters().size()).isEqualTo(1);
long filter = filterChain.getFilters()
.stream()
.filter((it) -> it instanceof UsernamePasswordAuthenticationFilter)
@@ -102,25 +102,25 @@ public class DefaultFiltersTests {
List<SecurityFilterChain> filterChains = this.spring.getContext()
.getBean(FilterChainProxy.class)
.getFilterChains();
assertThat(filterChains).hasSize(2);
assertThat(filterChains.size()).isEqualTo(2);
DefaultSecurityFilterChain firstFilter = (DefaultSecurityFilterChain) filterChains.get(0);
DefaultSecurityFilterChain secondFilter = (DefaultSecurityFilterChain) filterChains.get(1);
assertThat(firstFilter.getFilters().isEmpty()).isEqualTo(true);
assertThat(secondFilter.getRequestMatcher()).isInstanceOf(AnyRequestMatcher.class);
List<Class<? extends Filter>> classes = secondFilter.getFilters()
List<? extends Class<? extends Filter>> classes = secondFilter.getFilters()
.stream()
.map(Filter::getClass)
.collect(Collectors.toList());
assertThat(classes).contains(WebAsyncManagerIntegrationFilter.class);
assertThat(classes).contains(SecurityContextHolderFilter.class);
assertThat(classes).contains(HeaderWriterFilter.class);
assertThat(classes).contains(LogoutFilter.class);
assertThat(classes).contains(CsrfFilter.class);
assertThat(classes).contains(RequestCacheAwareFilter.class);
assertThat(classes).contains(SecurityContextHolderAwareRequestFilter.class);
assertThat(classes).contains(AnonymousAuthenticationFilter.class);
assertThat(classes).contains(ExceptionTranslationFilter.class);
assertThat(classes).contains(FilterSecurityInterceptor.class);
assertThat(classes.contains(WebAsyncManagerIntegrationFilter.class)).isTrue();
assertThat(classes.contains(SecurityContextHolderFilter.class)).isTrue();
assertThat(classes.contains(HeaderWriterFilter.class)).isTrue();
assertThat(classes.contains(LogoutFilter.class)).isTrue();
assertThat(classes.contains(CsrfFilter.class)).isTrue();
assertThat(classes.contains(RequestCacheAwareFilter.class)).isTrue();
assertThat(classes.contains(SecurityContextHolderAwareRequestFilter.class)).isTrue();
assertThat(classes.contains(AnonymousAuthenticationFilter.class)).isTrue();
assertThat(classes.contains(ExceptionTranslationFilter.class)).isTrue();
assertThat(classes.contains(FilterSecurityInterceptor.class)).isTrue();
}
@Test

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