Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a9d4dc8fc | |||
| 3d4e20597a | |||
| 81bd52ae48 | |||
| 25b6af2738 | |||
| 95987bffc1 | |||
| 6e5f8f2a1d | |||
| 4187af38b2 | |||
| 5b638a54a4 | |||
| 51eef2b980 | |||
| 302cfb116e | |||
| 695ea1717f | |||
| 1206c2b141 | |||
| 3539f06146 | |||
| a317a3d866 | |||
| 68b820ed09 | |||
| 44d32815b1 | |||
| 87c3335e01 | |||
| eefbb4da64 | |||
| a2793f31b4 | |||
| 679a47a51d | |||
| 08fca57d12 | |||
| acabacb971 | |||
| 1a130fca3c | |||
| 5a4ada04ac | |||
| a856baa6a8 | |||
| ac63cf4fa5 | |||
| f6bb55effb | |||
| 85b756cb74 | |||
| 7441ce7f16 | |||
| 9dbcd8cf00 | |||
| 835d6c1fbd | |||
| 95b2cdf7f4 | |||
| 3ecf84855e | |||
| 0039bc0cf0 | |||
| 057e5181ea | |||
| 178ca56aaf | |||
| 61ccf14953 | |||
| 6e683f2286 | |||
| b6e24db68c | |||
| aeb5fc1fb0 | |||
| 62f33d3fcf | |||
| 9fed1ac8c3 | |||
| 9dbe3bdcc0 | |||
| d547ae0181 | |||
| b8b1278e1f | |||
| 381047e386 | |||
| 376b40a735 | |||
| 89fa1cbdd2 | |||
| 0d75e6d10c | |||
| 01758c4c59 | |||
| f37833a59c | |||
| 52e6c4c4be | |||
| 96ceb535f4 | |||
| 0c54a55ae8 | |||
| 01ff3b086a | |||
| 33e6f4bd3f | |||
| cdd4b36d37 | |||
| 7672f76fde | |||
| 3db4999da4 | |||
| a708d2f61b | |||
| e726c05e76 | |||
| a7039fb3e6 | |||
| 88ea668f47 | |||
| 03a5de1955 | |||
| 06cbea383e | |||
| e250236279 | |||
| ef76ba040d | |||
| d69af716c8 | |||
| 7e37aa2b75 | |||
| 07bfe371b4 | |||
| e12edf43f2 | |||
| a499e56b9b | |||
| 8c3f6ea0d4 | |||
| e17d85e460 | |||
| f12036db05 | |||
| 7b5c502a97 | |||
| 706b059ea8 | |||
| ea3b112bea | |||
| 1261c229a3 | |||
| 4501ae7d1c | |||
| 48112d3d74 | |||
| b8735abb63 | |||
| 68a02ff176 | |||
| f37a706d62 | |||
| 522c48b3b5 | |||
| 1dae9aa459 | |||
| bec25edeb0 | |||
| 4d43edfb20 | |||
| 9f9699f8a5 | |||
| d29c984881 | |||
| 1116241ee3 | |||
| d87dc9ae57 | |||
| ac556a45f9 | |||
| c8731a8dc0 | |||
| 3e3eeda560 | |||
| 58df50c3a3 | |||
| 79156b2387 | |||
| 6c2b2a7611 | |||
| 0fab34f359 | |||
| 08e5b375ac | |||
| f9c32afb6f | |||
| 3d61276a1a | |||
| 10cb6f7003 | |||
| 3131642aae | |||
| 552d8d1d29 | |||
| f240f29433 | |||
| 1efacf1ad8 | |||
| fa7c6ea583 | |||
| 3841e0f6b3 | |||
| 50fad46df6 | |||
| e28eea208b | |||
| f646392542 | |||
| ffeda7a1b3 | |||
| 52ca16fa4b | |||
| 447e76bd06 | |||
| 46a9514420 | |||
| 8432df498e | |||
| 63162eb5f1 | |||
| 5c3b8c513b | |||
| d276c943fc | |||
| 18d9dd77ec | |||
| d6e3ec78cd | |||
| 48c1023fd6 | |||
| 04dbdc8588 | |||
| 3960bf950d | |||
| bc6ac7c8c6 | |||
| edd82ba82c | |||
| cf656ce6e1 | |||
| f75e9c7138 | |||
| 27f91e03f9 | |||
| b7230c367e | |||
| cd72cb1f2e | |||
| a865671526 | |||
| 4f52b71be8 | |||
| 398430f672 | |||
| 03b3b852f5 | |||
| e54bb9f3f5 | |||
| 30be114a5e | |||
| 75121bf455 | |||
| aa21e62fb8 | |||
| b58187082a | |||
| 8879aa83a3 | |||
| 84b124d29d | |||
| fee6a9bb0e | |||
| dd1f097131 | |||
| 508b3f26e3 | |||
| f2e674ff77 | |||
| e588a3528f | |||
| 7ea5be4b98 | |||
| e7ba153d5d | |||
| 05518f3402 | |||
| ffa2c195d8 | |||
| e1256d1d2f | |||
| 334df75c39 | |||
| 11b601159d | |||
| c77f42e80f | |||
| 8521b7d5a6 | |||
| f17a07e4c1 | |||
| 687eb4faf6 | |||
| 00fbe052f4 | |||
| 5023cc7e4c | |||
| 811be0a927 | |||
| 40e11752e0 | |||
| bc8d630bbc | |||
| 861c60a28f | |||
| 3cd8dc5c44 | |||
| 97342f83fd | |||
| dfe062b390 | |||
| 703a81390e | |||
| 0514ee4cc5 | |||
| 18c4c4c528 | |||
| 9fd6d54268 | |||
| 3f04f42abb | |||
| f585461427 | |||
| a155c035e1 | |||
| 6d722edab1 | |||
| e712531555 | |||
| 100c07de98 | |||
| 279afe3e28 |
@@ -14,4 +14,4 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@e415dadd0910c901e7a7fabd67bbb355b2324500 # 1
|
||||
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
secrets: inherit
|
||||
test:
|
||||
name: Test Against Snapshots
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -46,9 +46,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'spring-projects' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
@@ -67,21 +67,21 @@ jobs:
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
perform-release:
|
||||
name: Perform Release
|
||||
needs: [ deploy-artifacts, deploy-schema ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
|
||||
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
|
||||
@@ -97,6 +97,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Defer Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
defer-issues:
|
||||
name: Defer Issues
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Compute Version
|
||||
id: compute-version
|
||||
uses: spring-io/spring-release-actions/compute-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
- name: Get Today's Release Version
|
||||
id: todays-release
|
||||
uses: spring-io/spring-release-actions/get-todays-release-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
snapshot-version: ${{ steps.compute-version.outputs.version }}
|
||||
milestone-repository: ${{ github.repository }}
|
||||
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compute Next Version
|
||||
id: next-version
|
||||
uses: spring-io/spring-release-actions/compute-next-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
version: ${{ steps.todays-release.outputs.release-version }}
|
||||
- name: Schedule Next Milestone
|
||||
uses: spring-io/spring-release-actions/schedule-milestone@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
version: ${{ steps.next-version.outputs.version }}
|
||||
version-date: ${{ steps.next-version.outputs.version-date }}
|
||||
repository: ${{ github.repository }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Move Open Issues to Next Milestone
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CURRENT_MILESTONE: ${{ steps.todays-release.outputs.release-version }}
|
||||
NEXT_MILESTONE: ${{ steps.next-version.outputs.version }}
|
||||
run: |
|
||||
current_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
|
||||
--jq ".[] | select(.title == \"$CURRENT_MILESTONE\") | .number")
|
||||
if [ -z "$current_milestone_number" ]; then
|
||||
echo "No milestone found for $CURRENT_MILESTONE"
|
||||
exit 0
|
||||
fi
|
||||
next_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
|
||||
--jq ".[] | select(.title == \"$NEXT_MILESTONE\") | .number")
|
||||
if [ -z "$next_milestone_number" ]; then
|
||||
echo "No milestone found for $NEXT_MILESTONE"
|
||||
exit 1
|
||||
fi
|
||||
echo "Moving open issues from milestone '$CURRENT_MILESTONE' (#$current_milestone_number) to '$NEXT_MILESTONE' (#$next_milestone_number)"
|
||||
page=1
|
||||
while true; do
|
||||
issues=$(gh api "repos/${{ github.repository }}/issues?milestone=$current_milestone_number&state=open&per_page=100&page=$page" \
|
||||
--jq '.[].number')
|
||||
if [ -z "$issues" ]; then
|
||||
break
|
||||
fi
|
||||
for issue in $issues; do
|
||||
echo "Moving issue/PR #$issue to milestone $NEXT_MILESTONE"
|
||||
gh api repos/${{ github.repository }}/issues/$issue \
|
||||
--method PATCH \
|
||||
--field milestone=$next_milestone_number \
|
||||
--silent
|
||||
done
|
||||
page=$((page + 1))
|
||||
done
|
||||
echo "Done."
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -19,14 +19,14 @@ jobs:
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
- name: Upgrade Wrappers
|
||||
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
|
||||
env:
|
||||
|
||||
@@ -30,6 +30,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -11,9 +11,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
uses: spring-io/spring-gradle-build-action@efc55f07f4dfa22f2afd97f9ea1be4212eeed737 # v2.0.5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
|
||||
- name: Upload Docs
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
@@ -46,6 +46,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Dispatch
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
matrix:
|
||||
branch: [ '5.8.x', '6.2.x', '6.3.x', 'main' ]
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@e28269199d1d27975cf7f65e16d6095c555b3cd0
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: ${{ matrix.branch }}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on docs-build
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@e28269199d1d27975cf7f65e16d6095c555b3cd0
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: 'docs-build'
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
jobs:
|
||||
update-scheduled-release-version:
|
||||
name: Update Scheduled Release Version
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
secrets: inherit
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
+21
@@ -68,6 +68,27 @@ The https://github.com/spring-projects/spring-security/tree/docs-build[playbook
|
||||
|
||||
Discover more commands with `./gradlew tasks`.
|
||||
|
||||
=== IDE setup (IntelliJ)
|
||||
|
||||
No special steps are needed to open Spring Security in IntelliJ.
|
||||
|
||||
=== IDE setup (Eclipse and VS Code)
|
||||
|
||||
To work in Eclipse or VS Code, first generate Eclipse metadata so you can import the project into Eclipse or VS Code:
|
||||
|
||||
[indent=0]
|
||||
----
|
||||
./gradlew cleanEclipse eclipse
|
||||
----
|
||||
|
||||
If you have not built the project yet, run `./gradlew publishToMavenLocal` first so dependencies are resolved.
|
||||
|
||||
*VS Code:* Open the repository root as a folder. The repository includes `.vscode/settings.json` which disables automatic Gradle import so that the generated Eclipse metadata (`.classpath`, `.project`) is used. Do not use the Gradle for Java extension to import the project.
|
||||
|
||||
*Eclipse:* File → Import → General → Existing Projects into Workspace, then select the repository root.
|
||||
|
||||
The build uses a custom Eclipse plugin to work around Gradle dependency cycles that confuse IDE metadata generation. You may see Eclipse warnings about `xml-apis` from some test dependencies; those are excluded in the build and can be ignored.
|
||||
|
||||
== Getting Support
|
||||
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
|
||||
https://spring.io/support[Commercial support] is available too.
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Vendored
-240
@@ -1,240 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
-91
@@ -1,91 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2004-present the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
+3
@@ -36,6 +36,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity
|
||||
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
|
||||
import org.springframework.security.web.webauthn.management.CredentialRecordOwnerAuthorizationManager;
|
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||
@@ -166,6 +167,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
|
||||
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
|
||||
rpOperations);
|
||||
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
|
||||
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
|
||||
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
|
||||
rpOperations);
|
||||
if (creationOptionsRepository != null) {
|
||||
|
||||
@@ -211,6 +211,7 @@ import org.springframework.security.web.webauthn.api.AuthenticationExtensionsCli
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
|
||||
@@ -225,6 +226,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestO
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses;
|
||||
import org.springframework.security.web.webauthn.api.TestBytes;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
|
||||
@@ -654,6 +656,8 @@ final class SerializationSamples {
|
||||
generatorByClassName.put(CredentialPropertiesOutput.class, (o) -> credentialOutput);
|
||||
generatorByClassName.put(ImmutableAuthenticationExtensionsClientOutputs.class, (o) -> outputs);
|
||||
generatorByClassName.put(AuthenticatorAssertionResponse.class, (r) -> response);
|
||||
generatorByClassName.put(AuthenticatorAttestationResponse.class,
|
||||
(r) -> TestAuthenticatorAttestationResponses.createAuthenticatorAttestationResponse().build());
|
||||
generatorByClassName.put(RelyingPartyAuthenticationRequest.class, (r) -> authRequest);
|
||||
generatorByClassName.put(PublicKeyCredential.class, (r) -> credential);
|
||||
generatorByClassName.put(WebAuthnAuthenticationRequestToken.class, (r) -> requestToken);
|
||||
|
||||
+54
-5
@@ -33,10 +33,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
@@ -207,10 +207,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
boolean hasSerialVersion = Stream.of(clazz.getDeclaredFields())
|
||||
.map(Field::getName)
|
||||
.anyMatch((n) -> n.equals("serialVersionUID"));
|
||||
SuppressWarnings suppressWarnings = clazz.getAnnotation(SuppressWarnings.class);
|
||||
boolean hasSerialIgnore = suppressWarnings == null
|
||||
|| Arrays.asList(suppressWarnings.value()).contains("Serial");
|
||||
if (!hasSerialVersion && !hasSerialIgnore) {
|
||||
if (!hasSerialVersion && !hasSuppressSerialInSource(clazz)) {
|
||||
classes.add(clazz);
|
||||
continue;
|
||||
}
|
||||
@@ -249,6 +246,58 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
return classes.stream();
|
||||
}
|
||||
|
||||
private static boolean hasSuppressSerialInSource(Class<?> clazz) {
|
||||
try {
|
||||
Class<?> fileClass = clazz;
|
||||
while (fileClass.getEnclosingClass() != null) {
|
||||
fileClass = fileClass.getEnclosingClass();
|
||||
}
|
||||
var codeSource = fileClass.getProtectionDomain().getCodeSource();
|
||||
if (codeSource == null) {
|
||||
return false;
|
||||
}
|
||||
Path sourceFile = findSourceFile(Path.of(codeSource.getLocation().toURI()), fileClass);
|
||||
if (sourceFile == null) {
|
||||
return false;
|
||||
}
|
||||
return hasSuppressSerialAnnotation(Files.readAllLines(sourceFile), clazz.getSimpleName());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Path findSourceFile(Path start, Class<?> clazz) {
|
||||
String relativePath = clazz.getName().replace('.', '/') + ".java";
|
||||
Path dir = start;
|
||||
for (int i = 0; i < 10 && dir != null; i++) {
|
||||
for (String sourceRoot : List.of("src/main/java", "src/test/java")) {
|
||||
Path candidate = dir.resolve(sourceRoot).resolve(relativePath);
|
||||
if (Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
dir = dir.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean hasSuppressSerialAnnotation(List<String> lines, String simpleClassName) {
|
||||
Pattern classDeclaration = Pattern
|
||||
.compile("\\b(?:class|interface|enum|record)\\s+" + Pattern.quote(simpleClassName) + "\\b");
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (classDeclaration.matcher(lines.get(i)).find()) {
|
||||
for (int j = Math.max(0, i - 5); j < i; j++) {
|
||||
String line = lines.get(j);
|
||||
if (line.contains("@SuppressWarnings") && line.contains("\"serial\"")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getCurrentVersion() {
|
||||
String version = System.getProperty("springSecurityVersion");
|
||||
String[] parts = version.split("\\.");
|
||||
|
||||
+69
@@ -42,8 +42,15 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
|
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
@@ -56,6 +63,7 @@ import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
@@ -247,6 +255,24 @@ public class WebAuthnConfigurerTests {
|
||||
.andExpect(content().string(expectedBody));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webauthnWhenDeleteAndCredentialBelongsToUserThenNoContent() throws Exception {
|
||||
this.spring.register(DeleteCredentialConfiguration.class).autowire();
|
||||
this.mvc
|
||||
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
|
||||
.with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"))))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws Exception {
|
||||
this.spring.register(DeleteCredentialConfiguration.class).autowire();
|
||||
this.mvc
|
||||
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
|
||||
.with(authentication(new TestingAuthenticationToken("other-user", "password", "ROLE_USER"))))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class ConfigCredentialCreationOptionsRepository {
|
||||
@@ -417,4 +443,47 @@ public class WebAuthnConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class DeleteCredentialConfiguration {
|
||||
|
||||
static final String CREDENTIAL_ID_BASE64URL = "NauGCN7bZ5jEBwThcde51g";
|
||||
|
||||
static final Bytes USER_ENTITY_ID = Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM");
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
|
||||
return mock(WebAuthnRelyingPartyOperations.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserCredentialRepository userCredentialRepository() {
|
||||
MapUserCredentialRepository repository = new MapUserCredentialRepository();
|
||||
repository.save(TestCredentialRecords.userCredential().build());
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
PublicKeyCredentialUserEntityRepository userEntityRepository() {
|
||||
MapPublicKeyCredentialUserEntityRepository repository = new MapPublicKeyCredentialUserEntityRepository();
|
||||
repository.save(ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(USER_ENTITY_ID)
|
||||
.displayName("User")
|
||||
.build());
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+25
@@ -28,14 +28,17 @@ import jakarta.servlet.http.HttpSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.session.SessionLimit;
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@@ -150,6 +153,16 @@ public class HttpHeadersConfigTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenHeadersEagerlyConfiguredThenHeadersAreWritten() throws Exception {
|
||||
this.spring.configLocations(this.xml("HeadersEagerlyConfigured")).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/").secure(true))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(includesDefaults());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenFrameOptionsConfiguredThenIncludesHeader() throws Exception {
|
||||
Map<String, String> headers = new HashMap(defaultHeaders);
|
||||
@@ -955,6 +968,18 @@ public class HttpHeadersConfigTests {
|
||||
|
||||
}
|
||||
|
||||
public static class EagerHeadersBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof HeaderWriterFilter headerWriterFilter) {
|
||||
headerWriterFilter.setShouldWriteHeadersEagerly(true);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CustomSessionLimit implements SessionLimit {
|
||||
|
||||
@Override
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2004-present the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http auto-config="true">
|
||||
<headers/>
|
||||
<intercept-url pattern="/**" access="permitAll"/>
|
||||
</http>
|
||||
|
||||
<b:bean class="org.springframework.security.config.http.HttpHeadersConfigTests.EagerHeadersBeanPostProcessor"/>
|
||||
|
||||
<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
|
||||
|
||||
<b:import resource="userservice.xml"/>
|
||||
</b:beans>
|
||||
BIN
Binary file not shown.
+39
-4
@@ -92,6 +92,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
|
||||
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
|
||||
|
||||
private boolean alwaysPerformAdditionalChecksOnUser = true;
|
||||
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
|
||||
|
||||
/**
|
||||
@@ -146,8 +148,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
|
||||
}
|
||||
try {
|
||||
this.preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
if (!cacheWasUsed) {
|
||||
@@ -157,8 +158,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
// we're using latest data (i.e. not from the cache)
|
||||
cacheWasUsed = false;
|
||||
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
|
||||
this.preAuthenticationChecks.check(user);
|
||||
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
|
||||
}
|
||||
this.postAuthenticationChecks.check(user);
|
||||
if (!cacheWasUsed) {
|
||||
@@ -171,6 +171,25 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
return createSuccessAuthentication(principalToReturn, authentication, user);
|
||||
}
|
||||
|
||||
private void performPreCheck(UserDetails user, UsernamePasswordAuthenticationToken authentication) {
|
||||
try {
|
||||
this.preAuthenticationChecks.check(user);
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
if (!this.alwaysPerformAdditionalChecksOnUser) {
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
additionalAuthenticationChecks(user, authentication);
|
||||
}
|
||||
catch (AuthenticationException ignored) {
|
||||
// preserve the original failed check
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
additionalAuthenticationChecks(user, authentication);
|
||||
}
|
||||
|
||||
private String determineUsername(Authentication authentication) {
|
||||
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
|
||||
}
|
||||
@@ -313,6 +332,22 @@ public abstract class AbstractUserDetailsAuthenticationProvider
|
||||
this.postAuthenticationChecks = postAuthenticationChecks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to always perform the additional checks on the user, even if the
|
||||
* pre-authentication checks fail. This is useful to ensure that regardless of the
|
||||
* state of the user account, authentication takes the same amount of time to
|
||||
* complete.
|
||||
*
|
||||
* <p>
|
||||
* For applications that rely on the additional checks running only once should set
|
||||
* this value to {@code false}
|
||||
* @param alwaysPerformAdditionalChecksOnUser
|
||||
* @since 5.7.23
|
||||
*/
|
||||
public void setAlwaysPerformAdditionalChecksOnUser(boolean alwaysPerformAdditionalChecksOnUser) {
|
||||
this.alwaysPerformAdditionalChecksOnUser = alwaysPerformAdditionalChecksOnUser;
|
||||
}
|
||||
|
||||
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
|
||||
this.authoritiesMapper = authoritiesMapper;
|
||||
}
|
||||
|
||||
+5
-3
@@ -152,7 +152,9 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
|
||||
return null;
|
||||
}
|
||||
OneTimeToken token = tokens.get(0);
|
||||
deleteOneTimeToken(token);
|
||||
if (deleteOneTimeToken(token) == 0) {
|
||||
return null;
|
||||
}
|
||||
if (isExpired(token)) {
|
||||
return null;
|
||||
}
|
||||
@@ -170,11 +172,11 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
|
||||
return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper);
|
||||
}
|
||||
|
||||
private void deleteOneTimeToken(OneTimeToken oneTimeToken) {
|
||||
private int deleteOneTimeToken(OneTimeToken oneTimeToken) {
|
||||
List<SqlParameterValue> parameters = List
|
||||
.of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue()));
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
|
||||
return this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
|
||||
}
|
||||
|
||||
private ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
|
||||
|
||||
+5
-1
@@ -67,7 +67,11 @@ public final class AuthoritiesAuthorizationManager implements AuthorizationManag
|
||||
|
||||
private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
|
||||
for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
|
||||
if (authorities.contains(grantedAuthority.getAuthority())) {
|
||||
String authority = grantedAuthority.getAuthority();
|
||||
if (authority == null) {
|
||||
continue;
|
||||
}
|
||||
if (authorities.contains(authority)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
||||
* instead
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("serial")
|
||||
public class ExpressionAttributeAuthorizationDecision extends AuthorizationDecision {
|
||||
|
||||
private final ExpressionAttribute expressionAttribute;
|
||||
|
||||
+42
-6
@@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
@@ -42,6 +43,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
@@ -62,6 +64,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -452,12 +455,10 @@ public class DaoAuthenticationProviderTests {
|
||||
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
|
||||
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
|
||||
* SEC-2056 is fixed.
|
||||
*/
|
||||
public void IGNOREtestSec2056() {
|
||||
// SEC-2056
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
|
||||
public void testSec2056() {
|
||||
UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod",
|
||||
"koala");
|
||||
UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken
|
||||
@@ -491,6 +492,41 @@ public class DaoAuthenticationProviderTests {
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
// related to SEC-2056
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
|
||||
public void testDisabledUserTiming() {
|
||||
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
|
||||
PasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setPasswordEncoder(encoder);
|
||||
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
|
||||
users.password = encoder.encode((CharSequence) user.getCredentials());
|
||||
provider.setUserDetailsService(users);
|
||||
int sampleSize = 100;
|
||||
List<Long> enabledTimes = new ArrayList<>(sampleSize);
|
||||
for (int i = 0; i < sampleSize; i++) {
|
||||
long start = System.currentTimeMillis();
|
||||
provider.authenticate(user);
|
||||
enabledTimes.add(System.currentTimeMillis() - start);
|
||||
}
|
||||
UserDetailsChecker preChecks = mock(UserDetailsChecker.class);
|
||||
willThrow(new DisabledException("User is disabled")).given(preChecks).check(any(UserDetails.class));
|
||||
provider.setPreAuthenticationChecks(preChecks);
|
||||
List<Long> disabledTimes = new ArrayList<>(sampleSize);
|
||||
for (int i = 0; i < sampleSize; i++) {
|
||||
long start = System.currentTimeMillis();
|
||||
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> provider.authenticate(user));
|
||||
disabledTimes.add(System.currentTimeMillis() - start);
|
||||
}
|
||||
double enabledAvg = avg(enabledTimes);
|
||||
double disabledAvg = avg(disabledTimes);
|
||||
assertThat(Math.abs(disabledAvg - enabledAvg) <= 3)
|
||||
.withFailMessage("Disabled user average " + disabledAvg + " should be within 3ms of enabled user average "
|
||||
+ enabledAvg)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
private double avg(List<Long> counts) {
|
||||
return counts.stream().mapToLong(Long::longValue).average().orElse(0);
|
||||
}
|
||||
|
||||
+26
@@ -21,19 +21,24 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.PreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -145,6 +150,27 @@ class JdbcOneTimeTokenServiceTests {
|
||||
assertThat(consumedOneTimeToken).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeWhenTokenIsDeletedConcurrentlyThenReturnNull() throws Exception {
|
||||
// Simulates a concurrent consume: SELECT finds the token but DELETE affects
|
||||
// 0 rows because another caller already consumed it.
|
||||
JdbcOperations jdbcOperations = mock(JdbcOperations.class);
|
||||
Instant notExpired = Instant.now().plus(5, ChronoUnit.MINUTES);
|
||||
OneTimeToken token = new DefaultOneTimeToken(TOKEN_VALUE, USERNAME, notExpired);
|
||||
given(jdbcOperations.query(any(String.class), any(PreparedStatementSetter.class),
|
||||
ArgumentMatchers.<RowMapper<OneTimeToken>>any()))
|
||||
.willReturn(List.of(token));
|
||||
given(jdbcOperations.update(any(String.class), any(PreparedStatementSetter.class))).willReturn(0);
|
||||
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcOperations);
|
||||
try {
|
||||
OneTimeToken consumed = service.consume(new OneTimeTokenAuthenticationToken(TOKEN_VALUE));
|
||||
assertThat(consumed).isNull();
|
||||
}
|
||||
finally {
|
||||
service.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeWhenTokenIsExpiredThenReturnNull() {
|
||||
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
|
||||
|
||||
+20
@@ -17,7 +17,9 @@
|
||||
package org.springframework.security.authorization;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -30,11 +32,13 @@ import org.springframework.security.core.Authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthoritiesAuthorizationManager}.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Khyojae
|
||||
*/
|
||||
class AuthoritiesAuthorizationManagerTests {
|
||||
|
||||
@@ -84,4 +88,20 @@ class AuthoritiesAuthorizationManagerTests {
|
||||
assertThat(manager.check(authentication, Collections.singleton("ROLE_USER")).isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
// gh-18543
|
||||
void authorizeWhenAuthorityIsNullThenDoesNotThrowNullPointerException() {
|
||||
AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager();
|
||||
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password",
|
||||
Collections.singletonList(() -> null));
|
||||
|
||||
Collection<String> authoritiesContainsThrowsNPE = Set.of("ROLE_USER");
|
||||
|
||||
// must be Collection that throws NPE when .contains(null) is invoked
|
||||
// to replicate the issue in gh-18543
|
||||
assertThatNullPointerException().isThrownBy(() -> authoritiesContainsThrowsNPE.contains(null));
|
||||
assertThat(manager.authorize(() -> authentication, authoritiesContainsThrowsNPE).isGranted()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
plugins {
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.spring-module'
|
||||
|
||||
dependencies {
|
||||
@@ -6,6 +10,8 @@ dependencies {
|
||||
optional 'org.springframework:spring-core'
|
||||
optional 'org.bouncycastle:bcpkix-jdk18on'
|
||||
|
||||
testFixturesImplementation "org.assertj:assertj-core"
|
||||
|
||||
testImplementation "org.assertj:assertj-core"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params"
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.crypto.assertions;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link CryptoAssertions} and {@link CryptoStringAssert}.
|
||||
*/
|
||||
class CryptoAssertionsTests {
|
||||
|
||||
@Test
|
||||
void doesNotDecryptToPassesWhenSupplierThrows() {
|
||||
CryptoAssertions.assertThat((() -> {
|
||||
throw new RuntimeException("decrypt failed");
|
||||
})).doesNotDecryptTo("any");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotDecryptToPassesWhenResultDiffersFromExpected() {
|
||||
CryptoAssertions.assertThat(() -> "other").doesNotDecryptTo("plaintext");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotDecryptToFailsWhenResultEqualsExpected() {
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> CryptoAssertions.assertThat(() -> "plaintext").doesNotDecryptTo("plaintext"))
|
||||
.withMessageContaining("Expected supplier not to return <plaintext> but it did");
|
||||
}
|
||||
|
||||
}
|
||||
+8
-4
@@ -22,8 +22,9 @@ import java.security.interfaces.RSAPublicKey;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.crypto.assertions.CryptoAssertions;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
@@ -86,13 +87,15 @@ public class RsaSecretEncryptorTests {
|
||||
@Test
|
||||
public void roundTripWithMixedAlgorithm() {
|
||||
RsaSecretEncryptor oaep = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
|
||||
assertThatIllegalStateException().isThrownBy(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")));
|
||||
CryptoAssertions.assertThat(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")))
|
||||
.doesNotDecryptTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithMixedSalt() {
|
||||
RsaSecretEncryptor other = new RsaSecretEncryptor(this.encryptor.getPublicKey(), RsaAlgorithm.DEFAULT, "salt");
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.encryptor.decrypt(other.encrypt("encryptor")));
|
||||
CryptoAssertions.assertThat(() -> this.encryptor.decrypt(other.encrypt("encryptor")))
|
||||
.doesNotDecryptTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,7 +109,8 @@ public class RsaSecretEncryptorTests {
|
||||
public void publicKeyCannotDecrypt() {
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
|
||||
assertThat(encryptor.canDecrypt()).as("Encryptor schould not be able to decrypt").isFalse();
|
||||
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encryptor.encrypt("encryptor")));
|
||||
CryptoAssertions.assertThat(() -> encryptor.decrypt(encryptor.encrypt("encryptor")))
|
||||
.doesNotDecryptTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.crypto.assertions;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* AssertJ entry point for crypto-related assertions. Use {@link #assertThat(Supplier)} to
|
||||
* assert on a {@link Supplier}<{@link String}> (e.g. a decryption lambda).
|
||||
* <p>
|
||||
* Example: <pre>
|
||||
* assertThat(() -> encryptor.decrypt(ciphertext)).doesNotDecryptTo("plaintext");
|
||||
* </pre>
|
||||
*/
|
||||
public final class CryptoAssertions {
|
||||
|
||||
private CryptoAssertions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create assertions for the given supplier (e.g. a decryption expression).
|
||||
* @param actual the supplier to assert on
|
||||
* @return assertion object with methods like
|
||||
* {@link CryptoStringAssert#doesNotDecryptTo(String)}
|
||||
*/
|
||||
public static CryptoStringAssert assertThat(Supplier<String> actual) {
|
||||
return new CryptoStringAssert(actual);
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.crypto.assertions;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
|
||||
/**
|
||||
* AssertJ assertion for {@link Supplier}<{@link String}>, supporting
|
||||
* decryption-related checks such as {@link #doesNotDecryptTo(String)}.
|
||||
*/
|
||||
public final class CryptoStringAssert extends AbstractObjectAssert<CryptoStringAssert, Supplier<String>> {
|
||||
|
||||
CryptoStringAssert(Supplier<String> actual) {
|
||||
super(actual, CryptoStringAssert.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that either the supplier throws an exception when invoked, or the value it
|
||||
* returns is not equal to the given string. Use this to assert that a decryption
|
||||
* attempt does not yield a specific plaintext (e.g. wrong key or tampered
|
||||
* ciphertext).
|
||||
* @param expected the value that the supplier must not return
|
||||
* @return this assertion for chaining
|
||||
*/
|
||||
public CryptoStringAssert doesNotDecryptTo(String expected) {
|
||||
isNotNull();
|
||||
try {
|
||||
String result = this.actual.get();
|
||||
if (Objects.equals(result, expected)) {
|
||||
failWithMessage("Expected supplier not to return <%s> but it did", expected);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Exception thrown: supplier does not "decrypt to" the expected value
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,7 @@ urls:
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.26/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
|
||||
@@ -414,22 +414,22 @@ Instead of taking this responsibility away from developers, now it is simpler to
|
||||
|
||||
[method,java]
|
||||
----
|
||||
PathPatternRequestParser.Builder servlet = PathPatternRequestParser.withDefaults().basePath("/mvc");
|
||||
PathPatternRequestMatcher.Builder servlet = PathPatternRequestMatcher.withDefaults().basePath("/mvc");
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers(servlet.pattern("/orders/**").matcher()).authenticated()
|
||||
.requestMatchers(servlet.matcher("/orders/**")).authenticated()
|
||||
)
|
||||
----
|
||||
|
||||
|
||||
For paths that belong to the default servlet, use `PathPatternRequestParser.withDefaults()` instead:
|
||||
For paths that belong to the default servlet, use `PathPatternRequestMatcher.withDefaults()` instead:
|
||||
|
||||
[method,java]
|
||||
----
|
||||
PathPatternRequestParser.Builder request = PathPatternRequestParser.withDefaults();
|
||||
PathPatternRequestMatcher.Builder request = PathPatternRequestMatcher.withDefaults();
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers(request.pattern("/js/**").matcher()).authenticated()
|
||||
.requestMatchers(request.matcher("/js/**")).authenticated()
|
||||
)
|
||||
----
|
||||
|
||||
|
||||
@@ -39,17 +39,18 @@ This endpoint is referred to as a https://openid.net/specs/openid-connect-discov
|
||||
|
||||
When this property and these dependencies are used, Resource Server automatically configures itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
|
||||
|
||||
. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property.
|
||||
. Configure the validation strategy to query `jwks_url` for valid public keys.
|
||||
. Configure the validation strategy to validate each JWT's `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be receiving requests in order for Resource Server to successfully start up.
|
||||
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup fails.
|
||||
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder[`SupplierReactiveJwtDecoder`].
|
||||
Consider wrapping any <<webflux-oauth2resourceserver-decoder-bean,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
|
||||
====
|
||||
|
||||
=== Runtime Expectations
|
||||
@@ -85,7 +86,7 @@ From here, consider jumping to:
|
||||
[[webflux-oauth2resourceserver-jwt-jwkseturi]]
|
||||
=== Specifying the Authorization Server JWK Set Uri Directly
|
||||
|
||||
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, you can supply `jwk-set-uri` as well:
|
||||
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, you can supply `jwk-set-uri` as well:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
@@ -375,7 +376,9 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -385,7 +388,9 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -451,8 +456,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -462,8 +469,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -478,8 +487,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -489,8 +500,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -505,11 +518,13 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms(algorithms -> {
|
||||
algorithms.add(RS512);
|
||||
algorithms.add(ES512);
|
||||
}).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -519,12 +534,14 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
|
||||
val jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms {
|
||||
it.add(RS512)
|
||||
it.add(ES512)
|
||||
}
|
||||
.build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -319,7 +319,7 @@ If you have trouble working out where a session is being created, you can add so
|
||||
[[appendix-faq-forbidden-csrf]]
|
||||
=== I get a 403 Forbidden when performing a POST. What is wrong?
|
||||
|
||||
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to https://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
|
||||
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to xref:features/exploits/csrf.adoc#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
|
||||
|
||||
[[appendix-faq-no-security-on-forward]]
|
||||
=== I am forwarding a request to another URL by using the RequestDispatcher, but my security constraints are not being applied.
|
||||
|
||||
@@ -137,7 +137,8 @@ fun method(authentication: Authentication?): String {
|
||||
will always return "not anonymous", even for anonymous requests.
|
||||
The reason is that Spring MVC resolves the parameter using `HttpServletRequest#getPrincipal`, which is `null` when the request is anonymous.
|
||||
|
||||
If you'd like to obtain the `Authentication` in anonymous requests, use `@CurrentSecurityContext` instead:
|
||||
If you'd like to obtain the `Authentication` in anonymous requests, use
|
||||
xref:servlet/integrations/mvc.adoc#mvc-current-security-context[`@CurrentSecurityContext`] instead:
|
||||
|
||||
.Use CurrentSecurityContext for Anonymous requests
|
||||
[tabs]
|
||||
|
||||
@@ -97,7 +97,11 @@ val authorities = authentication.authorities
|
||||
----
|
||||
======
|
||||
|
||||
// FIXME: Add links to and relevant description of HttpServletRequest.getRemoteUser() and @CurrentSecurityContext @AuthenticationPrincipal
|
||||
In Spring MVC, you can resolve the current principal with
|
||||
xref:servlet/integrations/mvc.adoc#mvc-authentication-principal[`@AuthenticationPrincipal`]
|
||||
and the full `SecurityContext` with
|
||||
xref:servlet/integrations/mvc.adoc#mvc-current-security-context[`@CurrentSecurityContext`].
|
||||
For Servlet API access, use `HttpServletRequest#getRemoteUser`.
|
||||
|
||||
By default, `SecurityContextHolder` uses a `ThreadLocal` to store these details, which means that the `SecurityContext` is always available to methods in the same thread, even if the `SecurityContext` is not explicitly passed around as an argument to those methods.
|
||||
Using a `ThreadLocal` in this way is quite safe if you take care to clear the thread after the present principal's request is processed.
|
||||
|
||||
@@ -259,7 +259,7 @@ Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directives.ALL));
|
||||
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL));
|
||||
http
|
||||
.logout((logout) -> logout.addLogoutHandler(clearSiteData))
|
||||
----
|
||||
@@ -268,7 +268,7 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directives.ALL))
|
||||
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directive.ALL))
|
||||
http {
|
||||
logout {
|
||||
addLogoutHandler(clearSiteData)
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
Once you have got an application that is xref:servlet/authentication/index.adoc[authenticating requests], it is important to consider how that resulting authentication will be persisted and restored on future requests.
|
||||
|
||||
This is done automatically by default, so no additional code is necessary, though it is important to know what `requireExplicitSave` means in `HttpSecurity`.
|
||||
This is done automatically by default.
|
||||
If you have a custom filter or controller that is setting the security context, you will need to use a `SecurityContextRepository` to persist it across requests.
|
||||
|
||||
If you are upgrading from an older version, you may be interested in the `requireExplicitSave` setting that preserves Spring Security 5's default, though note that this is primarily for migration purposes.
|
||||
|
||||
If you like, <<how-it-works-requireexplicitsave,you can read more about what requireExplicitSave is doing>> or <<requireexplicitsave,why it's important>>. Otherwise, in most cases you are done with this section.
|
||||
|
||||
|
||||
@@ -1377,14 +1377,17 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
|
||||
public class MyPreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
|
||||
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class MyPostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
|
||||
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
@@ -1395,12 +1398,15 @@ Kotlin::
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
|
||||
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
|
||||
class MyPreAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocation> {
|
||||
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationResult {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
|
||||
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
|
||||
@Component
|
||||
class MyPostAuthorizeAuthorizationManager : AuthorizationManager<MethodInvocationResult> {
|
||||
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
|
||||
// ... authorization logic
|
||||
}
|
||||
}
|
||||
@@ -1422,13 +1428,13 @@ Java::
|
||||
class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor preAuthorize(MyAuthorizationManager manager) {
|
||||
Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor postAuthorize(MyAuthorizationManager manager) {
|
||||
Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
|
||||
}
|
||||
}
|
||||
@@ -1441,15 +1447,15 @@ Kotlin::
|
||||
@Configuration
|
||||
@EnableMethodSecurity(prePostEnabled = false)
|
||||
class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun preAuthorize(manager: MyAuthorizationManager) : Advisor {
|
||||
fun preAuthorize(manager: MyPreAuthorizeAuthorizationManager): Advisor {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun postAuthorize(manager: MyAuthorizationManager) : Advisor {
|
||||
fun postAuthorize(manager: MyPostAuthorizeAuthorizationManager): Advisor {
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
|
||||
}
|
||||
}
|
||||
@@ -1466,13 +1472,13 @@ Xml::
|
||||
<bean id="preAuthorize"
|
||||
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
|
||||
factory-method="preAuthorize">
|
||||
<constructor-arg ref="myAuthorizationManager"/>
|
||||
<constructor-arg ref="myPreAuthorizeAuthorizationManager"/>
|
||||
</bean>
|
||||
|
||||
<bean id="postAuthorize"
|
||||
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
|
||||
factory-method="postAuthorize">
|
||||
<constructor-arg ref="myAuthorizationManager"/>
|
||||
<constructor-arg ref="myPostAuthorizeAuthorizationManager"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
@@ -1482,6 +1488,8 @@ Xml::
|
||||
You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
|
||||
====
|
||||
|
||||
You can also implement `MethodAuthorizationDeniedHandler` in the same manager class to override the default exception-handling behavior.
|
||||
|
||||
[[customizing-expression-handling]]
|
||||
=== Customizing Expression Handling
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ To use this tag, you must also have an instance of `WebInvocationPrivilegeEvalua
|
||||
If you are using the namespace, one is automatically registered.
|
||||
This is an instance of `DefaultWebInvocationPrivilegeEvaluator`, which creates a dummy web request for the supplied URL and invokes the security interceptor to see whether the request would succeed or fail.
|
||||
This lets you delegate to the access-control setup you defined by using `intercept-url` declarations within the `<http>` namespace configuration and saves having to duplicate the information (such as the required roles) within your JSPs.
|
||||
You can also combine this approach with a `method` attribute (supplying the HTTP method, such as `POST`) for a more specific match.
|
||||
|
||||
If you have xref:servlet/authorization/authorize-http-requests.adoc#match-by-httpmethod[method-based authorization rules], you should combine this approach with the `method` attribute (supplying the HTTP method, such as `POST`) to activate the intended method-based rule.
|
||||
For example, if you have a rule `.requestMatchers(POST, "/admin").hasRole("ADMIN")`, then you should do `<sec:authorize method="POST" url="/admin">` to match.
|
||||
|
||||
You can store the Boolean result of evaluating the tag (whether it grants or denies access) in a page context scope variable by setting the `var` attribute to the variable name, avoiding the need for duplicating and re-evaluating the condition at other points in the page.
|
||||
|
||||
|
||||
@@ -453,6 +453,72 @@ open fun findMessagesForUser(@CurrentUser("user_id") userId: String?): ModelAndV
|
||||
----
|
||||
======
|
||||
|
||||
[[mvc-current-security-context]]
|
||||
== @CurrentSecurityContext
|
||||
|
||||
Spring Security provides `CurrentSecurityContextArgumentResolver`, which can automatically resolve the current `SecurityContext` for Spring MVC arguments.
|
||||
By using `@EnableWebSecurity`, you automatically have this added to your Spring MVC configuration.
|
||||
If you use XML-based configuration, you must add this yourself:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<mvc:annotation-driven>
|
||||
<mvc:argument-resolvers>
|
||||
<bean class="org.springframework.security.web.method.annotation.CurrentSecurityContextArgumentResolver" />
|
||||
</mvc:argument-resolvers>
|
||||
</mvc:annotation-driven>
|
||||
----
|
||||
|
||||
Once `CurrentSecurityContextArgumentResolver` is configured, you can access the `SecurityContext` directly:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/me")
|
||||
public String me(@CurrentSecurityContext SecurityContext context) {
|
||||
return context.getAuthentication().getName();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/me")
|
||||
fun me(@CurrentSecurityContext context: SecurityContext): String {
|
||||
return context.authentication.name
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
You can also use a SpEL expression that is rooted at the `SecurityContext`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/me")
|
||||
public String me(@CurrentSecurityContext(expression = "authentication") Authentication authentication) {
|
||||
return authentication.getName();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/me")
|
||||
fun me(@CurrentSecurityContext(expression = "authentication") authentication: Authentication): String {
|
||||
return authentication.name
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[mvc-async]]
|
||||
== Spring MVC Async Integration
|
||||
|
||||
|
||||
@@ -40,17 +40,20 @@ And that's it!
|
||||
|
||||
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
|
||||
|
||||
1. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property
|
||||
2. Query the `jwks_url` endpoint for supported algorithms
|
||||
3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found
|
||||
4. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
|
||||
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
|
||||
|
||||
[NOTE]
|
||||
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
|
||||
====
|
||||
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierJwtDecoder[`SupplierJwtDecoder`].
|
||||
Consider wrapping any <<oauth2resourceserver-jwt-decoder,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
|
||||
====
|
||||
|
||||
=== Runtime Expectations
|
||||
|
||||
@@ -66,7 +69,7 @@ So long as this scheme is indicated, Resource Server will attempt to process the
|
||||
|
||||
Given a well-formed JWT, Resource Server will:
|
||||
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup or on first request, depending on configuration, and matched against the JWT
|
||||
2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`.
|
||||
|
||||
@@ -111,7 +114,7 @@ Ultimately, the returned `JwtAuthenticationToken` will be set on the xref:servle
|
||||
[[oauth2resourceserver-jwt-jwkseturi]]
|
||||
== Specifying the Authorization Server JWK Set Uri Directly
|
||||
|
||||
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
|
||||
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
@@ -516,7 +519,9 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -526,7 +531,9 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -592,8 +599,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -603,8 +612,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -619,8 +630,10 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -630,8 +643,10 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -646,11 +661,13 @@ Java::
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms(algorithms -> {
|
||||
algorithms.add(RS512);
|
||||
algorithms.add(ES512);
|
||||
}).build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer));
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
@@ -660,11 +677,13 @@ Kotlin::
|
||||
----
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(this.issuer)
|
||||
.jwsAlgorithms {
|
||||
it.add(RS512)
|
||||
it.add(ES512)
|
||||
}.build()
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(this.issuer))
|
||||
return jwtDecoder
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.8",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.1",
|
||||
"antora": "3.2.0-alpha.11",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||
"@antora/collector-extension": "1.0.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.4",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
"@springio/antora-extensions": "1.14.11",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=3.3.3
|
||||
version=6.5.8-SNAPSHOT
|
||||
version=6.5.10
|
||||
samplesBranch=6.5.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+14
-14
@@ -5,8 +5,8 @@ io-spring-javaformat = "0.0.47"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.2.0"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-apache-maven-resolver = "1.9.25"
|
||||
org-aspectj = "1.9.25"
|
||||
org-apache-maven-resolver = "1.9.27"
|
||||
org-aspectj = "1.9.25.1"
|
||||
org-bouncycastle = "1.80"
|
||||
org-eclipse-jetty = "11.0.26"
|
||||
org-jetbrains-kotlin = "1.9.25"
|
||||
@@ -14,11 +14,11 @@ org-jetbrains-kotlinx = "1.10.2"
|
||||
org-mockito = "5.17.0"
|
||||
org-opensaml = "4.3.2"
|
||||
org-opensaml5 = "5.1.2"
|
||||
org-springframework = "6.2.14"
|
||||
org-springframework = "6.2.18"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.21"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.5"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.6"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.4"
|
||||
@@ -28,16 +28,16 @@ com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.
|
||||
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
|
||||
com-unboundid-unboundid-ldapsdk7 = "com.unboundid:unboundid-ldapsdk:7.0.1"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.3"
|
||||
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.4"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.14"
|
||||
io-mockk = "io.mockk:mockk:1.14.7"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.13"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.17"
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
|
||||
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
|
||||
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.13"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.15"
|
||||
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
@@ -46,7 +46,7 @@ jakarta-servlet-jsp-jakarta-servlet-jsp-api = "jakarta.servlet.jsp:jakarta.servl
|
||||
jakarta-servlet-jsp-jstl-jakarta-servlet-jsp-jstl-api = "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2"
|
||||
jakarta-websocket-jakarta-websocket-api = { module = "jakarta.websocket:jakarta.websocket-api", version.ref = "jakarta-websocket" }
|
||||
jakarta-websocket-jakarta-websocket-client-api = { module = "jakarta.websocket:jakarta.websocket-client-api", version.ref = "jakarta-websocket" }
|
||||
jakarta-xml-bind-jakarta-xml-bind-api = "jakarta.xml.bind:jakarta.xml.bind-api:4.0.4"
|
||||
jakarta-xml-bind-jakarta-xml-bind-api = "jakarta.xml.bind:jakarta.xml.bind-api:4.0.5"
|
||||
ldapsdk = "ldapsdk:ldapsdk:4.1"
|
||||
net-sourceforge-htmlunit = "net.sourceforge.htmlunit:htmlunit:2.70.0"
|
||||
org-htmlunit-htmlunit = "org.htmlunit:htmlunit:4.11.1"
|
||||
@@ -57,7 +57,7 @@ org-apache-directory-server-apacheds-protocol-shared = { module = "org.apache.di
|
||||
org-apache-directory-server-apacheds-server-jndi = { module = "org.apache.directory.server:apacheds-server-jndi", version.ref = "org-apache-directory-server" }
|
||||
org-apache-directory-shared-shared-ldap = "org.apache.directory.shared:shared-ldap:0.9.15"
|
||||
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents:httpclient:4.5.14"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.11"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.15"
|
||||
org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" }
|
||||
@@ -65,13 +65,13 @@ org-apereo-cas-client-cas-client-core = "org.apereo.cas.client:cas-client-core:4
|
||||
io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:8.13.1"
|
||||
org-aspectj-aspectjrt = { module = "org.aspectj:aspectjrt", version.ref = "org-aspectj" }
|
||||
org-aspectj-aspectjweaver = { module = "org.aspectj:aspectjweaver", version.ref = "org-aspectj" }
|
||||
org-assertj-assertj-core = "org.assertj:assertj-core:3.27.6"
|
||||
org-assertj-assertj-core = "org.assertj:assertj-core:3.27.7"
|
||||
org-bouncycastle-bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "org-bouncycastle" }
|
||||
org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "org-bouncycastle" }
|
||||
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
|
||||
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
|
||||
org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.39.Final"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.49.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
|
||||
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
|
||||
@@ -89,8 +89,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.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.1.12"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.15"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.1.13"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.16"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
distributionSha256Sum=f1771298a70f6db5a29daf62378c4e18a17fc33c9ba6b14362e0cdf40610380d
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
@rem
|
||||
@rem Copyright 2004-present the original author or authors.
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
|
||||
+5
-1
@@ -300,7 +300,11 @@ public final class OidcAuthorizedClientRefreshedEventListener
|
||||
return;
|
||||
}
|
||||
|
||||
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
|
||||
// The auth_time claim MUST represent the time of the original authentication OR
|
||||
// the most recent time when the end-user reauthenticated when "prompt=login" is
|
||||
// passed in the authentication request
|
||||
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())
|
||||
&& !idToken.getAuthenticatedAt().isAfter(existingOidcUser.getIdToken().getAuthenticatedAt())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
|
||||
REFRESH_TOKEN_RESPONSE_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
|
||||
+1
@@ -269,6 +269,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class OAuth2AuthorizationRequestException extends AuthenticationException {
|
||||
|
||||
OAuth2AuthorizationRequestException(Throwable cause) {
|
||||
|
||||
+21
-3
@@ -419,8 +419,8 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtDoesNotMatchThenThrowsOAuth2AuthenticationException() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.SECONDS);
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtBeforeCurrentAuthenticatedAtThenThrowsOAuth2AuthenticationException() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().minus(5, ChronoUnit.MINUTES);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
|
||||
@@ -440,8 +440,26 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
|
||||
verifyNoInteractions(this.userService, this.applicationEventPublisher);
|
||||
}
|
||||
|
||||
// gh-18839
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesThenOidcUserRefreshedEventPublished() {
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtAfterCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
|
||||
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.MINUTES);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
|
||||
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
|
||||
given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
|
||||
|
||||
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
|
||||
this.accessTokenResponse, this.authorizedClient);
|
||||
this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
|
||||
|
||||
verify(this.applicationEventPublisher).publishEvent(any(OidcUserRefreshedEvent.class));
|
||||
verifyNoMoreInteractions(this.applicationEventPublisher);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesCurrentAuthenticatedAtThenOidcUserRefreshedEventPublished() {
|
||||
Instant authTime = this.authentication.getPrincipal().getAttribute(IdTokenClaimNames.AUTH_TIME);
|
||||
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
|
||||
|
||||
+35
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.core.oidc.user;
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
@@ -114,4 +115,38 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
||||
return this.userInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || this.getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DefaultOidcUser that = (DefaultOidcUser) obj;
|
||||
if (!this.getName().equals(that.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (!this.getAuthorities().equals(that.getAuthorities())) {
|
||||
return false;
|
||||
}
|
||||
if (this.getIdToken().getIssuer() == null || that.getIdToken().getIssuer() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(this.getIdToken().getIssuer().toExternalForm(),
|
||||
that.getIdToken().getIssuer().toExternalForm())
|
||||
&& Objects.equals(this.getIdToken().getSubject(), that.getIdToken().getSubject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.getName().hashCode();
|
||||
result = 31 * result + this.getAuthorities().hashCode();
|
||||
result = 31 * result + ((this.getIdToken().getIssuer() != null)
|
||||
? this.getIdToken().getIssuer().toExternalForm().hashCode() : 0);
|
||||
result = 31 * result
|
||||
+ ((this.getIdToken().getSubject() != null) ? this.getIdToken().getSubject().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.core.oidc.user;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -147,4 +148,31 @@ public class DefaultOidcUserTests {
|
||||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
||||
}
|
||||
|
||||
// gh-18622
|
||||
@Test
|
||||
public void equalsWhenOidcUserPrincipalSameThenTrue() {
|
||||
String issuer = "https://example.com";
|
||||
String subject = "subject-1";
|
||||
|
||||
// @formatter:off
|
||||
OidcIdToken idToken1 = OidcIdToken.withTokenValue("id-token-value-1")
|
||||
.issuer(issuer)
|
||||
.subject(subject)
|
||||
.issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
|
||||
.build();
|
||||
|
||||
OidcIdToken idToken2 = OidcIdToken.withTokenValue("id-token-value-2")
|
||||
.issuer(issuer)
|
||||
.subject(subject)
|
||||
.issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plus(30, ChronoUnit.MINUTES))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
DefaultOidcUser user1 = new DefaultOidcUser(AUTHORITIES, idToken1, USER_INFO);
|
||||
DefaultOidcUser user2 = new DefaultOidcUser(AUTHORITIES, idToken2, USER_INFO);
|
||||
assertThat(user1).isEqualTo(user2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -185,6 +185,7 @@ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory<DPoPP
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final class JtiCache extends LinkedHashMap<String, Long> {
|
||||
|
||||
private static final int MAX_SIZE = 1000;
|
||||
|
||||
+13
-2
@@ -230,7 +230,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
.getConfigurationForIssuerLocation(issuer, rest);
|
||||
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
|
||||
return configuration.get("jwks_uri").toString();
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,6 +290,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
|
||||
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (rest) -> jwkSetUri;
|
||||
@@ -423,6 +426,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
Assert.notNull(validator, "validator cannot be null");
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
|
||||
if (this.signatureAlgorithms.isEmpty()) {
|
||||
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
|
||||
@@ -461,7 +470,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
* @return the configured {@link NimbusJwtDecoder}
|
||||
*/
|
||||
public NimbusJwtDecoder build() {
|
||||
return new NimbusJwtDecoder(processor());
|
||||
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
private static final class SpringJWKSource<C extends SecurityContext> implements JWKSetSource<C> {
|
||||
|
||||
+12
-2
@@ -241,7 +241,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
}
|
||||
return Mono.just(configuration.get("jwks_uri").toString());
|
||||
}),
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,6 +333,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
|
||||
private BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
|
||||
@@ -456,6 +459,11 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer(
|
||||
BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer) {
|
||||
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
|
||||
@@ -468,7 +476,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
* @return the configured {@link NimbusReactiveJwtDecoder}
|
||||
*/
|
||||
public NimbusReactiveJwtDecoder build() {
|
||||
return new NimbusReactiveJwtDecoder(processor());
|
||||
NimbusReactiveJwtDecoder decoder = new NimbusReactiveJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
|
||||
|
||||
+16
-1
@@ -328,11 +328,26 @@ public class NimbusJwtDecoderTests {
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
|
||||
.restOperations(restOperations)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class)))
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
assertThatExceptionOfType(JwtValidationException.class).isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
|
||||
// @formatter:off
|
||||
|
||||
+22
-2
@@ -617,11 +617,31 @@ public class NimbusReactiveJwtDecoderTests {
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
WebClient real = WebClient.builder().build();
|
||||
WebClient.RequestHeadersUriSpec spec = spy(real.get());
|
||||
WebClient webClient = spy(WebClient.class);
|
||||
given(webClient.get()).willReturn(spec);
|
||||
WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
|
||||
given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
assertThatExceptionOfType(JwtValidationException.class)
|
||||
.isThrownBy(() -> jwtDecoder.decode(this.messageReadToken).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
@@ -30,6 +30,23 @@ import org.springframework.util.Assert;
|
||||
* A {@link Jwt} to {@link GrantedAuthority} {@link Converter} that is a composite of
|
||||
* converters.
|
||||
*
|
||||
* <p>
|
||||
* This is handy when needing to read authorities from multiple locations in a JWT; each
|
||||
* underlying converter is called in series and the results are aggregated into a single
|
||||
* collection of authorities.
|
||||
*
|
||||
* <p>
|
||||
* For example, you might have a claim called "scope" and another called "roles". With
|
||||
* {@link DelegatingJwtGrantedAuthoritiesConverter}, you can do:
|
||||
*
|
||||
* <code>
|
||||
* JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
|
||||
* JwtGrantedAuthoritiesConverter roles = new JwtGrantedAUthoritiesConverter();
|
||||
* roles.setAuthoritiesClaimName("roles");
|
||||
* roles.setAuthorityPrefix("ROLE_");
|
||||
* return new DelegatingJwtGrantedAuthoritiesConverter(scopes, roles);
|
||||
* </code>
|
||||
*
|
||||
* @author Laszlo Stahorszki
|
||||
* @author Josh Cummings
|
||||
* @since 5.5
|
||||
|
||||
-3
@@ -36,9 +36,6 @@ import org.springframework.util.Assert;
|
||||
* Uses an expression for extracting the token claim value to use for mapping
|
||||
* {@link GrantedAuthority authorities}.
|
||||
*
|
||||
* Note this can be used in combination with a
|
||||
* {@link DelegatingJwtGrantedAuthoritiesConverter}.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @since 6.4
|
||||
*/
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
@@ -33,6 +34,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class Saml2AuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5225098478444036532L;
|
||||
|
||||
private final RelyingPartyRegistration relyingPartyRegistration;
|
||||
|
||||
private final String saml2Response;
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
* </pre>
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("serial")
|
||||
public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistration {
|
||||
|
||||
OpenSamlRelyingPartyRegistration(RelyingPartyRegistration registration) {
|
||||
|
||||
+3
-1
@@ -35,6 +35,7 @@ import org.springframework.util.Assert;
|
||||
* that it was for the user trying to log in. Please see the reference for details.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @author Andrey Litvitski
|
||||
* @since 6.5
|
||||
*/
|
||||
public final class CacheSaml2AuthenticationRequestRepository
|
||||
@@ -52,7 +53,8 @@ public final class CacheSaml2AuthenticationRequestRepository
|
||||
@Override
|
||||
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
|
||||
Assert.notNull(authenticationRequest, "authenticationRequest must not be null");
|
||||
String relayState = authenticationRequest.getRelayState();
|
||||
Assert.notNull(relayState, "relayState must not be null");
|
||||
this.cache.put(relayState, authenticationRequest);
|
||||
}
|
||||
|
||||
+9
-7
@@ -42,9 +42,10 @@ class CacheSaml2AuthenticationRequestRepositoryTests {
|
||||
|
||||
@Test
|
||||
void loadAuthenticationRequestWhenCachedThenReturns() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, "test");
|
||||
Saml2PostAuthenticationRequest authenticationRequest = TestSaml2PostAuthenticationRequests.create();
|
||||
String relayState = authenticationRequest.getRelayState();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, relayState);
|
||||
this.repository.saveAuthenticationRequest(authenticationRequest, request, null);
|
||||
assertThat(this.repository.loadAuthenticationRequest(request)).isEqualTo(authenticationRequest);
|
||||
this.repository.removeAuthenticationRequest(request, null);
|
||||
@@ -77,15 +78,16 @@ class CacheSaml2AuthenticationRequestRepositoryTests {
|
||||
CacheSaml2AuthenticationRequestRepository repository = new CacheSaml2AuthenticationRequestRepository();
|
||||
Cache cache = spy(new ConcurrentMapCache("requests"));
|
||||
repository.setCache(cache);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, "test");
|
||||
Saml2PostAuthenticationRequest authenticationRequest = TestSaml2PostAuthenticationRequests.create();
|
||||
String relayState = authenticationRequest.getRelayState();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, relayState);
|
||||
repository.saveAuthenticationRequest(authenticationRequest, request, null);
|
||||
verify(cache).put(eq("test"), any());
|
||||
verify(cache).put(eq(relayState), any());
|
||||
repository.loadAuthenticationRequest(request);
|
||||
verify(cache).get("test", AbstractSaml2AuthenticationRequest.class);
|
||||
verify(cache).get(relayState, AbstractSaml2AuthenticationRequest.class);
|
||||
repository.removeAuthenticationRequest(request, null);
|
||||
verify(cache).evict("test");
|
||||
verify(cache).evict(relayState);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ pluginManagement {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "io.spring.develocity.conventions" version "0.0.24"
|
||||
id "io.spring.develocity.conventions" version "0.0.25"
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
|
||||
@@ -254,6 +254,11 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||
|
||||
/**
|
||||
* Convenience method, mainly for testing.
|
||||
* <p>
|
||||
* Attempt to find the matching filter chain based on the given {@code url}. Note that
|
||||
* the URI is often not enough information and this method should be used with
|
||||
* caution. Instead, consider using Spring Security's testing support that mocks a
|
||||
* full HTTP request.
|
||||
* @param url the URL
|
||||
* @return matching filter list
|
||||
*/
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ import org.springframework.security.web.FilterInvocation;
|
||||
* {@link AuthorizationManager}.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("serial")
|
||||
class WebExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<FilterInvocation> {
|
||||
|
||||
private final Expression authorizeExpression;
|
||||
|
||||
+7
-2
@@ -112,9 +112,14 @@ public abstract class AbstractAuthenticationTargetUrlRequestHandler {
|
||||
trace("Using url %s from request parameter %s", targetUrlParameterValue, this.targetUrlParameter);
|
||||
return targetUrlParameterValue;
|
||||
}
|
||||
|
||||
String refererHeader = request.getHeader("Referer");
|
||||
if (!StringUtils.hasText(refererHeader)) {
|
||||
return this.defaultTargetUrl;
|
||||
}
|
||||
if (this.useReferer) {
|
||||
trace("Using url %s from Referer header", request.getHeader("Referer"));
|
||||
return request.getHeader("Referer");
|
||||
trace("Using url %s from Referer header", refererHeader);
|
||||
return refererHeader;
|
||||
}
|
||||
return this.defaultTargetUrl;
|
||||
}
|
||||
|
||||
+1
-7
@@ -52,18 +52,12 @@ public final class HaveIBeenPwnedRestApiPasswordChecker implements CompromisedPa
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final MessageDigest sha1Digest;
|
||||
|
||||
private RestClient restClient = RestClient.builder().baseUrl(API_URL).build();
|
||||
|
||||
public HaveIBeenPwnedRestApiPasswordChecker() {
|
||||
this.sha1Digest = getSha1Digest();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public CompromisedPasswordDecision check(String password) {
|
||||
byte[] hash = this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] hash = getSha1Digest().digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
String encoded = new String(Hex.encode(hash)).toUpperCase(Locale.ROOT);
|
||||
String prefix = encoded.substring(0, PREFIX_LENGTH);
|
||||
String suffix = encoded.substring(PREFIX_LENGTH);
|
||||
|
||||
+1
-7
@@ -54,12 +54,6 @@ public class HaveIBeenPwnedRestApiReactivePasswordChecker implements ReactiveCom
|
||||
|
||||
private WebClient webClient = WebClient.builder().baseUrl(API_URL).build();
|
||||
|
||||
private final MessageDigest sha1Digest;
|
||||
|
||||
public HaveIBeenPwnedRestApiReactivePasswordChecker() {
|
||||
this.sha1Digest = getSha1Digest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CompromisedPasswordDecision> check(String password) {
|
||||
return getHash(password).map((hash) -> new String(Hex.encode(hash)))
|
||||
@@ -95,7 +89,7 @@ public class HaveIBeenPwnedRestApiReactivePasswordChecker implements ReactiveCom
|
||||
}
|
||||
|
||||
private Mono<byte[]> getHash(String password) {
|
||||
return Mono.fromSupplier(() -> this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8)))
|
||||
return Mono.fromSupplier(() -> getSha1Digest().digest(password.getBytes(StandardCharsets.UTF_8)))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.publishOn(Schedulers.parallel());
|
||||
}
|
||||
|
||||
+1
@@ -91,6 +91,7 @@ public class CookieRequestCache implements RequestCache {
|
||||
.setServerPort(port)
|
||||
.setMethod(request.getMethod())
|
||||
.setLocales(Collections.list(request.getLocales()))
|
||||
.setParameters(request.getParameterMap())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -103,11 +103,11 @@ public class HttpSessionRequestCache implements RequestCache {
|
||||
@Override
|
||||
public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) {
|
||||
if (this.matchingRequestParameterName != null) {
|
||||
if (!StringUtils.hasText(request.getQueryString())
|
||||
|| !UriComponentsBuilder.fromUriString(UrlUtils.buildRequestUrl(request))
|
||||
.build()
|
||||
.getQueryParams()
|
||||
.containsKey(this.matchingRequestParameterName)) {
|
||||
if (!StringUtils.hasText(request.getQueryString()) || !UriComponentsBuilder.newInstance()
|
||||
.query(request.getQueryString())
|
||||
.build()
|
||||
.getQueryParams()
|
||||
.containsKey(this.matchingRequestParameterName)) {
|
||||
this.logger.trace(
|
||||
"matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided");
|
||||
return null;
|
||||
|
||||
+2
-1
@@ -49,7 +49,8 @@ public final class ServerOneTimeTokenAuthenticationConverter implements ServerAu
|
||||
Assert.notNull(exchange, "exchange cannot be null");
|
||||
if (isFormEncodedRequest(exchange.getRequest())) {
|
||||
return exchange.getFormData()
|
||||
.map((data) -> OneTimeTokenAuthenticationToken.unauthenticated(data.getFirst(TOKEN)));
|
||||
.mapNotNull((data) -> data.getFirst(TOKEN))
|
||||
.map((data) -> OneTimeTokenAuthenticationToken.unauthenticated(data));
|
||||
}
|
||||
String token = resolveTokenFromRequest(exchange.getRequest());
|
||||
if (!StringUtils.hasText(token)) {
|
||||
|
||||
+13
@@ -305,6 +305,19 @@ public final class PathPatternRequestMatcher implements RequestMatcher {
|
||||
return this.method.name().equals(request.getMethod());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof HttpMethodRequestMatcher that)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(this.method, that.method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpMethod [" + this.method + "]";
|
||||
|
||||
+30
-2
@@ -58,10 +58,38 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
|
||||
|
||||
@Override
|
||||
public void addHeader(String name, String value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.addHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIntHeader(String name, int value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.addIntHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHeader(String name, String value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.setHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIntHeader(String name, int value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.setIntHeader(name, value);
|
||||
}
|
||||
|
||||
private void checkContentLengthHeader(String name, int value) {
|
||||
if ("Content-Length".equalsIgnoreCase(name)) {
|
||||
setContentLength(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkContentLengthHeader(String name, String value) {
|
||||
if (value != null && "Content-Length".equalsIgnoreCase(name)) {
|
||||
setContentLength(Long.parseLong(value));
|
||||
}
|
||||
super.addHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -477,7 +505,7 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
|
||||
|
||||
@Override
|
||||
public PrintWriter append(CharSequence csq) {
|
||||
checkContentLength(csq.length());
|
||||
checkContentLength((csq != null) ? csq.length() : 4);
|
||||
return this.delegate.append(csq);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
* @author Eddú Meléndez
|
||||
* @since 3.0.2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RequestMatcher {
|
||||
|
||||
/**
|
||||
|
||||
+4
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.web.webauthn.api;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -34,6 +35,9 @@ import java.util.List;
|
||||
*/
|
||||
public final class AuthenticatorAttestationResponse extends AuthenticatorResponse {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -1628559840895428945L;
|
||||
|
||||
private final Bytes attestationObject;
|
||||
|
||||
private final List<AuthenticatorTransport> transports;
|
||||
|
||||
+5
-6
@@ -56,11 +56,8 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
|
||||
throws IOException, JacksonException {
|
||||
List<AuthenticationExtensionsClientOutput<?>> outputs = new ArrayList<>();
|
||||
for (String key = parser.nextFieldName(); key != null; key = parser.nextFieldName()) {
|
||||
JsonToken startObject = parser.nextValue();
|
||||
if (startObject != JsonToken.START_OBJECT) {
|
||||
break;
|
||||
}
|
||||
if (CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
|
||||
JsonToken next = parser.nextToken();
|
||||
if (next == JsonToken.START_OBJECT && CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
|
||||
CredentialPropertiesOutput output = parser.readValueAs(CredentialPropertiesOutput.class);
|
||||
outputs.add(output);
|
||||
}
|
||||
@@ -68,7 +65,9 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping unknown extension with id " + key);
|
||||
}
|
||||
parser.nextValue();
|
||||
if (next.isStructStart()) {
|
||||
parser.skipChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.webauthn.management;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.CredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthorizationManager} that grants access when the {@link CredentialRecord}
|
||||
* identified by the provided credential id is owned by the currently authenticated user.
|
||||
*
|
||||
* <p>
|
||||
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
|
||||
* specification</a>, a credential id must contain at least 16 bytes with at least 100
|
||||
* bits of entropy, making it practically unguessable. The specification also advises that
|
||||
* credential ids should be kept private, as exposing them can leak personally identifying
|
||||
* information (see
|
||||
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§ 14.6.3
|
||||
* Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is therefore
|
||||
* intended as defense in depth: even if a credential id were somehow exposed, an
|
||||
* unauthorized user could not delete another user's credential.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.5.10
|
||||
*/
|
||||
public final class CredentialRecordOwnerAuthorizationManager implements AuthorizationManager<Bytes> {
|
||||
|
||||
private final AuthenticatedAuthorizationManager<Bytes> authenticatedAuthorizationManager = AuthenticatedAuthorizationManager
|
||||
.authenticated();
|
||||
|
||||
private final UserCredentialRepository userCredentials;
|
||||
|
||||
private final PublicKeyCredentialUserEntityRepository userEntities;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param userCredentials the {@link UserCredentialRepository} to use
|
||||
* @param userEntities the {@link PublicKeyCredentialUserEntityRepository} to use
|
||||
*/
|
||||
public CredentialRecordOwnerAuthorizationManager(UserCredentialRepository userCredentials,
|
||||
PublicKeyCredentialUserEntityRepository userEntities) {
|
||||
Assert.notNull(userCredentials, "userCredentials cannot be null");
|
||||
Assert.notNull(userEntities, "userEntities cannot be null");
|
||||
this.userCredentials = userCredentials;
|
||||
this.userEntities = userEntities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, Bytes credentialId) {
|
||||
AuthorizationDecision decision = this.authenticatedAuthorizationManager.check(authentication, credentialId);
|
||||
if (!decision.isGranted()) {
|
||||
return decision;
|
||||
}
|
||||
Authentication auth = authentication.get();
|
||||
CredentialRecord credential = this.userCredentials.findByCredentialId(credentialId);
|
||||
if (credential == null) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
if (credential.getUserEntityUserId() == null) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
PublicKeyCredentialUserEntity userEntity = this.userEntities.findByUsername(auth.getName());
|
||||
if (userEntity == null) {
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
return new AuthorizationDecision(credential.getUserEntityUserId().equals(userEntity.getId()));
|
||||
}
|
||||
|
||||
}
|
||||
+58
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.web.webauthn.registration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
@@ -34,6 +35,12 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.SingleResultAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
@@ -87,6 +94,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final UserCredentialRepository userCredentials;
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
|
||||
JsonMapper.builder().addModule(new WebauthnJackson2Module()).build());
|
||||
|
||||
@@ -98,6 +108,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
private RequestMatcher removeCredentialMatcher = PathPatternRequestMatcher.withDefaults()
|
||||
.matcher(HttpMethod.DELETE, "/webauthn/register/{id}");
|
||||
|
||||
private AuthorizationManager<Bytes> deleteCredentialAuthorizationManager = SingleResultAuthorizationManager
|
||||
.denyAll();
|
||||
|
||||
public WebAuthnRegistrationFilter(UserCredentialRepository userCredentials,
|
||||
WebAuthnRelyingPartyOperations rpOptions) {
|
||||
Assert.notNull(userCredentials, "userCredentials must not be null");
|
||||
@@ -132,6 +145,42 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
this.removeCredentialMatcher = removeCredentialMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthorizationManager} used to authorize the delete credential
|
||||
* operation. The object being authorized is the credential id as {@link Bytes}. By
|
||||
* default, all delete requests are denied.
|
||||
*
|
||||
* <p>
|
||||
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
|
||||
* specification</a>, a credential id must contain at least 16 bytes with at least 100
|
||||
* bits of entropy, making it practically unguessable. The specification also advises
|
||||
* that credential ids should be kept private, as exposing them can leak personally
|
||||
* identifying information (see
|
||||
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§
|
||||
* 14.6.3 Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is
|
||||
* therefore intended as defense in depth: even if a credential id were somehow
|
||||
* exposed, an unauthorized user could not delete another user's credential.
|
||||
* @param deleteCredentialAuthorizationManager the {@link AuthorizationManager} to use
|
||||
* @since 6.5.10
|
||||
*/
|
||||
public void setDeleteCredentialAuthorizationManager(
|
||||
AuthorizationManager<Bytes> deleteCredentialAuthorizationManager) {
|
||||
Assert.notNull(deleteCredentialAuthorizationManager, "deleteCredentialAuthorizationManager cannot be null");
|
||||
this.deleteCredentialAuthorizationManager = deleteCredentialAuthorizationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextHolderStrategy} to use. The default is
|
||||
* {@link SecurityContextHolder#getContextHolderStrategy()}.
|
||||
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
|
||||
* use
|
||||
* @since 6.5.10
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
|
||||
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
@@ -203,7 +252,15 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
||||
|
||||
private void removeCredential(HttpServletRequest request, HttpServletResponse response, String id)
|
||||
throws IOException {
|
||||
this.userCredentials.delete(Bytes.fromBase64(id));
|
||||
Bytes credentialId = Bytes.fromBase64(id);
|
||||
Supplier<Authentication> authentication = () -> this.securityContextHolderStrategy.getContext()
|
||||
.getAuthentication();
|
||||
AuthorizationResult result = this.deleteCredentialAuthorizationManager.authorize(authentication, credentialId);
|
||||
if (result != null && !result.isGranted()) {
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
return;
|
||||
}
|
||||
this.userCredentials.delete(credentialId);
|
||||
response.setStatus(HttpStatus.NO_CONTENT.value());
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -114,4 +114,12 @@ public class AbstractAuthenticationTargetUrlRequestHandlerTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setRedirectStrategy(null));
|
||||
}
|
||||
|
||||
// gh-18805
|
||||
@Test
|
||||
void returnDefaultUrlIfUseRefererIsTrueAndRefererHeaderIsEmpty() {
|
||||
this.handler.setUseReferer(true);
|
||||
this.request.addHeader("Referer", "");
|
||||
assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(DEFAULT_TARGET_URL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
@@ -108,6 +108,21 @@ public class CookieRequestCacheTests {
|
||||
assertThat(savedRequest.getRedirectUrl()).isEqualTo(redirectUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRequestWhenRequestContainsSavedRequestCookieThenSavedRequestContainsRequestParameters() {
|
||||
CookieRequestCache cookieRequestCache = new CookieRequestCache();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie("https://abc.com/destination")));
|
||||
request.setParameter("single", "first");
|
||||
request.addParameter("multi", "second");
|
||||
request.addParameter("multi", "third");
|
||||
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
|
||||
assertThat(savedRequest).isNotNull();
|
||||
assertThat(savedRequest.getParameterValues("single")).containsExactly("first");
|
||||
assertThat(savedRequest.getParameterValues("multi")).containsExactly("second", "third");
|
||||
assertThat(savedRequest.getParameterMap()).containsKeys("single", "multi");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchingRequestWhenRequestDoesNotContainSavedRequestCookieThenReturnsNull() {
|
||||
CookieRequestCache cookieRequestCache = new CookieRequestCache();
|
||||
|
||||
+15
@@ -168,6 +168,21 @@ public class HttpSessionRequestCacheTests {
|
||||
verify(request, never()).getParameterMap();
|
||||
}
|
||||
|
||||
// gh-16656
|
||||
@Test
|
||||
public void getMatchingRequestWhenMatchingRequestPathContainsPercentSignThenLookedUp() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setServletPath("/30 % off");
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
cache.saveRequest(request, new MockHttpServletResponse());
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setServletPath("/30 % off");
|
||||
requestToMatch.setQueryString("continue");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNotNull();
|
||||
}
|
||||
|
||||
private static final class CustomSavedRequest implements SavedRequest {
|
||||
|
||||
private final SavedRequest delegate;
|
||||
|
||||
+12
@@ -72,6 +72,18 @@ public class ServerOneTimeTokenAuthenticationConverterTests {
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
// gh-18973
|
||||
@Test
|
||||
void convertWhenNoTokenFormParameterThenNull() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body("username=Max");
|
||||
|
||||
Authentication authentication = this.converter.convert(MockServerWebExchange.from(request)).block();
|
||||
|
||||
assertThat(authentication).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertWhenTokenEncodedFormParameterThenReturnOneTimeTokenAuthenticationToken() {
|
||||
// @formatter:off
|
||||
|
||||
+11
@@ -145,6 +145,17 @@ public class PathPatternRequestMatcherTests {
|
||||
assertThat(matcher.matches(mock)).isTrue();
|
||||
}
|
||||
|
||||
// gh-18911
|
||||
@Test
|
||||
void testEqualsWithSameAndDifferentHttpMethod() {
|
||||
PathPatternRequestMatcher.Builder builder = PathPatternRequestMatcher.withDefaults();
|
||||
PathPatternRequestMatcher matcher1 = builder.matcher(HttpMethod.GET, "/foo");
|
||||
PathPatternRequestMatcher matcher2 = builder.matcher(HttpMethod.GET, "/foo");
|
||||
PathPatternRequestMatcher matcher3 = builder.matcher(HttpMethod.POST, "/foo");
|
||||
assertThat(matcher1).isEqualTo(matcher2);
|
||||
assertThat(matcher1).isNotEqualTo(matcher3);
|
||||
}
|
||||
|
||||
MockHttpServletRequest request(String uri) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
|
||||
+142
@@ -28,7 +28,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -1006,6 +1009,63 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.addHeader(null, "value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addHeaderNullValueDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.addHeader(HttpHeaders.CONTENT_LENGTH, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
int expected = 1234;
|
||||
this.response.addIntHeader("Content-Length", String.valueOf(expected).length());
|
||||
this.response.getWriter().write(expected);
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIntHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.addIntHeader(null, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
int expected = 1234;
|
||||
this.response.setHeader("Content-Length", String.valueOf(String.valueOf(expected).length()));
|
||||
this.response.getWriter().write(expected);
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.setHeader(null, "value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHeaderNullValueDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.setHeader(HttpHeaders.CONTENT_LENGTH, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
int expected = 1234;
|
||||
this.response.setIntHeader("Content-Length", String.valueOf(expected).length());
|
||||
this.response.getWriter().write(expected);
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setIntHeaderNullNameDoesNotThrow() {
|
||||
assertThatNoException().isThrownBy(() -> this.response.setIntHeader(null, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bufferSizePrintWriterWriteCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
@@ -1027,4 +1087,86 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintNullStringDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().print(s));
|
||||
verify(this.writer).print(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintlnNullStringDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().println(s));
|
||||
verify(this.writer).println(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintNullObjectDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
Object obj = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().print(obj));
|
||||
verify(this.writer).print(obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterPrintlnNullObjectDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
Object obj = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().println(obj));
|
||||
verify(this.writer).println(obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterWriteNullStringDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().write(s));
|
||||
verify(this.writer).write(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterAppendNullCharSequenceDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
CharSequence csq = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().append(csq));
|
||||
verify(this.writer).append(csq);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printWriterAppendNullCharSequenceIntIntDoesNotThrow() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
CharSequence csq = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getWriter().append(csq, 0, 3));
|
||||
verify(this.writer).append(csq, 0, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outputStreamPrintNullStringDoesNotThrow() throws Exception {
|
||||
givenGetOutputStreamThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getOutputStream().print(s));
|
||||
verify(this.out).print(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outputStreamPrintlnNullStringDoesNotThrow() throws Exception {
|
||||
givenGetOutputStreamThenReturn();
|
||||
String s = null;
|
||||
assertThatNoException().isThrownBy(() -> this.response.getOutputStream().println(s));
|
||||
verify(this.out).println(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendErrorWithNullMsgDoesNotThrow() throws Exception {
|
||||
assertThatNoException().isThrownBy(() -> this.response.sendError(400, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendRedirectWithNullLocationDoesNotThrow() throws Exception {
|
||||
assertThatNoException().isThrownBy(() -> this.response.sendRedirect(null));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -122,6 +122,47 @@ class JacksonTests {
|
||||
assertThat(outputs).usingRecursiveComparison().isEqualTo(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenAppId() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"appid": false,
|
||||
"credProps": {
|
||||
"rk": false
|
||||
}
|
||||
}
|
||||
""";
|
||||
CredentialPropertiesOutput credProps = new CredentialPropertiesOutput(false);
|
||||
|
||||
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
|
||||
AuthenticationExtensionsClientOutputs.class);
|
||||
assertThat(outputs.getOutputs()).usingRecursiveFieldByFieldElementComparator().contains(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenUnknownExtension() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"unknownObject1": {
|
||||
"key": "value"
|
||||
},
|
||||
"unknownArray": [
|
||||
{ "key": "value1" },
|
||||
{ "key": "value2" }
|
||||
],
|
||||
"credProps": {
|
||||
"rk": false
|
||||
},
|
||||
"unknownObject2": {}
|
||||
}
|
||||
""";
|
||||
CredentialPropertiesOutput credProps = new CredentialPropertiesOutput(false);
|
||||
|
||||
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
|
||||
AuthenticationExtensionsClientOutputs.class);
|
||||
assertThat(outputs.getOutputs()).usingRecursiveFieldByFieldElementComparator().contains(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenFieldAfter() throws Exception {
|
||||
String json = """
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.webauthn.management;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.TestBytes;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link CredentialRecordOwnerAuthorizationManager}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 6.5.10
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CredentialRecordOwnerAuthorizationManagerTests {
|
||||
|
||||
@Mock
|
||||
private UserCredentialRepository userCredentials;
|
||||
|
||||
@Mock
|
||||
private PublicKeyCredentialUserEntityRepository userEntities;
|
||||
|
||||
@Test
|
||||
void constructorWhenNullUserCredentialsThenIllegalArgument() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(null, this.userEntities));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenNullUserEntitiesTonIllegalArgument() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(this.userCredentials, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenAuthenticationNullThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
AuthorizationDecision decision = manager.check(() -> null, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenNotAuthenticatedThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
|
||||
authentication.setAuthenticated(false);
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialNotFoundThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialUserEntityUserIdNullThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().userEntityUserId(null).build());
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenUserEntityNotFoundThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().build());
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialBelongsToUserThenGranted() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
Bytes userId = TestCredentialRecords.userCredential().build().getUserEntityUserId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().build());
|
||||
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(userId)
|
||||
.displayName("User")
|
||||
.build();
|
||||
given(this.userEntities.findByUsername("user")).willReturn(userEntity);
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenCredentialBelongsToDifferentUserThenDenied() {
|
||||
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
|
||||
this.userCredentials, this.userEntities);
|
||||
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
|
||||
given(this.userCredentials.findByCredentialId(credentialId))
|
||||
.willReturn(TestCredentialRecords.userCredential().build());
|
||||
PublicKeyCredentialUserEntity otherUserEntity = ImmutablePublicKeyCredentialUserEntity.builder()
|
||||
.name("user")
|
||||
.id(TestBytes.get())
|
||||
.displayName("User")
|
||||
.build();
|
||||
given(this.userEntities.findByUsername("user")).willReturn(otherUserEntity);
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
|
||||
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
|
||||
assertThat(decision.isGranted()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
+33
@@ -31,7 +31,11 @@ import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.SingleResultAuthorizationManager;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||
@@ -213,12 +217,41 @@ class WebAuthnRegistrationFilterTests {
|
||||
|
||||
@Test
|
||||
void doFilterWhenDeleteSuccessThenNoContent() throws Exception {
|
||||
this.filter.setDeleteCredentialAuthorizationManager(SingleResultAuthorizationManager.permitAll());
|
||||
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
||||
.buildRequest(new MockServletContext());
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setDeleteCredentialAuthorizationManagerWhenNullThenIllegalArgument() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setDeleteCredentialAuthorizationManager(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDeleteAndCustomAuthorizationManagerThenUses() throws Exception {
|
||||
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
|
||||
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(true));
|
||||
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
|
||||
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
||||
.buildRequest(new MockServletContext());
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
verify(authorizationManager).authorize(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDeleteAndAuthorizationDeniedThenForbidden() throws Exception {
|
||||
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
|
||||
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(false));
|
||||
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
|
||||
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
||||
.buildRequest(new MockServletContext());
|
||||
this.filter.doFilter(request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.FORBIDDEN.value());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest registerCredentialRequest(String body) {
|
||||
return MockMvcRequestBuilders.post(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL)
|
||||
.content(body)
|
||||
|
||||
Reference in New Issue
Block a user