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

Compare commits

..

79 Commits

Author SHA1 Message Date
Rob Winch 1a6915d3c0 rm .github/workflows for unsupported branch 2025-05-02 12:35:31 -05:00
Rob Winch 8325728035 rm merge-dependabot-pr.yml from Unsupported Branch 2025-04-25 13:16:44 -05:00
Josh Cummings 643a3f1206 Test Setting logoutRequestRepository
Issue gh-16093
2024-12-19 08:55:18 -07:00
Steven Williams 7aafe2ed5a Set Saml2RelyingPartyInitiatedLogoutSuccessHandler#logoutRequestRepository
Closes gh-16093
2024-12-19 08:53:02 -07:00
dependabot[bot] e528923878 Bump io.projectreactor:reactor-bom from 2023.0.12 to 2023.0.13
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.12 to 2023.0.13.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2023.0.12...2023.0.13)

---
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-12-10 19:18:45 -08:00
Josh Cummings 2fe7faea87 Merge branch '5.8.x' into 6.2.x 2024-12-09 15:27:06 -07:00
Josh Cummings eb313ec901 Link to Messaging SpEL Migration Details
Issue gh-12650
2024-12-09 15:26:54 -07:00
Josh Cummings 348f064df1 Merge branch '5.8.x' into 6.2.x 2024-12-09 15:25:50 -07:00
Josh Cummings 8b9fe13c88 Document Messaging SpEL Migration
Issue gh-12650
2024-12-09 15:25:33 -07:00
dependabot[bot] cac03995a3 Bump @antora/collector-extension from 1.0.0 to 1.0.1 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 17:07:09 -08:00
dependabot[bot] 6ea7da5178 Bump antora from 3.2.0-alpha.6 to 3.2.0-alpha.8 in /docs
Bumps [antora](https://gitlab.com/antora/antora) from 3.2.0-alpha.6 to 3.2.0-alpha.8.
- [Changelog](https://gitlab.com/antora/antora/blob/main/CHANGELOG.adoc)
- [Commits](https://gitlab.com/antora/antora/compare/v3.2.0-alpha.6...v3.2.0-alpha.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 17:06:45 -08:00
dependabot[bot] 16272f634c Bump @antora/collector-extension from 1.0.0 to 1.0.1 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 16:48:56 -08:00
dependabot[bot] 0b9887505e Bump antora from 3.2.0-alpha.6 to 3.2.0-alpha.8 in /docs
Bumps [antora](https://gitlab.com/antora/antora) from 3.2.0-alpha.6 to 3.2.0-alpha.8.
- [Changelog](https://gitlab.com/antora/antora/blob/main/CHANGELOG.adoc)
- [Commits](https://gitlab.com/antora/antora/compare/v3.2.0-alpha.6...v3.2.0-alpha.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 16:48:02 -08:00
Josh Cummings 87de6cea1b Use Reactive JSON Encoder
Closes gh-16177
2024-12-06 15:14:07 -07:00
Josh Cummings 3d1e4b5f18 Polish Tests
Confirm that responses are a valid JSON map

Issue gh-16177
2024-12-06 15:14:07 -07:00
Josh Cummings 96a9cf0d2d Restore Previous Behavior for Servlet 5
Closes gh-16173
2024-12-05 09:52:06 -07:00
dependabot[bot] a6c3d123ed Bump @antora/collector-extension from 1.0.0-rc.1 to 1.0.0 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 17:08:47 -08:00
dependabot[bot] e79ceaeb75 Bump @antora/collector-extension from 1.0.0-rc.1 to 1.0.0 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 16:27:59 -08:00
Harpreet Singh 68d91916e2 Polish Dark Mode for CSRF and Method Security diagrams
Closes gh-16151
2024-11-26 01:53:56 +05:30
dependabot[bot] 1dbaa08cd4 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-24 16:48:27 -08:00
dependabot[bot] 1d916c35d1 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-24 16:27:14 -08:00
Steve Riesenberg 0eb6acde96 Polish gh-16133 2024-11-20 15:50:29 -06:00
Kai Zander 73f3f75712 Always return current ClientRegistration in loadAuthorizedClient
This changes `InMemoryOAuth2AuthorizedClientService.loadAuthorizedClient`
(and its reactive counterpart) to always return `OAuth2AuthorizedClient`
instances containing the current `ClientRegistration` as obtained from
the `ClientRegistrationRepository`.

Before this change, the first `ClientRegistration` instance was cached,
with the effect that any changes made in the `ClientRegistrationRepository`
(such as a new client secret) would not have taken effect.

Closes gh-15511
2024-11-20 15:50:29 -06:00
Steve Riesenberg b8e9f47dd4 Merge branch '5.8.x' into 6.2.x 2024-11-18 16:19:06 -06:00
github-actions[bot] 9363959566 Update Antora Spring UI to v0.4.18 2024-11-18 16:18:28 -06:00
github-actions[bot] f38129b5a0 Update Antora Spring UI to v0.4.18 2024-11-18 16:18:27 -06:00
Steve Riesenberg 7d55c079c9 Merge branch '5.8.x' into 6.2.x 2024-11-18 16:04:27 -06:00
Steve Riesenberg e44fc3817a Merge branch '5.7.x' into 5.8.x 2024-11-18 16:04:13 -06:00
Josh Cummings ba520db7f7 Merge branch '5.8.x' into 6.2.x 2024-11-18 12:12:46 -07:00
Josh Cummings 85248083c0 Add Dark Mode CSS Style
Closes gh-14834
2024-11-18 12:10:48 -07:00
Josh Cummings be09ed7e2b Merge branch '5.8.x' into 6.2.x 2024-11-18 10:40:07 -07:00
Joe Grandja d224dbe334 Next development version 2024-11-18 12:14:18 -05:00
github-actions[bot] c62168ca5b Next development version 2024-11-18 17:12:34 +00:00
github-actions[bot] d0b2b33dce Release 5.7.14 2024-11-18 16:36:56 +00:00
github-actions[bot] 7881660ca0 Next development version 2024-11-18 16:01:43 +00:00
github-actions[bot] 506e5b7f11 Release 5.8.16 2024-11-18 15:23:30 +00:00
github-actions[bot] a5cd7ce122 Release 6.2.8 2024-11-18 15:23:13 +00:00
Joe Grandja a8c4d6cead Require Locale argument for toLower/toUpperCase usage 2024-11-18 04:22:26 -05:00
Joe Grandja a7bf8f7cc6 Require Locale argument for toLower/toUpperCase usage 2024-11-18 04:03:07 -05:00
dependabot[bot] 5f838b0e93 Bump io.micrometer:micrometer-observation from 1.12.12 to 1.12.13
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.12.12 to 1.12.13.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.12...v1.12.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 19:53:46 -08:00
dependabot[bot] bc3e6f2919 Bump org.springframework.data:spring-data-bom
Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.11 to 2023.1.12.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.11...2023.1.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 19:51:06 -08:00
dependabot[bot] 451fbf0227 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 16:59:48 -08:00
dependabot[bot] 810d83e2f8 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 16:59:03 -08:00
Joe Grandja 0eaffb37e7 Require Locale argument for toLower/toUpperCase usage 2024-11-15 11:01:23 -05:00
dependabot[bot] 127ed4b7cf Bump org.springframework:spring-framework-bom from 6.1.14 to 6.1.15
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.14 to 6.1.15.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.14...v6.1.15)

---
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-11-14 20:08:07 -08:00
dependabot[bot] da345a3bac Bump org.springframework.ldap:spring-ldap-core from 3.2.7 to 3.2.8
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 3.2.7 to 3.2.8.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/3.2.7...3.2.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 19:56:04 -08:00
dependabot[bot] d985d044bc Bump org.springframework.ldap:spring-ldap-core from 2.4.2 to 2.4.4
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 2.4.2 to 2.4.4.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/2.4.2...2.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 19:40:38 -08:00
dependabot[bot] 0790978590 Bump io.projectreactor:reactor-bom from 2023.0.11 to 2023.0.12
Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.11 to 2023.0.12.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2023.0.11...2023.0.12)

---
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-11-12 20:04:22 -08:00
dependabot[bot] 0d8b8ee04b Bump io.micrometer:micrometer-observation from 1.12.11 to 1.12.12
Bumps [io.micrometer:micrometer-observation](https://github.com/micrometer-metrics/micrometer) from 1.12.11 to 1.12.12.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.11...v1.12.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 20:04:38 -08:00
Rob Winch 81e74e65d4 Support ServerExchangeRejectedHandler @Bean
Closes gh-16061
2024-11-11 14:58:00 -06:00
Rob Winch c24b5ebe98 Support ServerExchangeRejectedHandler @Bean
Closes gh-15975
2024-11-11 14:42:19 -06:00
Rob Winch fd900c288e checkExpectedBranchVersion trim version 2024-11-11 14:42:18 -06:00
Rob Winch e86d88d0cf Support ServerExchangeRejectedHandler @Bean
Closes gh-15975
2024-11-08 14:48:40 -06:00
Steve Riesenberg 86f3cd6dc7 Polish gh-15940
Closes gh-15885
2024-11-04 13:34:12 -06:00
Hyeongi Jeong 4c6fef82b9 Fix error when Bearer token is requested with empty string
Issue gh-15885
2024-11-04 13:33:58 -06:00
dependabot[bot] 18129f3af3 Bump org.hsqldb:hsqldb from 2.7.3 to 2.7.4
Bumps org.hsqldb:hsqldb from 2.7.3 to 2.7.4.

---
updated-dependencies:
- dependency-name: org.hsqldb:hsqldb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 20:01:04 -08:00
dependabot[bot] f28c26fd54 Bump org.hsqldb:hsqldb from 2.7.3 to 2.7.4
Bumps org.hsqldb:hsqldb from 2.7.3 to 2.7.4.

---
updated-dependencies:
- dependency-name: org.hsqldb:hsqldb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 19:47:57 -08:00
dependabot[bot] 421430330a Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 16:34:33 -08:00
dependabot[bot] fed3c99c1d Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 16:24:16 -08:00
Rob Winch 5c2106b22e Format 2024-10-25 12:24:54 -05:00
Rob Winch 1ba6301afa Support ServerWebExchangeFirewall @Bean
Closes gh-15987
2024-10-25 12:13:41 -05:00
Rob Winch 48241deba3 Merge branch '5.7.x' into 5.8.x
Support ServerWebExchangeFirewall @Bean

Closes gh-15977
2024-10-25 12:11:57 -05:00
Rob Winch 9cc11be9f3 Merge branch '5.7.x' (early part) into 5.8.x 2024-10-25 12:11:51 -05:00
Josh Cummings addc7c53b2 Merge branch '5.8.x' into 6.2.x
Closes gh-15985
2024-10-24 11:56:16 -06:00
DingHao 1399a82ea9 Return Null Request When Cookie Is Malformed
Closes gh-15905
2024-10-24 10:55:36 -07:00
Steve Riesenberg 3592253b8e Merge branch '5.8.x' into 6.2.x 2024-10-24 11:16:49 -05:00
github-actions[bot] 0b83830fb3 Update Antora Spring UI to v0.4.17 2024-10-24 11:14:43 -05:00
github-actions[bot] ec33e40748 Update Antora Spring UI to v0.4.17 2024-10-24 11:12:30 -05:00
Scott Murphy Heiberg 18dba34bde Make RequestMatcherDelegatingAuthorizationManager Post-Processable
Closes gh-15948
2024-10-23 15:15:10 -07:00
Rob Winch e48d6b039b Support ServerWebExchangeFirewall @Bean
Closes gh-15974
2024-10-22 18:25:28 -05:00
Steve Riesenberg e9bbe31b1a Merge branch '5.8.x' into 6.2.x 2024-10-21 13:52:08 -05:00
github-actions[bot] dd3c6892e9 Next development version 2024-10-21 17:58:56 +00:00
github-actions[bot] f9f533499b Next development version 2024-10-21 17:51:39 +00:00
github-actions[bot] 7209155482 Next development version 2024-10-21 17:42:46 +00:00
github-actions[bot] c552366a78 Release 5.7.13 2024-10-21 17:23:29 +00:00
github-actions[bot] 72a208e02c Release 5.8.15 2024-10-21 17:16:06 +00:00
Steve Riesenberg 7215c72373 Migrate slack notifications to GChat
Issue gh-15503

[skip ci]
2024-10-21 11:24:40 -05:00
Rob Winch 542071b1f8 Merge Add Firewall for WebFlux
Closes gh-15967
2024-10-21 08:56:42 -05:00
Rob Winch 4ce7cde155 Add Firewall for WebFlux
Closes gh-15967
2024-10-21 08:46:13 -05:00
79 changed files with 845 additions and 610 deletions
@@ -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,125 +0,0 @@
name: CI
on:
push:
branches-ignore:
- "dependabot/**"
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
env:
DEVELOCITY_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
send-notification:
name: Send Notification
needs: [ perform-release ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
-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 }}
@@ -1,45 +0,0 @@
name: Mark Duplicate Dependabot PRs
on:
pull_request:
types: [closed]
jobs:
check_duplicate_prs:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Extract Dependency Name from PR Title
id: extract
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}')
echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT
- name: Find PRs
id: find_duplicates
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "${{ steps.extract.outputs.dependency_name }}"' --json number --jq 'map(.number) | join(",")')
echo "prs=$PRS" >> $GITHUB_OUTPUT
- name: Label Duplicate PRs
if: steps.find_duplicates.outputs.prs != ''
env:
PRS: ${{ steps.find_duplicates.outputs.prs }}
CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
for i in ${PRS//,/ }
do
if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then
echo "Marking PR $i as duplicate"
gh pr edit "$i" --add-label "status: duplicate"
gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER"
fi
done
-63
View File
@@ -1,63 +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:
name: Merge Dependabot PR
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
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 }}
send-notification:
name: Send Notification
needs: [ merge-dependabot-pr ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -1,35 +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
send-notification:
name: Send Notification
needs: [ spring-releasetrain-checks ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
-54
View File
@@ -1,54 +0,0 @@
name: PR Build
on: pull_request
env:
DEVELOCITY_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 -PskipCheckExpectedBranchVersion --continue
generate-docs:
name: Generate Docs
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: Run Antora
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
- name: Upload Docs
id: upload
uses: actions/upload-artifact@v4
with:
name: docs
path: docs/build/site
overwrite: true
send-notification:
name: Send Notification
needs: [ build, generate-docs ]
if: ${{ failure() && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
-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,23 +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
send-notification:
name: Send Notification
needs: [ update-scheduled-release-version ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -18,6 +18,7 @@ package org.springframework.security.cas.userdetails;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apereo.cas.client.validation.Assertion;
@@ -73,7 +74,8 @@ public final class GrantedAuthorityFromAssertionAttributesUserDetailsService
}
private SimpleGrantedAuthority createSimpleGrantedAuthority(Object o) {
return new SimpleGrantedAuthority(this.convertToUpperCase ? o.toString().toUpperCase() : o.toString());
return new SimpleGrantedAuthority(
this.convertToUpperCase ? o.toString().toUpperCase(Locale.ROOT) : o.toString());
}
/**
@@ -171,7 +171,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
Assert.state(this.mappingCount > 0,
"At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())");
ObservationRegistry registry = getObservationRegistry();
RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build());
AuthorizationManager<HttpServletRequest> manager = postProcess(
(AuthorizationManager<HttpServletRequest>) this.managerBuilder.build());
if (registry.isNoop()) {
return manager;
}
@@ -21,6 +21,7 @@ import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.ApplicationContext;
@@ -33,6 +34,8 @@ import org.springframework.security.web.reactive.result.view.CsrfRequestDataValu
import org.springframework.security.web.server.ObservationWebFilterChainDecorator;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.result.view.AbstractView;
@@ -79,11 +82,14 @@ class WebFluxSecurityConfiguration {
@Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
@Order(WEB_FILTER_CHAIN_FILTER_ORDER)
WebFilterChainProxy springSecurityWebFilterChainFilter() {
WebFilterChainProxy springSecurityWebFilterChainFilter(ObjectProvider<ServerWebExchangeFirewall> firewall,
ObjectProvider<ServerExchangeRejectedHandler> rejectedHandler) {
WebFilterChainProxy proxy = new WebFilterChainProxy(getSecurityWebFilterChains());
if (!this.observationRegistry.isNoop()) {
proxy.setFilterChainDecorator(new ObservationWebFilterChainDecorator(this.observationRegistry));
}
firewall.ifUnique(proxy::setFirewall);
rejectedHandler.ifUnique(proxy::setExchangeRejectedHandler);
return proxy;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.config.http;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.ServletRequest;
@@ -313,7 +314,7 @@ class HttpConfigurationBuilder {
// Needed to account for placeholders
static String createPath(String path, boolean lowerCase) {
return lowerCase ? path.toLowerCase() : path;
return lowerCase ? path.toLowerCase(Locale.ENGLISH) : path;
}
BeanMetadataElement getSecurityContextHolderStrategyForAuthenticationFilters() {
@@ -147,6 +147,7 @@ final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
.addConstructorArgValue(logoutRequestResolver)
.addPropertyValue("logoutRequestRepository", logoutRequestRepository)
.getBeanDefinition();
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
.addConstructorArgValue(saml2LogoutRequestSuccessHandler)
@@ -0,0 +1,65 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.ClassUtils;
/**
* Utility methods for {@link HttpMessageConverter}'s.
*
* @author Joe Grandja
* @author luamas
* @since 5.1
*/
final class HttpMessageConverters {
private static final boolean jackson2Present;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
static {
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
}
private HttpMessageConverters() {
}
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
if (jackson2Present) {
return new MappingJackson2HttpMessageConverter();
}
if (gsonPresent) {
return new GsonHttpMessageConverter();
}
if (jsonbPresent) {
return new JsonbHttpMessageConverter();
}
return null;
}
}
@@ -0,0 +1,95 @@
/*
* 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.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageEncoder;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.MimeType;
class OAuth2ErrorEncoder implements HttpMessageEncoder<OAuth2Error> {
private final HttpMessageConverter<Object> messageConverter = HttpMessageConverters.getJsonMessageConverter();
@NotNull
@Override
public List<MediaType> getStreamingMediaTypes() {
return List.of();
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
return getEncodableMimeTypes().contains(mimeType);
}
@NotNull
@Override
public Flux<DataBuffer> encode(Publisher<? extends OAuth2Error> error, DataBufferFactory bufferFactory,
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
return Mono.from(error).flatMap((data) -> {
ByteArrayHttpOutputMessage bytes = new ByteArrayHttpOutputMessage();
try {
this.messageConverter.write(data, MediaType.APPLICATION_JSON, bytes);
return Mono.just(bytes.getBody().toByteArray());
}
catch (IOException ex) {
return Mono.error(ex);
}
}).map(bufferFactory::wrap).flux();
}
@NotNull
@Override
public List<MimeType> getEncodableMimeTypes() {
return List.of(MediaType.APPLICATION_JSON);
}
private static class ByteArrayHttpOutputMessage implements HttpOutputMessage {
private final ByteArrayOutputStream body = new ByteArrayOutputStream();
@NotNull
@Override
public ByteArrayOutputStream getBody() {
return this.body;
}
@NotNull
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
}
}
@@ -16,16 +16,17 @@
package org.springframework.security.config.web.server;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
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.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
@@ -62,6 +63,9 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
new OAuth2ErrorEncoder());
/**
* Construct an {@link OidcBackChannelLogoutWebFilter}
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
@@ -84,7 +88,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
})
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap(this.authenticationManager::authenticate)
@@ -93,7 +97,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
})
.flatMap((authentication) -> {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
@@ -101,19 +105,12 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
});
}
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
private Mono<Void> handleAuthenticationFailure(ServerWebExchange exchange, 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));
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
return this.errorHttpMessageConverter.write(Mono.just(oauth2Error(ex)), ResolvableType.forClass(Object.class),
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
exchange.getResponse(), Collections.emptyMap());
}
private OAuth2Error oauth2Error(Exception ex) {
@@ -16,8 +16,8 @@
package org.springframework.security.config.web.server;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -25,14 +25,15 @@ 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.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
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;
@@ -44,6 +45,7 @@ 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.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -63,6 +65,9 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
new OAuth2ErrorEncoder());
private WebClient web = WebClient.create();
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
@@ -97,7 +102,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
totalCount.intValue()));
}
if (!list.isEmpty()) {
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
return handleLogoutFailure(exchange.getExchange(), oauth2Error(list));
}
else {
return Mono.empty();
@@ -148,17 +153,11 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
"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));
private Mono<Void> handleLogoutFailure(ServerWebExchange exchange, OAuth2Error error) {
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
return this.errorHttpMessageConverter.write(Mono.just(error), ResolvableType.forClass(Object.class),
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
exchange.getResponse(), Collections.emptyMap());
}
/**
@@ -153,6 +153,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.spring.register(ObjectPostProcessorConfig.class).autowire();
ObjectPostProcessor objectPostProcessor = this.spring.getContext().getBean(ObjectPostProcessor.class);
verify(objectPostProcessor).postProcess(any(RequestMatcherDelegatingAuthorizationManager.class));
verify(objectPostProcessor).postProcess(any(AuthorizationManager.class));
verify(objectPostProcessor).postProcess(any(AuthorizationFilter.class));
}
@@ -484,6 +484,7 @@ public class Saml2LogoutConfigurerTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
@@ -16,14 +16,26 @@
package org.springframework.security.config.annotation.web.reactive;
import java.util.Collections;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.firewall.HttpStatusExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
@@ -47,6 +59,46 @@ public class WebFluxSecurityConfigurationTests {
assertThat(webFilterChainProxy).isNotNull();
}
@Test
void loadConfigWhenDefaultThenFirewalled() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
void loadConfigWhenCustomRejectedHandler() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class, CustomServerExchangeRejectedHandlerConfig.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode())
.isEqualTo(CustomServerExchangeRejectedHandlerConfig.EXPECTED_STATUS);
}
@Test
void loadConfigWhenFirewallBeanThenCustomized() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class, NoOpFirewallConfig.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode()).isNotEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void loadConfigWhenBeanProxyingEnabledAndSubclassThenWebFilterChainProxyExists() {
this.spring
@@ -57,6 +109,32 @@ public class WebFluxSecurityConfigurationTests {
assertThat(webFilterChainProxy).isNotNull();
}
private static @NotNull DefaultWebFilterChain emptyChain() {
return new DefaultWebFilterChain((webExchange) -> Mono.empty(), Collections.emptyList());
}
@Configuration
static class NoOpFirewallConfig {
@Bean
ServerWebExchangeFirewall noOpFirewall() {
return ServerWebExchangeFirewall.INSECURE_NOOP;
}
}
@Configuration
static class CustomServerExchangeRejectedHandlerConfig {
static HttpStatus EXPECTED_STATUS = HttpStatus.I_AM_A_TEAPOT;
@Bean
ServerExchangeRejectedHandler rejectedHandler() {
return new HttpStatusExchangeRejectedHandler(EXPECTED_STATUS);
}
}
@Configuration
static class SubclassConfig extends WebFluxSecurityConfiguration {
@@ -63,6 +63,7 @@ import org.springframework.web.util.UriUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
@@ -380,6 +381,22 @@ public class Saml2LogoutBeanDefinitionParserTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.configLocations(this.xml("CustomComponents")).autowire();
RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(this.rpLogoutRequest)
.id(this.rpLogoutRequestId)
.relayState(this.rpLogoutRequestRelayState)
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature))
.build();
given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest);
this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf()));
verify(getBean(Saml2LogoutRequestRepository.class)).saveLogoutRequest(eq(logoutRequest), any(), any());
}
private <T> T getBean(Class<T> clazz) {
return this.spring.getContext().getBean(clazz);
}
@@ -50,6 +50,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -97,6 +98,7 @@ import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
@@ -195,7 +197,10 @@ public class OidcLogoutSpecTests {
.body(BodyInserters.fromFormData("logout_token", "invalid"))
.exchange()
.expectStatus()
.isBadRequest();
.isBadRequest()
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
})
.value(hasValue("invalid_request"));
this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk();
}
@@ -262,9 +267,10 @@ public class OidcLogoutSpecTests {
.exchange()
.expectStatus()
.isBadRequest()
.expectBody(String.class)
.value(containsString("partial_logout"))
.value(containsString("not all sessions were terminated"));
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
})
.value(hasValue("partial_logout"))
.value(hasValue(containsString("not all sessions were terminated")));
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.security.authentication;
import java.util.Locale;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
@@ -53,7 +55,7 @@ public final class AuthenticationObservationConvention
if (authenticationType.endsWith("Authentication")) {
authenticationType = authenticationType.substring(0, authenticationType.lastIndexOf("Authentication"));
}
return "authenticate " + authenticationType.toLowerCase();
return "authenticate " + authenticationType.toLowerCase(Locale.ENGLISH);
}
return "authenticate";
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -79,10 +79,10 @@ public class SimpleAttributes2GrantedAuthoritiesMapper
*/
private GrantedAuthority getGrantedAuthority(String attribute) {
if (isConvertAttributeToLowerCase()) {
attribute = attribute.toLowerCase(Locale.getDefault());
attribute = attribute.toLowerCase(Locale.ROOT);
}
else if (isConvertAttributeToUpperCase()) {
attribute = attribute.toUpperCase(Locale.getDefault());
attribute = attribute.toUpperCase(Locale.ROOT);
}
if (isAddPrefixIfAlreadyExisting() || !attribute.startsWith(getAttributePrefix())) {
return new SimpleGrantedAuthority(getAttributePrefix() + attribute);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.core.authority.mapping;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.springframework.beans.factory.InitializingBean;
@@ -71,10 +72,10 @@ public final class SimpleAuthorityMapper implements GrantedAuthoritiesMapper, In
private GrantedAuthority mapAuthority(String name) {
if (this.convertToUpperCase) {
name = name.toUpperCase();
name = name.toUpperCase(Locale.ROOT);
}
else if (this.convertToLowerCase) {
name = name.toLowerCase();
name = name.toLowerCase(Locale.ROOT);
}
if (this.prefix.length() > 0 && !name.startsWith(this.prefix)) {
name = this.prefix + name;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.core.userdetails;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -91,7 +92,7 @@ public class MapReactiveUserDetailsService implements ReactiveUserDetailsService
}
private String getKey(String username) {
return username.toLowerCase();
return username.toLowerCase(Locale.ROOT);
}
}
@@ -19,6 +19,7 @@ package org.springframework.security.core.userdetails.memory;
import java.beans.PropertyEditorSupport;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.util.StringUtils;
@@ -45,10 +46,10 @@ public class UserAttributeEditor extends PropertyEditorSupport {
userAttrib.setPassword(currentToken);
}
else {
if (currentToken.toLowerCase().equals("enabled")) {
if (currentToken.toLowerCase(Locale.ENGLISH).equals("enabled")) {
userAttrib.setEnabled(true);
}
else if (currentToken.toLowerCase().equals("disabled")) {
else if (currentToken.toLowerCase(Locale.ENGLISH).equals("disabled")) {
userAttrib.setEnabled(false);
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.security.provisioning;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
@@ -96,23 +97,23 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
@Override
public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
}
@Override
public void deleteUser(String username) {
this.users.remove(username.toLowerCase());
this.users.remove(username.toLowerCase(Locale.ROOT));
}
@Override
public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()), "user should exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
}
@Override
public boolean userExists(String username) {
return this.users.containsKey(username.toLowerCase());
return this.users.containsKey(username.toLowerCase(Locale.ROOT));
}
@Override
@@ -143,14 +144,14 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
MutableUserDetails mutableUser = this.users.get(username.toLowerCase(Locale.ROOT));
mutableUser.setPassword(newPassword);
return mutableUser;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = this.users.get(username.toLowerCase());
UserDetails user = this.users.get(username.toLowerCase(Locale.ROOT));
if (user == null) {
throw new UsernameNotFoundException(username);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.crypto.password;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Locale;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
@@ -50,11 +51,11 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
private static final String SSHA_PREFIX = "{SSHA}";
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase();
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase(Locale.ENGLISH);
private static final String SHA_PREFIX = "{SHA}";
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase();
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase(Locale.ENGLISH);
private BytesKeyGenerator saltGenerator;
+1 -1
View File
@@ -30,7 +30,7 @@ urls:
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
snapshot: true
runtime:
log:
@@ -200,3 +200,35 @@ firewall.setAllowedHeaderValues {
}
----
======
The `ServerExchangeRejectedHandler` interface is used to handle `ServerExchangeRejectedException` throw by Spring Security's `ServerWebExchangeFirewall`.
By default `HttpStatusExchangeRejectedHandler` is used to send an HTTP 400 response to clients when a request is rejected.
To customize the behavior, users can expose a `ServerExchangeRejectedHandler` Bean.
For example, the following will send an HTTP 404 when the request is rejected:
.Send 404 on Request Rejected
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ServerExchangeRejectedHandler rejectedHandler() {
return new HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND);
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun rejectedHandler(): ServerExchangeRejectedHandler {
return HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND)
}
----
======
Handling can be completely customized by creating a custom `ServerExchangeRejectedHandler` implementation.
@@ -14,6 +14,7 @@ The following image shows the typical layering of the handlers for a single HTTP
.FilterChain
[[servlet-filterchain-figure]]
[.invert-dark]
image::{figures}/filterchain.png[]
The client sends a request to the application, and the container creates a `FilterChain`, which contains the `Filter` instances and `Servlet` that should process the `HttpServletRequest`, based on the path of the request URI.
@@ -66,6 +67,7 @@ Here is a picture of how `DelegatingFilterProxy` fits into the <<servlet-filters
.DelegatingFilterProxy
[[servlet-delegatingfilterproxy-figure]]
[.invert-dark]
image::{figures}/delegatingfilterproxy.png[]
`DelegatingFilterProxy` looks up __Bean Filter~0~__ from the `ApplicationContext` and then invokes __Bean Filter~0~__.
@@ -113,6 +115,7 @@ The following image shows the role of `FilterChainProxy`.
.FilterChainProxy
[[servlet-filterchainproxy-figure]]
[.invert-dark]
image::{figures}/filterchainproxy.png[]
[[servlet-securityfilterchain]]
@@ -124,6 +127,7 @@ The following image shows the role of `SecurityFilterChain`.
.SecurityFilterChain
[[servlet-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/securityfilterchain.png[]
The <<servlet-security-filters,Security Filters>> in `SecurityFilterChain` are typically Beans, but they are registered with `FilterChainProxy` instead of <<servlet-delegatingfilterproxy>>.
@@ -145,6 +149,7 @@ The following image shows multiple `SecurityFilterChain` instances:
.Multiple SecurityFilterChain
[[servlet-multi-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/multi-securityfilterchain.png[]
In the <<servlet-multi-securityfilterchain-figure>> figure, `FilterChainProxy` decides which `SecurityFilterChain` should be used.
@@ -398,6 +403,7 @@ The {security-api-url}org/springframework/security/web/access/ExceptionTranslati
The following image shows the relationship of `ExceptionTranslationFilter` to other components:
[.invert-dark]
image::{figures}/exceptiontranslationfilter.png[]
@@ -22,6 +22,7 @@ This also gives a good idea of the high level flow of authentication and how pie
At the heart of Spring Security's authentication model is the `SecurityContextHolder`.
It contains the <<servlet-authentication-securitycontext>>.
[.invert-dark]
image::{figures}/securitycontextholder.png[]
The `SecurityContextHolder` is where Spring Security stores the details of who is xref:features/authentication/index.adoc#authentication[authenticated].
@@ -175,6 +176,7 @@ While the implementation of `AuthenticationManager` could be anything, the most
Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide.
If none of the configured `AuthenticationProvider` instances can authenticate, authentication fails with a `ProviderNotFoundException`, which is a special `AuthenticationException` that indicates that the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it.
[.invert-dark]
image::{figures}/providermanager.png[]
In practice each `AuthenticationProvider` knows how to perform a specific type of authentication.
@@ -184,11 +186,13 @@ This lets each `AuthenticationProvider` do a very specific type of authenticatio
`ProviderManager` also allows configuring an optional parent `AuthenticationManager`, which is consulted in the event that no `AuthenticationProvider` can perform authentication.
The parent can be any type of `AuthenticationManager`, but it is often an instance of `ProviderManager`.
[.invert-dark]
image::{figures}/providermanager-parent.png[]
In fact, multiple `ProviderManager` instances might share the same parent `AuthenticationManager`.
This is somewhat common in scenarios where there are multiple xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] instances that have some authentication in common (the shared parent `AuthenticationManager`), but also different authentication mechanisms (the different `ProviderManager` instances).
[.invert-dark]
image::{figures}/providermanagers-parent.png[]
[[servlet-authentication-providermanager-erasing-credentials]]
@@ -234,6 +238,7 @@ Before the credentials can be authenticated, Spring Security typically requests
Next, the `AbstractAuthenticationProcessingFilter` can authenticate any authentication requests that are submitted to it.
[.invert-dark]
image::{figures}/abstractauthenticationprocessingfilter.png[]
image:{icondir}/number_1.png[] When the user submits their credentials, the `AbstractAuthenticationProcessingFilter` creates an <<servlet-authentication-authentication,`Authentication`>> from the `HttpServletRequest` to be authenticated.
@@ -9,6 +9,7 @@ This section describes how HTTP Basic Authentication works within Spring Securit
First, we see the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/basicauthenticationentrypoint.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -26,6 +27,7 @@ The following image shows the flow for the username and password being processed
[[servlet-authentication-basicauthenticationfilter]]
.Authenticating Username and Password
[.invert-dark]
image::{figures}/basicauthenticationfilter.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -8,6 +8,7 @@ This section examines how `DaoAuthenticationProvider` works within Spring Securi
The following figure explains the workings of the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section.
.`DaoAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/daoauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section passes a `UsernamePasswordAuthenticationToken` to the `AuthenticationManager`, which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -10,6 +10,7 @@ This section examines how form-based login works within Spring Security.
First, we see how the user is redirected to the login form:
.Redirecting to the Login Page
[.invert-dark]
image::{figures}/loginurlauthenticationentrypoint.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -30,6 +31,7 @@ When the username and password are submitted, the `UsernamePasswordAuthenticatio
The `UsernamePasswordAuthenticationFilter` extends xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[AbstractAuthenticationProcessingFilter], so the following diagram should look pretty similar:
.Authenticating Username and Password
[.invert-dark]
image::{figures}/usernamepasswordauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -191,6 +191,7 @@ In Spring Security 6, the example shown above is the default configuration.
The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextpersistencefilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -212,6 +213,7 @@ To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `
The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextholderfilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -126,6 +126,7 @@ For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` a
[[authz-authorization-manager-implementations]]
.Authorization Manager Implementations
[.invert-dark]
image::{figures}/authorizationhierarchy.png[]
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
@@ -345,6 +346,7 @@ The following image shows the `AccessDecisionManager` interface:
[[authz-access-voting]]
.Voting Decision Manager
[.invert-dark]
image::{figures}/access-decision-voting.png[]
By using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
@@ -405,6 +407,7 @@ For example, on the Spring web site, you can find a https://spring.io/blog/2009/
[[authz-after-invocation]]
.After Invocation Implementation
[.invert-dark]
image::{figures}/after-invocation.png[]
Like many other parts of Spring Security, `AfterInvocationManager` has a single concrete implementation, `AfterInvocationProviderManager`, which polls a list of ``AfterInvocationProvider``s.
@@ -65,6 +65,7 @@ In many cases, your authorization rules will be more sophisticated than that, so
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works at the request level in Servlet-based applications.
.Authorize HttpServletRequest
[.invert-dark]
image::{figures}/authorizationfilter.png[]
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` constructs a `Supplier` that retrieves an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
@@ -114,6 +114,7 @@ open class MyCustomerService {
A given invocation to `MyCustomerService#readCustomer` may look something like this when Method Security <<activate-method-security,is activated>>:
[.invert-dark]
image::{figures}/methodsecurity.png[]
1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PreAuthorize` pointcut>>
@@ -82,6 +82,7 @@ To learn more about CSRF protection for your application, consider the following
CSRF protection is provided by several components that are composed within the {security-api-url}org/springframework/security/web/csrf/CsrfFilter.html[`CsrfFilter`]:
.`CsrfFilter` Components
[.invert-dark]
image::{figures}/csrf.png[]
CSRF protection is divided into two parts:
@@ -90,6 +91,7 @@ CSRF protection is divided into two parts:
2. Determine if the request requires CSRF protection, load and validate the token, and <<csrf-access-denied-handler,handle `AccessDeniedException`>>.
.`CsrfFilter` Processing
[.invert-dark]
image::{figures}/csrf-processing.png[]
* image:{icondir}/number_1.png[] First, the {security-api-url}org/springframework/security/web/csrf/DeferredCsrfToken.html[`DeferredCsrfToken`] is loaded, which holds a reference to the <<csrf-token-repository,`CsrfTokenRepository`>> so that the persisted `CsrfToken` can be loaded later (in image:{icondir}/number_4.png[]).
@@ -205,6 +205,78 @@ This will ensure that:
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
[[migrating-spel-expressions]]
=== Migrating SpEL Expressions
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
However, to ease migration, you can also use a class like the following:
[source,java]
----
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
private Expression expression;
public MessageExpressionAuthorizationManager(String expressionString) {
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
return new ExpressionAuthorizationDecision(granted, this.expression);
}
}
----
And specify an instance for each matcher that you cannot get migrate:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
// ...
.simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
// ...
return messages.build();
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
messages
// ..
.simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
// ...
return messages.build()
}
}
----
======
[[websocket-authorization-notes]]
=== WebSocket Authorization Notes
@@ -21,6 +21,7 @@ Now we can consider how Bearer Token Authentication works within Spring Security
First, we see that, as with xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/bearerauthenticationentrypoint.png[]
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -38,6 +39,7 @@ The following image shows the flow for the bearer token being processed:
[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
.Authenticating Bearer Token
[.invert-dark]
image::{figures}/bearertokenauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -92,6 +92,7 @@ Let's take a look at how `JwtAuthenticationProvider` works within Spring Securit
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
.`JwtAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/jwtauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -88,6 +88,7 @@ Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
.`OpaqueTokenAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/opaquetokenauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -7,6 +7,7 @@ First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes
It does this through a series of redirects:
.Redirecting to Asserting Party Authentication
[.invert-dark]
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
[NOTE]
@@ -34,6 +35,7 @@ The following image shows how Spring Security authenticates a `<saml2:Response>`
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
.Authenticating a `<saml2:Response>`
[.invert-dark]
image::{figures}/saml2webssoauthenticationfilter.png[]
[NOTE]
+2 -2
View File
@@ -1,8 +1,8 @@
{
"dependencies": {
"antora": "3.2.0-alpha.6",
"antora": "3.2.0-alpha.8",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-beta.3",
"@antora/collector-extension": "1.0.1",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
@@ -40,4 +40,8 @@
<!-- Lambdas that we can't replace with a method reference because a closure is required -->
<suppress files="BearerTokenAuthenticationFilter\.java" checks="SpringLambda"/>
<!-- Ignore String.toUpperCase() and String.toLowerCase() checks in tests -->
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toLowerCaseWithoutLocale"/>
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toUpperCaseWithoutLocale"/>
</suppressions>
+16
View File
@@ -30,5 +30,21 @@
<property name="message" value="Please use assertThatExceptionOfType." />
<property name="ignoreComments" value="true" />
</module>
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
<property name="id" value="toLowerCaseWithoutLocale"/>
<property name="format" value="\.toLowerCase\(\)"/>
<property name="maximum" value="0"/>
<property name="message"
value="String.toLowerCase() should be String.toLowerCase(Locale.ROOT) or String.toLowerCase(Locale.ENGLISH)"/>
<property name="ignoreComments" value="true"/>
</module>
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
<property name="id" value="toUpperCaseWithoutLocale"/>
<property name="format" value="\.toUpperCase\(\)"/>
<property name="maximum" value="0"/>
<property name="message"
value="String.toUpperCase() should be String.toUpperCase(Locale.ROOT) or String.toUpperCase(Locale.ENGLISH)"/>
<property name="ignoreComments" value="true"/>
</module>
</module>
</module>
+1 -1
View File
@@ -1,5 +1,5 @@
springBootVersion=3.1.1
version=6.2.7
version=6.2.9-SNAPSHOT
samplesBranch=6.2.x
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+6 -6
View File
@@ -13,7 +13,7 @@ org-jetbrains-kotlin = "1.9.25"
org-jetbrains-kotlinx = "1.7.3"
org-mockito = "5.5.0"
org-opensaml = "4.3.2"
org-springframework = "6.1.14"
org-springframework = "6.1.15"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14"
@@ -26,9 +26,9 @@ com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebser
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
commons-collections = "commons-collections:commons-collections:3.2.2"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.11"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.13"
io-mockk = "io.mockk:mockk:1.13.13"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.11"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.13"
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
@@ -68,7 +68,7 @@ org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", ve
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.3.2.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.3"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
@@ -84,8 +84,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.11"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.7"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.12"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.8"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
@@ -1,5 +1,5 @@
/*
* Copyright 2005-2010 the original author or authors.
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.security.ldap;
import java.util.Locale;
import org.springframework.ldap.BadLdapGrammarException;
/**
@@ -72,7 +74,7 @@ final class LdapEncoder {
}
protected static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase();
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
return (raw.length() > 1) ? raw : "0" + raw;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2005-2010 the original author or authors.
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.security.ldap.authentication;
import java.util.Locale;
import org.springframework.ldap.BadLdapGrammarException;
/**
@@ -72,7 +74,7 @@ final class LdapEncoder {
}
protected static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase();
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
return (raw.length() > 1) ? raw : "0" + raw;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -144,9 +145,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
this.url = url;
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null;
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase(Locale.ROOT) : null;
}
/**
@@ -155,7 +156,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
this.url = url;
this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null;
}
@@ -350,7 +351,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
}
String createBindPrincipal(String username) {
if (this.domain == null || username.toLowerCase().endsWith(this.domain)) {
if (this.domain == null || username.toLowerCase(Locale.ROOT).endsWith(this.domain)) {
return username;
}
return username + "@" + this.domain;
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -179,7 +180,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
return null;
}
if (this.convertToUpperCase) {
role = role.toUpperCase();
role = role.toUpperCase(Locale.ROOT);
}
return new SimpleGrantedAuthority(this.rolePrefix + role);
};
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
@@ -124,7 +125,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
NamingEnumeration<?> ne = roleAttr.getAll();
Object group = ne.next();
String role = group.toString();
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase());
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase(Locale.ROOT));
};
private String[] attributesToRetrieve;
@@ -289,7 +290,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
@Deprecated
protected DistinguishedName buildGroupDn(String group) {
DistinguishedName dn = new DistinguishedName(this.groupSearchBase);
dn.add(this.groupRoleAttributeName, group.toLowerCase());
dn.add(this.groupRoleAttributeName, group.toLowerCase(Locale.ROOT));
return dn;
}
@@ -17,6 +17,7 @@
package org.springframework.security.ldap.userdetails;
import java.util.Collection;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -127,7 +128,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
protected GrantedAuthority createAuthority(Object role) {
if (role instanceof String) {
if (this.convertToUpperCase) {
role = ((String) role).toUpperCase();
role = ((String) role).toUpperCase(Locale.ROOT);
}
return new SimpleGrantedAuthority(this.rolePrefix + role);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.ldap.userdetails;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -193,7 +194,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
}
for (String role : roles) {
if (isConvertToUpperCase()) {
role = role.toUpperCase();
role = role.toUpperCase(Locale.ROOT);
}
role = getRolePrefix() + role;
// if the group already exist, we will not search for it's parents again.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -80,7 +80,13 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
if (registration == null) {
return null;
}
return (T) this.authorizedClients.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients
.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
if (cachedAuthorizedClient == null) {
return null;
}
return (T) new OAuth2AuthorizedClient(registration, cachedAuthorizedClient.getPrincipalName(),
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,8 +62,15 @@ public final class InMemoryReactiveOAuth2AuthorizedClientService implements Reac
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return (Mono<T>) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
.mapNotNull((clientRegistration) -> {
OAuth2AuthorizedClientId id = new OAuth2AuthorizedClientId(clientRegistrationId, principalName);
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients.get(id);
if (cachedAuthorizedClient == null) {
return null;
}
return new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(),
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
});
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
@@ -539,7 +540,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
// @formatter:off
return Stream.of(wwwAuthenticateHeader)
.filter((header) -> StringUtils.hasLength(header))
.filter((header) -> header.toLowerCase().startsWith("bearer"))
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
.map((header) -> header.substring("bearer".length()))
.map((header) -> header.split(","))
.flatMap(Stream::of)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -654,7 +655,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
private Map<String, String> parseAuthParameters(String wwwAuthenticateHeader) {
// @formatter:off
return Stream.of(wwwAuthenticateHeader).filter((header) -> StringUtils.hasLength(header))
.filter((header) -> header.toLowerCase().startsWith("bearer"))
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
.map((header) -> header.substring("bearer".length()))
.map((header) -> header.split(","))
.flatMap(Stream::of)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,22 +18,25 @@ package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.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.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatObject;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link InMemoryOAuth2AuthorizedClientService}.
@@ -79,9 +82,11 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
@Test
public void constructorWhenAuthorizedClientsProvidedThenUseProvidedAuthorizedClients() {
String registrationId = this.registration3.getRegistrationId();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration3, this.principalName1,
mock(OAuth2AccessToken.class));
Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients = Collections.singletonMap(
new OAuth2AuthorizedClientId(this.registration3.getRegistrationId(), this.principalName1),
mock(OAuth2AuthorizedClient.class));
authorizedClient);
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(eq(registrationId))).willReturn(this.registration3);
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
@@ -124,7 +129,35 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
}
@Test
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnAuthorizedClientWithUpdatedClientRegistration() {
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.registration1)
.clientSecret("updated secret")
.build();
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(this.registration1.getRegistrationId()))
.willReturn(this.registration1, updatedRegistration);
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
clientRegistrationRepository);
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.registration1,
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
authorizedClientService.saveAuthorizedClient(cachedAuthorizedClient,
new TestingAuthenticationToken(this.principalName1, null));
OAuth2AuthorizedClient authorizedClientWithUpdatedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
OAuth2AuthorizedClient firstLoadedClient = authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
OAuth2AuthorizedClient secondLoadedClient = authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
assertThat(firstLoadedClient).satisfies(isEqualTo(cachedAuthorizedClient));
assertThat(secondLoadedClient).satisfies(isEqualTo(authorizedClientWithUpdatedRegistration));
}
@Test
@@ -148,7 +181,7 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
.loadAuthorizedClient(this.registration3.getRegistrationId(), this.principalName2);
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
}
@Test
@@ -180,4 +213,38 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
assertThat(loadedAuthorizedClient).isNull();
}
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
return (actual) -> {
assertThat(actual).isNotNull();
assertThat(actual.getClientRegistration().getRegistrationId())
.isEqualTo(expected.getClientRegistration().getRegistrationId());
assertThat(actual.getClientRegistration().getClientName())
.isEqualTo(expected.getClientRegistration().getClientName());
assertThat(actual.getClientRegistration().getRedirectUri())
.isEqualTo(expected.getClientRegistration().getRedirectUri());
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
assertThat(actual.getClientRegistration().getClientId())
.isEqualTo(expected.getClientRegistration().getClientId());
assertThat(actual.getClientRegistration().getClientSecret())
.isEqualTo(expected.getClientRegistration().getClientSecret());
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
if (expected.getRefreshToken() != null) {
assertThat(actual.getRefreshToken()).isNotNull();
assertThat(actual.getRefreshToken().getTokenValue())
.isEqualTo(expected.getRefreshToken().getTokenValue());
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
assertThat(actual.getRefreshToken().getExpiresAt())
.isEqualTo(expected.getRefreshToken().getExpiresAt());
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,12 +18,14 @@ package org.springframework.security.oauth2.client;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -34,7 +36,9 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
@@ -56,8 +60,9 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
private Authentication principal = new TestingAuthenticationToken(this.principalName, "notused");
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", Instant.now(),
Instant.now().plus(Duration.ofDays(1)));
private OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;
// @formatter:off
private ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(this.clientRegistrationId)
@@ -79,6 +84,11 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
public void setup() {
this.authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(
this.clientRegistrationRepository);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofDays(1));
this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt);
this.refreshToken = new OAuth2RefreshToken("refresh", issuedAt, expiresAt);
}
@Test
@@ -153,11 +163,37 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
.saveAuthorizedClient(authorizedClient, this.principal)
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
StepVerifier.create(saveAndLoad)
.expectNext(authorizedClient)
.assertNext(isEqualTo(authorizedClient))
.verifyComplete();
// @formatter:on
}
@Test
@SuppressWarnings("unchecked")
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnsAuthorizedClientWithUpdatedClientRegistration() {
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.clientRegistration)
.clientSecret("updated secret")
.build();
given(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId))
.willReturn(Mono.just(this.clientRegistration), Mono.just(updatedRegistration));
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.clientRegistration,
this.principalName, this.accessToken, this.refreshToken);
OAuth2AuthorizedClient authorizedClientWithChangedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
this.principalName, this.accessToken, this.refreshToken);
Flux<OAuth2AuthorizedClient> saveAndLoadTwice = this.authorizedClientService
.saveAuthorizedClient(cachedAuthorizedClient, this.principal)
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
.concatWith(
this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
StepVerifier.create(saveAndLoadTwice)
.assertNext(isEqualTo(cachedAuthorizedClient))
.assertNext(isEqualTo(authorizedClientWithChangedRegistration))
.verifyComplete();
}
@Test
public void saveAuthorizedClientWhenAuthorizedClientNullThenIllegalArgumentException() {
OAuth2AuthorizedClient authorizedClient = null;
@@ -246,4 +282,38 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
// @formatter:on
}
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
return (actual) -> {
assertThat(actual).isNotNull();
assertThat(actual.getClientRegistration().getRegistrationId())
.isEqualTo(expected.getClientRegistration().getRegistrationId());
assertThat(actual.getClientRegistration().getClientName())
.isEqualTo(expected.getClientRegistration().getClientName());
assertThat(actual.getClientRegistration().getRedirectUri())
.isEqualTo(expected.getClientRegistration().getRedirectUri());
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
assertThat(actual.getClientRegistration().getClientId())
.isEqualTo(expected.getClientRegistration().getClientId());
assertThat(actual.getClientRegistration().getClientSecret())
.isEqualTo(expected.getClientRegistration().getClientSecret());
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
if (expected.getRefreshToken() != null) {
assertThat(actual.getRefreshToken()).isNotNull();
assertThat(actual.getRefreshToken().getTokenValue())
.isEqualTo(expected.getRefreshToken().getTokenValue());
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
assertThat(actual.getRefreshToken().getExpiresAt())
.isEqualTo(expected.getRefreshToken().getExpiresAt());
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -57,13 +57,18 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
? resolveFromRequestParameters(request) : null;
if (authorizationHeaderToken != null) {
if (parameterToken != null) {
final BearerTokenError error = BearerTokenErrors
BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
if (!StringUtils.hasText(parameterToken)) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
}
return null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -78,6 +78,11 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
if (!StringUtils.hasText(parameterToken)) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
}
return null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,8 +21,11 @@ import java.util.Base64;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -258,4 +261,35 @@ public class DefaultBearerTokenResolverTests {
assertThat(this.resolver.resolve(request)).isNull();
}
@Test
public void resolveWhenQueryParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
this.resolver.setAllowUriQueryParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.addParameter("access_token", "");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.resolver.resolve(request))
.withMessageContaining("The requested token parameter is an empty string")
.satisfies((e) -> {
BearerTokenError error = (BearerTokenError) e.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
}
@Test
public void resolveWhenFormParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
this.resolver.setAllowFormEncodedBodyParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", "");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.resolver.resolve(request))
.withMessageContaining("The requested token parameter is an empty string")
.satisfies((e) -> {
BearerTokenError error = (BearerTokenError) e.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -187,9 +187,9 @@ public class ServerBearerTokenAuthenticationConverterTests {
.isThrownBy(() -> convertToToken(request))
.satisfies((ex) -> {
BearerTokenError error = (BearerTokenError) ex.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_TOKEN);
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
// @formatter:on
}
@@ -1,5 +1,5 @@
/*
* Copyright 2004-2022 the original author or authors.
* Copyright 2004-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.security.taglibs.authz;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.ServletContext;
@@ -169,7 +170,7 @@ public abstract class AbstractAuthorizeTag {
}
public void setMethod(String method) {
this.method = (method != null) ? method.toUpperCase() : null;
this.method = (method != null) ? method.toUpperCase(Locale.ENGLISH) : null;
}
private SecurityContext getContext() {
@@ -16,6 +16,8 @@
package org.springframework.security.web;
import java.util.Locale;
import jakarta.servlet.ServletRequest;
import org.springframework.util.Assert;
@@ -45,7 +47,7 @@ public class PortResolverImpl implements PortResolver {
@Override
public int getServerPort(ServletRequest request) {
int serverPort = request.getServerPort();
String scheme = request.getScheme().toLowerCase();
String scheme = request.getScheme().toLowerCase(Locale.ENGLISH);
Integer mappedPort = getMappedPort(serverPort, scheme);
return (mappedPort != null) ? mappedPort : serverPort;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -97,8 +98,18 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
this.cookieCustomizer.accept(cookieBuilder);
Cookie cookie = mapToCookie(cookieBuilder.build());
response.addCookie(cookie);
ResponseCookie responseCookie = cookieBuilder.build();
if (!StringUtils.hasLength(responseCookie.getSameSite())) {
Cookie cookie = mapToCookie(responseCookie);
response.addCookie(cookie);
}
else if (request.getServletContext().getMajorVersion() > 5) {
Cookie cookie = mapToCookie(responseCookie);
response.addCookie(cookie);
}
else {
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
}
// Set request attribute to signal that response has blank cookie value,
// which allows loadToken to return null when token has been removed
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -73,6 +73,9 @@ public class CookieRequestCache implements RequestCache {
return null;
}
String originalURI = decodeCookie(savedRequestCookie.getValue());
if (originalURI == null) {
return null;
}
UriComponents uriComponents = UriComponentsBuilder.fromUriString(originalURI).build();
DefaultSavedRequest.Builder builder = new DefaultSavedRequest.Builder();
int port = getPort(uriComponents);
@@ -122,8 +125,14 @@ public class CookieRequestCache implements RequestCache {
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
}
private static String decodeCookie(String encodedCookieValue) {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
private String decodeCookie(String encodedCookieValue) {
try {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
catch (IllegalArgumentException ex) {
this.logger.debug("Failed decode cookie value " + encodedCookieValue);
return null;
}
}
private static String getCookiePath(HttpServletRequest request) {
@@ -16,6 +16,7 @@
package org.springframework.security.web.util;
import java.util.Locale;
import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletRequest;
@@ -49,7 +50,7 @@ public final class UrlUtils {
*/
public static String buildFullRequestUrl(String scheme, String serverName, int serverPort, String requestURI,
String queryString) {
scheme = scheme.toLowerCase();
scheme = scheme.toLowerCase(Locale.ENGLISH);
StringBuilder url = new StringBuilder();
url.append(scheme).append("://").append(serverName);
// Only add port if not default
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.security.web.util.matcher;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
@@ -303,7 +304,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
private SubpathMatcher(String subpath, boolean caseSensitive) {
Assert.isTrue(!subpath.contains("*"), "subpath cannot contain \"*\"");
this.subpath = caseSensitive ? subpath : subpath.toLowerCase();
this.subpath = caseSensitive ? subpath : subpath.toLowerCase(Locale.ROOT);
this.length = subpath.length();
this.caseSensitive = caseSensitive;
}
@@ -311,7 +312,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
@Override
public boolean matches(String path) {
if (!this.caseSensitive) {
path = path.toLowerCase();
path = path.toLowerCase(Locale.ROOT);
}
return path.startsWith(this.subpath) && (path.length() == this.length || path.charAt(this.length) == '/');
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,16 +17,20 @@
package org.springframework.security.web.csrf;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
@@ -447,6 +451,44 @@ class CookieCsrfTokenRepositoryTests {
assertThat(tokenCookie.isHttpOnly()).isEqualTo(Boolean.FALSE);
}
// gh-16173
@Test
void saveTokenWhenSameSiteAndServletVersion5ThenUsesAddHeader() {
HttpServletResponse response = mock(HttpServletResponse.class);
((MockServletContext) this.request.getServletContext()).setMajorVersion(5);
this.repository.setCookieCustomizer((builder) -> builder.sameSite("Strict"));
CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, response);
verify(response, never()).addCookie(any(Cookie.class));
verify(response).addHeader(any(), any());
}
// gh-16173
@Test
void saveTokenWhenSameSiteAndServletVersion6OrHigherThenUsesAddCookie() {
HttpServletResponse response = mock(HttpServletResponse.class);
this.repository.setCookieCustomizer((builder) -> builder.sameSite("Strict"));
CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, response);
verify(response).addCookie(any(Cookie.class));
verify(response, never()).addHeader(any(), any());
}
// gh-16173
@Test
void saveTokenWhenNoSameSiteThenUsesAddCookie() {
HttpServletResponse response = mock(HttpServletResponse.class);
CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, response);
verify(response).addCookie(any(Cookie.class));
verify(response, never()).addHeader(any(), any());
((MockServletContext) this.request.getServletContext()).setMajorVersion(5);
response = mock(HttpServletResponse.class);
this.repository.saveToken(token, this.request, response);
verify(response).addCookie(any(Cookie.class));
verify(response, never()).addHeader(any(), any());
}
@Test
void setCookieNameNullIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieName(null));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -212,4 +212,14 @@ public class CookieRequestCacheTests {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
// gh-15905
@Test
public void illegalCookieValueReturnNull() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, "123^456"));
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
assertThat(savedRequest).isNull();
}
}