Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae1537b409 | |||
| 49cddee343 | |||
| 1c56c0c0c8 | |||
| d79cf75dfb | |||
| e34c5e73e1 | |||
| 46254e01fb | |||
| effe682fc4 | |||
| 7f36155b47 | |||
| 888d87619d | |||
| e8028e15c0 | |||
| 482eb0e2cd | |||
| b0f8aa5ea0 | |||
| f75ac6c837 | |||
| 5e56fc13be | |||
| c0568ea9b0 | |||
| 4048b2bd7d | |||
| 86acba9d22 | |||
| a4cd6f4278 | |||
| 5da31ab8a8 | |||
| cae3467a8d | |||
| a17b2a18d9 | |||
| 0cbc38cdd6 | |||
| eb30fd7f59 | |||
| 260d298cc5 | |||
| 78a60d0d84 | |||
| a001f27690 | |||
| 27319e3f9b | |||
| ec462e8bc5 | |||
| a4111a606b | |||
| 505fe3abed | |||
| 9710492619 | |||
| 9436796973 | |||
| 51239359ed | |||
| e48f26e51e | |||
| 3b7e3a6c5c | |||
| a04025c114 | |||
| 1564076276 | |||
| ae09f36291 | |||
| bcef6ed74f | |||
| c8581683da | |||
| f631a0fcd5 | |||
| 0e84f31a00 | |||
| 9c76ab69f0 | |||
| 5354e4d2c5 | |||
| db48d4ca50 | |||
| 547d174f3e | |||
| d2d1275b39 | |||
| 7bf776ec38 | |||
| e47a6714a5 | |||
| b9cae82b89 | |||
| f6354250a1 | |||
| a5d963387b | |||
| 99c4f58c34 | |||
| c1aa99fdd2 | |||
| eb01394427 | |||
| 0d3d6f75f8 | |||
| eb83c35ded | |||
| 3c0fef59b5 | |||
| da94fbe431 | |||
| 857ef6fe08 | |||
| b7df86197c | |||
| c84c438075 | |||
| 1e7db094d1 | |||
| 456604ab45 | |||
| 15b9a50060 | |||
| fcc1bd598d | |||
| af2668f7cb | |||
| 883765b2de | |||
| 58e7ba4a4b | |||
| 72554f7f36 | |||
| 65e83f8e7a | |||
| ab6e9d2d1f | |||
| a53ca7c3d0 | |||
| af40d7e35a | |||
| daf8cfe8d2 | |||
| 75b537f99a | |||
| d1b7f8a119 | |||
| 5353d499b4 | |||
| 1d75b907f9 | |||
| 7dbd69fee1 | |||
| 147081f771 | |||
| 709d9bc039 | |||
| d9bb16e913 | |||
| 11114919ec | |||
| adb303e152 | |||
| 46f0dc6dfc | |||
| 36ea1b11a7 | |||
| e793a962c5 | |||
| 46cd94b5f4 | |||
| acd2de4553 | |||
| 31f593cca2 | |||
| 64bdcecdcd | |||
| bc0fd60e1a | |||
| a0cfb2777c | |||
| e8206b42d2 | |||
| 0737957f94 | |||
| 77892d571f | |||
| 06026684e5 | |||
| c3d45ae529 | |||
| acf19c1f1f | |||
| b3c880cc24 | |||
| 33d96d574f | |||
| b64d5af9c4 | |||
| 002dbf355a | |||
| 7d5414b349 | |||
| 2de2e3803a | |||
| 43a2fbf5ad | |||
| d6b295ba2c | |||
| 211fa52649 | |||
| 7f410ce5b4 | |||
| a3f6825f9c | |||
| ea0ec9e662 | |||
| 2e9e5d0555 | |||
| 92161aae2e | |||
| 3ffda83ba1 | |||
| d44923f64e | |||
| 15faf0f621 | |||
| a9f8a23e50 | |||
| 643a3f1206 | |||
| 7aafe2ed5a | |||
| ce90b85945 | |||
| 87888d42fc | |||
| 85ca9e9a57 | |||
| d9e9e3cdeb | |||
| 3e43eda42b | |||
| e7d6dc22b2 | |||
| da06f6a9e6 | |||
| 6a0b683e60 | |||
| 2b6d586987 | |||
| e528923878 | |||
| 8d1b0d9764 | |||
| 2fe7faea87 | |||
| eb313ec901 | |||
| 7873ab8601 | |||
| 348f064df1 | |||
| 8b9fe13c88 | |||
| 1f7dcc0fa6 | |||
| 908b9b5a85 | |||
| 807c3dd3ab | |||
| cac03995a3 | |||
| 6ea7da5178 | |||
| 49f1b3554c | |||
| 519df3f7d5 | |||
| 16272f634c | |||
| 0b9887505e | |||
| dd8ee38194 | |||
| 87de6cea1b | |||
| 3d1e4b5f18 | |||
| ef7b11ac01 | |||
| 0f85da77be | |||
| 96a9cf0d2d | |||
| ebb75f5e90 | |||
| a6c3d123ed | |||
| 324de7af93 | |||
| e79ceaeb75 | |||
| f361ee72e8 | |||
| 21ac1022ef | |||
| 68d91916e2 | |||
| 753f8aecc9 | |||
| 84e4c9bc42 | |||
| 1dbaa08cd4 | |||
| 1d916c35d1 | |||
| e8ba039a61 | |||
| 4b41f8cb5b | |||
| 0eb6acde96 | |||
| 73f3f75712 | |||
| 70a950119c | |||
| b8e9f47dd4 | |||
| a0a9b48224 | |||
| 9363959566 | |||
| f38129b5a0 | |||
| 6542509207 | |||
| 7d55c079c9 | |||
| e44fc3817a | |||
| 1414b88ef3 | |||
| ba520db7f7 | |||
| 85248083c0 | |||
| a91433d52b | |||
| d34bd346c0 | |||
| be09ed7e2b | |||
| d224dbe334 | |||
| c62168ca5b | |||
| d0b2b33dce | |||
| 7881660ca0 | |||
| 96c47b30d3 | |||
| 506e5b7f11 | |||
| a5cd7ce122 | |||
| a7bf8f7cc6 | |||
| 451fbf0227 | |||
| 0eaffb37e7 | |||
| d985d044bc | |||
| c24b5ebe98 | |||
| fd900c288e | |||
| e86d88d0cf | |||
| f28c26fd54 | |||
| 421430330a | |||
| 5c2106b22e | |||
| 48241deba3 | |||
| 9cc11be9f3 | |||
| e48d6b039b | |||
| dd3c6892e9 | |||
| c552366a78 | |||
| 7215c72373 |
@@ -1,41 +0,0 @@
|
||||
version: 2
|
||||
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "main"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
registries:
|
||||
- "spring-milestones"
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
# GitHub Actions
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: "main"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
- dependency-name: "sjohnr/*"
|
||||
- dependency-name: "spring-io/*"
|
||||
- dependency-name: "spring-security-release-tools/*"
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule
|
||||
- cron: '0 5 * * *'
|
||||
permissions: read-all
|
||||
jobs:
|
||||
codeql-analysis-call:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -64,39 +64,23 @@ jobs:
|
||||
./gradlew publishMavenJavaPublicationToLocalRepository
|
||||
./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR"
|
||||
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" :runAllTests
|
||||
check-tangles:
|
||||
name: Check for Package Tangles
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'spring-projects' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Check for package tangles
|
||||
env:
|
||||
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
|
||||
run: |
|
||||
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1
|
||||
with:
|
||||
should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
@@ -116,7 +100,7 @@ jobs:
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ perform-release ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
if: ${{ !success() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
name: Auto Merge Forward Dependabot Commits
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: dependabot-auto-merge-forward
|
||||
|
||||
jobs:
|
||||
get-supported-branches:
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
|
||||
with:
|
||||
project: spring-security
|
||||
type: oss
|
||||
repository_name: spring-projects/spring-security
|
||||
|
||||
auto-merge-forward-dependabot:
|
||||
name: Auto Merge Forward Dependabot Commits
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-supported-branches]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Setup GitHub User
|
||||
id: setup-gh-user
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Run Auto Merge Forward
|
||||
id: run-auto-merge-forward
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/auto-merge-forward@actions-v1
|
||||
with:
|
||||
branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
|
||||
from-author: dependabot[bot]
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [ auto-merge-forward-dependabot ]
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
uses: Gamesight/slack-workflow-status@v1.3.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
@@ -4,7 +4,8 @@ on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 2am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
jobs:
|
||||
upgrade_wrapper:
|
||||
name: Execution
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
name: Mark Duplicate Dependabot PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
check_duplicate_prs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Dependency Name from PR Title
|
||||
id: extract
|
||||
run: |
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}')
|
||||
echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find PRs
|
||||
id: find_duplicates
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "${{ steps.extract.outputs.dependency_name }}"' --json number --jq 'map(.number) | join(",")')
|
||||
echo "prs=$PRS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Label Duplicate PRs
|
||||
if: steps.find_duplicates.outputs.prs != ''
|
||||
env:
|
||||
PRS: ${{ steps.find_duplicates.outputs.prs }}
|
||||
CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
for i in ${PRS//,/ }
|
||||
do
|
||||
if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then
|
||||
echo "Marking PR $i as duplicate"
|
||||
gh pr edit "$i" --add-label "status: duplicate"
|
||||
gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER"
|
||||
fi
|
||||
done
|
||||
@@ -1,63 +0,0 @@
|
||||
name: Merge Dependabot PR
|
||||
|
||||
on: pull_request_target
|
||||
|
||||
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
merge-dependabot-pr:
|
||||
name: Merge Dependabot PR
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set Milestone to Dependabot Pull Request
|
||||
id: set-milestone
|
||||
run: |
|
||||
if test -f pom.xml
|
||||
then
|
||||
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
|
||||
else
|
||||
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
|
||||
fi
|
||||
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
|
||||
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
|
||||
|
||||
if [ -z $MILESTONE ]
|
||||
then
|
||||
gh run cancel ${{ github.run_id }}
|
||||
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
|
||||
else
|
||||
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
|
||||
echo mergeEnabled=true >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Merge Dependabot pull request
|
||||
if: steps.set-milestone.outputs.mergeEnabled
|
||||
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ merge-dependabot-pr ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
@@ -2,9 +2,6 @@ name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Trigger Dependabot Auto Merge Forward
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*.x'
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
trigger-worflow:
|
||||
name: Trigger Workflow
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
- id: trigger
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run dependabot-auto-merge-forward.yml -r main
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Update dependabot.yml
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
get-supported-branches:
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
|
||||
with:
|
||||
project: spring-security
|
||||
type: oss
|
||||
repository_name: spring-projects/spring-security
|
||||
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-supported-branches]
|
||||
if: ${{ (github.repository == 'spring-projects/spring-security') && (github.ref == 'refs/heads/main') }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: spring-io/spring-security-release-tools/.github/actions/generate-dependabot-yml@actions-v1
|
||||
name: Update dependabot.yml
|
||||
with:
|
||||
gradle-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
|
||||
github-actions-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main,docs-build
|
||||
gh-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Update dependabot.yml
|
||||
@@ -20,4 +20,5 @@ dependencies {
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
testAspect sourceSets.main.output
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml"
|
||||
|
||||
+1
-7
@@ -20,7 +20,6 @@ plugins {
|
||||
|
||||
apply plugin: 'io.spring.nohttp'
|
||||
apply plugin: 'locks'
|
||||
apply plugin: 's101'
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
apply plugin: 'org.jetbrains.kotlin.jvm'
|
||||
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
|
||||
@@ -117,16 +116,11 @@ tasks.register('cloneRepository', IncludeRepoTask) {
|
||||
outputDirectory = project.hasProperty("cloneOutputDirectory") ? project.file("$cloneOutputDirectory") : defaultDirectory
|
||||
}
|
||||
|
||||
s101 {
|
||||
repository = 'https://structure101.com/binaries/latest'
|
||||
configurationDirectory = project.file("etc/s101")
|
||||
}
|
||||
|
||||
wrapperUpgrade {
|
||||
gradle {
|
||||
'spring-security' {
|
||||
repo = 'spring-projects/spring-security'
|
||||
baseBranch = '6.1.x' // runs only on 6.1.x and the update is merged forward to main
|
||||
baseBranch = '6.3.x' // runs only on 6.3.x and the update is merged forward to main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ dependencies {
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||
testImplementation libs.com.squareup.okhttp3.mockwebserver
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class S101Plugin implements Plugin<Project> {
|
||||
|
||||
private void configure(JavaExec exec) {
|
||||
exec.setDescription("Runs Structure101 headless analysis, installing and configuring if necessary");
|
||||
exec.dependsOn("check");
|
||||
exec.dependsOn("assemble");
|
||||
Project project = exec.getProject();
|
||||
S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class);
|
||||
exec
|
||||
|
||||
@@ -6,5 +6,7 @@ dependencies {
|
||||
implementation 'org.springframework:spring-core'
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
|
||||
@@ -5,4 +5,6 @@ dependencies {
|
||||
optional 'ch.qos.logback:logback-classic'
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -22,4 +22,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
testImplementation 'org.skyscreamer:jsonassert'
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ dependencies {
|
||||
testImplementation libs.org.instancio.instancio.junit
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
def rncToXsd = tasks.named('rncToXsd', RncToXsd)
|
||||
|
||||
+10
-49
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.annotation.authentication.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -63,62 +62,24 @@ class InitializeAuthenticationProviderBeanManagerConfigurer extends GlobalAuthen
|
||||
if (auth.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
List<BeanWithName<AuthenticationProvider>> authenticationProviders = getBeansWithName(
|
||||
AuthenticationProvider.class);
|
||||
if (authenticationProviders.isEmpty()) {
|
||||
String[] beanNames = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(AuthenticationProvider.class);
|
||||
if (beanNames.length == 0) {
|
||||
return;
|
||||
}
|
||||
else if (authenticationProviders.size() > 1) {
|
||||
List<String> beanNames = authenticationProviders.stream().map(BeanWithName::getName).toList();
|
||||
else if (beanNames.length > 1) {
|
||||
this.logger.info(LogMessage.format("Found %s AuthenticationProvider beans, with names %s. "
|
||||
+ "Global Authentication Manager will not be configured with AuthenticationProviders. "
|
||||
+ "Consider publishing a single AuthenticationProvider bean, or wiring your Providers directly "
|
||||
+ "using the DSL.", authenticationProviders.size(), beanNames));
|
||||
+ "using the DSL.", beanNames.length, Arrays.toString(beanNames)));
|
||||
return;
|
||||
}
|
||||
AuthenticationProvider authenticationProvider = authenticationProviders.get(0).getBean();
|
||||
String authenticationProviderBeanName = authenticationProviders.get(0).getName();
|
||||
|
||||
AuthenticationProvider authenticationProvider = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBean(beanNames[0], AuthenticationProvider.class);
|
||||
auth.authenticationProvider(authenticationProvider);
|
||||
this.logger.info(LogMessage.format(
|
||||
"Global AuthenticationManager configured with AuthenticationProvider bean with name %s",
|
||||
authenticationProviderBeanName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of beans of the requested class, along with their names. If
|
||||
* there are no registered beans of that type, the list is empty.
|
||||
*/
|
||||
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
|
||||
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
|
||||
String[] beanNames = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(type);
|
||||
for (String beanName : beanNames) {
|
||||
T bean = InitializeAuthenticationProviderBeanManagerConfigurer.this.context.getBean(beanName, type);
|
||||
beanWithNames.add(new BeanWithName<T>(bean, beanName));
|
||||
}
|
||||
return beanWithNames;
|
||||
}
|
||||
|
||||
static class BeanWithName<T> {
|
||||
|
||||
private final T bean;
|
||||
|
||||
private final String name;
|
||||
|
||||
BeanWithName(T bean, String name) {
|
||||
this.bean = bean;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
T getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
beanNames[0]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-48
@@ -16,8 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.annotation.authentication.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -66,9 +65,10 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
|
||||
@Override
|
||||
public void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
List<BeanWithName<UserDetailsService>> userDetailsServices = getBeansWithName(UserDetailsService.class);
|
||||
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(UserDetailsService.class);
|
||||
if (auth.isConfigured()) {
|
||||
if (!userDetailsServices.isEmpty()) {
|
||||
if (beanNames.length > 0) {
|
||||
this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. "
|
||||
+ "UserDetailsService beans will not be used for username/password login. "
|
||||
+ "Consider removing the AuthenticationProvider bean. "
|
||||
@@ -78,19 +78,18 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
return;
|
||||
}
|
||||
|
||||
if (userDetailsServices.isEmpty()) {
|
||||
if (beanNames.length == 0) {
|
||||
return;
|
||||
}
|
||||
else if (userDetailsServices.size() > 1) {
|
||||
List<String> beanNames = userDetailsServices.stream().map(BeanWithName::getName).toList();
|
||||
else if (beanNames.length > 1) {
|
||||
this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. "
|
||||
+ "Global Authentication Manager will not use a UserDetailsService for username/password login. "
|
||||
+ "Consider publishing a single UserDetailsService bean.", userDetailsServices.size(),
|
||||
beanNames));
|
||||
+ "Consider publishing a single UserDetailsService bean.", beanNames.length,
|
||||
Arrays.toString(beanNames)));
|
||||
return;
|
||||
}
|
||||
UserDetailsService userDetailsService = userDetailsServices.get(0).getBean();
|
||||
String userDetailsServiceBeanName = userDetailsServices.get(0).getName();
|
||||
UserDetailsService userDetailsService = InitializeUserDetailsBeanManagerConfigurer.this.context
|
||||
.getBean(beanNames[0], UserDetailsService.class);
|
||||
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
|
||||
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
|
||||
CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class);
|
||||
@@ -111,8 +110,7 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
provider.afterPropertiesSet();
|
||||
auth.authenticationProvider(provider);
|
||||
this.logger.info(LogMessage.format(
|
||||
"Global AuthenticationManager configured with UserDetailsService bean with name %s",
|
||||
userDetailsServiceBeanName));
|
||||
"Global AuthenticationManager configured with UserDetailsService bean with name %s", beanNames[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,41 +125,6 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of beans of the requested class, along with their names. If
|
||||
* there are no registered beans of that type, the list is empty.
|
||||
*/
|
||||
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
|
||||
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
|
||||
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type);
|
||||
for (String beanName : beanNames) {
|
||||
T bean = InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanName, type);
|
||||
beanWithNames.add(new BeanWithName<T>(bean, beanName));
|
||||
}
|
||||
return beanWithNames;
|
||||
}
|
||||
|
||||
static class BeanWithName<T> {
|
||||
|
||||
private final T bean;
|
||||
|
||||
private final String name;
|
||||
|
||||
BeanWithName(T bean, String name) {
|
||||
this.bean = bean;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
T getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -98,6 +98,10 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
private AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer = new AuthorizationCodeGrantConfigurer();
|
||||
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
/**
|
||||
* Sets the repository of client registrations.
|
||||
* @param clientRegistrationRepository the repository of client registrations
|
||||
@@ -107,6 +111,7 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -119,6 +124,7 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository);
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -283,8 +289,7 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (this.authorizationRequestResolver != null) {
|
||||
return this.authorizationRequestResolver;
|
||||
}
|
||||
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
|
||||
.getClientRegistrationRepository(getBuilder());
|
||||
ClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(getBuilder());
|
||||
return new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
|
||||
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
|
||||
}
|
||||
@@ -292,8 +297,8 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
private OAuth2AuthorizationCodeGrantFilter createAuthorizationCodeGrantFilter(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter(
|
||||
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder),
|
||||
OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), authenticationManager);
|
||||
getClientRegistrationRepository(builder), getAuthorizedClientRepository(builder),
|
||||
authenticationManager);
|
||||
if (this.authorizationRequestRepository != null) {
|
||||
authorizationCodeGrantFilter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
|
||||
}
|
||||
@@ -315,6 +320,18 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
|
||||
private ClientRegistrationRepository getClientRegistrationRepository(B builder) {
|
||||
return (OAuth2ClientConfigurer.this.clientRegistrationRepository != null)
|
||||
? OAuth2ClientConfigurer.this.clientRegistrationRepository
|
||||
: OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder);
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientRepository getAuthorizedClientRepository(B builder) {
|
||||
return (OAuth2ClientConfigurer.this.authorizedClientRepository != null)
|
||||
? OAuth2ClientConfigurer.this.authorizedClientRepository
|
||||
: OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getBeanOrNull(ResolvableType type) {
|
||||
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
|
||||
|
||||
+20
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -172,6 +172,10 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
private String loginProcessingUrl = OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
|
||||
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
/**
|
||||
* Sets the repository of client registrations.
|
||||
* @param clientRegistrationRepository the repository of client registrations
|
||||
@@ -181,6 +185,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -194,6 +199,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository);
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -339,8 +345,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
@Override
|
||||
public void init(B http) throws Exception {
|
||||
OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter(
|
||||
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
|
||||
OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()), this.loginProcessingUrl);
|
||||
this.getClientRegistrationRepository(), this.getAuthorizedClientRepository(), this.loginProcessingUrl);
|
||||
authenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
this.setAuthenticationFilter(authenticationFilter);
|
||||
super.loginProcessingUrl(this.loginProcessingUrl);
|
||||
@@ -406,8 +411,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
|
||||
}
|
||||
authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
|
||||
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
|
||||
authorizationRequestBaseUri);
|
||||
this.getClientRegistrationRepository(), authorizationRequestBaseUri);
|
||||
}
|
||||
if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {
|
||||
authorizationRequestFilter
|
||||
@@ -439,6 +443,16 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return new AntPathRequestMatcher(loginProcessingUrl);
|
||||
}
|
||||
|
||||
private ClientRegistrationRepository getClientRegistrationRepository() {
|
||||
return (this.clientRegistrationRepository != null) ? this.clientRegistrationRepository
|
||||
: OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder());
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
|
||||
return (this.authorizedClientRepository != null) ? this.authorizedClientRepository
|
||||
: OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private JwtDecoderFactory<ClientRegistration> getJwtDecoderFactoryBean() {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(JwtDecoderFactory.class, ClientRegistration.class);
|
||||
@@ -529,8 +543,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, String> getLoginLinks() {
|
||||
Iterable<ClientRegistration> clientRegistrations = null;
|
||||
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
|
||||
.getClientRegistrationRepository(this.getBuilder());
|
||||
ClientRegistrationRepository clientRegistrationRepository = this.getClientRegistrationRepository();
|
||||
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
|
||||
if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
|
||||
clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
|
||||
|
||||
+3
@@ -183,6 +183,9 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
|
||||
BeanDefinitionBuilder csrfAuthenticationStrategy = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(CsrfAuthenticationStrategy.class);
|
||||
csrfAuthenticationStrategy.addConstructorArgReference(this.csrfRepositoryRef);
|
||||
if (StringUtils.hasText(this.requestHandlerRef)) {
|
||||
csrfAuthenticationStrategy.addPropertyReference("requestHandler", this.requestHandlerRef);
|
||||
}
|
||||
return csrfAuthenticationStrategy.getBeanDefinition();
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -147,6 +147,7 @@ final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
|
||||
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
|
||||
.addConstructorArgValue(logoutRequestResolver)
|
||||
.addPropertyValue("logoutRequestRepository", logoutRequestRepository)
|
||||
.getBeanDefinition();
|
||||
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
|
||||
.addConstructorArgValue(saml2LogoutRequestSuccessHandler)
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.GsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for {@link HttpMessageConverter}'s.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author luamas
|
||||
* @since 5.1
|
||||
*/
|
||||
final class HttpMessageConverters {
|
||||
|
||||
private static final boolean jackson2Present;
|
||||
|
||||
private static final boolean gsonPresent;
|
||||
|
||||
private static final boolean jsonbPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|
||||
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
||||
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
|
||||
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
|
||||
}
|
||||
|
||||
private HttpMessageConverters() {
|
||||
}
|
||||
|
||||
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
|
||||
if (jackson2Present) {
|
||||
return new MappingJackson2HttpMessageConverter();
|
||||
}
|
||||
if (gsonPresent) {
|
||||
return new GsonHttpMessageConverter();
|
||||
}
|
||||
if (jsonbPresent) {
|
||||
return new JsonbHttpMessageConverter();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.HttpMessageEncoder;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
class OAuth2ErrorEncoder implements HttpMessageEncoder<OAuth2Error> {
|
||||
|
||||
private final HttpMessageConverter<Object> messageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<MediaType> getStreamingMediaTypes() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
|
||||
return getEncodableMimeTypes().contains(mimeType);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Flux<DataBuffer> encode(Publisher<? extends OAuth2Error> error, DataBufferFactory bufferFactory,
|
||||
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
|
||||
return Mono.from(error).flatMap((data) -> {
|
||||
ByteArrayHttpOutputMessage bytes = new ByteArrayHttpOutputMessage();
|
||||
try {
|
||||
this.messageConverter.write(data, MediaType.APPLICATION_JSON, bytes);
|
||||
return Mono.just(bytes.getBody().toByteArray());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
}).map(bufferFactory::wrap).flux();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<MimeType> getEncodableMimeTypes() {
|
||||
return List.of(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
private static class ByteArrayHttpOutputMessage implements HttpOutputMessage {
|
||||
|
||||
private final ByteArrayOutputStream body = new ByteArrayOutputStream();
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ByteArrayOutputStream getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return new HttpHeaders();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+18
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,16 +16,17 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
@@ -47,6 +48,7 @@ import org.springframework.web.server.WebFilterChain;
|
||||
* A filter for the Client-side OIDC Back-Channel Logout endpoint
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @author Andrey Litvitski
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
@@ -62,6 +64,9 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
|
||||
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
|
||||
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
|
||||
new OAuth2ErrorEncoder());
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
|
||||
@@ -84,7 +89,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
|
||||
})
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap(this.authenticationManager::authenticate)
|
||||
@@ -93,7 +98,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
|
||||
})
|
||||
.flatMap((authentication) -> {
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
@@ -101,19 +106,12 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
|
||||
private Mono<Void> handleAuthenticationFailure(ServerWebExchange exchange, Exception ex) {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
OAuth2Error error = oauth2Error(ex);
|
||||
byte[] bytes = String.format("""
|
||||
{
|
||||
"error_code": "%s",
|
||||
"error_description": "%s",
|
||||
"error_uri: "%s"
|
||||
}
|
||||
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Flux.just(buffer));
|
||||
exchange.getResponse().setRawStatusCode(HttpStatus.BAD_REQUEST.value());
|
||||
return this.errorHttpMessageConverter.write(Mono.just(oauth2Error(ex)), ResolvableType.forClass(Object.class),
|
||||
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
|
||||
exchange.getResponse(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Exception ex) {
|
||||
|
||||
+18
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,23 +16,24 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
|
||||
@@ -44,6 +45,7 @@ import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@@ -52,6 +54,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* Back-Channel Logout Token and invalidates each one.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @author Andrey Litvitski
|
||||
* @since 6.2
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
@@ -63,6 +66,9 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
|
||||
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
||||
|
||||
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
|
||||
new OAuth2ErrorEncoder());
|
||||
|
||||
private WebClient web = WebClient.create();
|
||||
|
||||
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
|
||||
@@ -97,7 +103,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
totalCount.intValue()));
|
||||
}
|
||||
if (!list.isEmpty()) {
|
||||
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
|
||||
return handleLogoutFailure(exchange.getExchange(), oauth2Error(list));
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
@@ -148,17 +154,11 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
|
||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
byte[] bytes = String.format("""
|
||||
{
|
||||
"error_code": "%s",
|
||||
"error_description": "%s",
|
||||
"error_uri: "%s"
|
||||
}
|
||||
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Flux.just(buffer));
|
||||
private Mono<Void> handleLogoutFailure(ServerWebExchange exchange, OAuth2Error error) {
|
||||
exchange.getResponse().setRawStatusCode(HttpStatus.BAD_REQUEST.value());
|
||||
return this.errorHttpMessageConverter.write(Mono.just(error), ResolvableType.forClass(Object.class),
|
||||
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
|
||||
exchange.getResponse(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,6 +47,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
@@ -1007,6 +1008,21 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThat(service.getIdPath("uid")).isEqualTo("uid");
|
||||
}
|
||||
|
||||
// gh-16819
|
||||
@Test
|
||||
void autowireWhenDefaultsThenAdvisorAnnotationsAreSorted() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class).autowire();
|
||||
AuthorizationAdvisorProxyFactory proxyFactory = this.spring.getContext()
|
||||
.getBean(AuthorizationAdvisorProxyFactory.class);
|
||||
AnnotationAwareOrderComparator comparator = AnnotationAwareOrderComparator.INSTANCE;
|
||||
AuthorizationAdvisor previous = null;
|
||||
for (AuthorizationAdvisor advisor : proxyFactory) {
|
||||
boolean ordered = previous == null || comparator.compare(previous, advisor) < 0;
|
||||
assertThat(ordered).isTrue();
|
||||
previous = advisor;
|
||||
}
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
+92
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -75,6 +75,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
@@ -285,6 +286,49 @@ public class OAuth2ClientConfigurerTests {
|
||||
verify(authorizationRedirectStrategy).sendRedirect(any(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenOAuth2LoginBeansConfiguredThenNotShared() throws Exception {
|
||||
this.spring.register(OAuth2ClientConfigWithOAuth2Login.class).autowire();
|
||||
// Setup the Authorization Request in the session
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, this.registration1.getRegistrationId());
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri(this.registration1.getProviderDetails().getAuthorizationUri())
|
||||
.clientId(this.registration1.getClientId())
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state("state")
|
||||
.attributes(attributes)
|
||||
.build();
|
||||
// @formatter:on
|
||||
AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
|
||||
MockHttpSession session = (MockHttpSession) request.getSession();
|
||||
String principalName = "user1";
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password");
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder clientRequest = get("/client-1")
|
||||
.param(OAuth2ParameterNames.CODE, "code")
|
||||
.param(OAuth2ParameterNames.STATE, "state")
|
||||
.with(authentication(authentication))
|
||||
.session(session);
|
||||
this.mockMvc.perform(clientRequest)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/client-1"));
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = authorizedClientRepository
|
||||
.loadAuthorizedClient(this.registration1.getRegistrationId(), authentication, request);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
// Ensure shared objects set for OAuth2 Client are not used
|
||||
ClientRegistrationRepository clientRegistrationRepository = this.spring.getContext()
|
||||
.getBean(ClientRegistrationRepository.class);
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext()
|
||||
.getBean(OAuth2AuthorizedClientRepository.class);
|
||||
verifyNoInteractions(clientRegistrationRepository, authorizedClientRepository);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@@ -362,4 +406,51 @@ public class OAuth2ClientConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class OAuth2ClientConfigWithOAuth2Login {
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository = mock(
|
||||
ClientRegistrationRepository.class);
|
||||
|
||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository = mock(
|
||||
OAuth2AuthorizedClientRepository.class);
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2Client((oauth2Client) -> oauth2Client
|
||||
.clientRegistrationRepository(OAuth2ClientConfigurerTests.clientRegistrationRepository)
|
||||
.authorizedClientService(OAuth2ClientConfigurerTests.authorizedClientService)
|
||||
.authorizationCodeGrant((authorizationCode) -> authorizationCode
|
||||
.authorizationRequestResolver(authorizationRequestResolver)
|
||||
.authorizationRedirectStrategy(authorizationRedirectStrategy)
|
||||
.accessTokenResponseClient(accessTokenResponseClient)
|
||||
)
|
||||
)
|
||||
.oauth2Login((oauth2Login) -> oauth2Login
|
||||
.clientRegistrationRepository(this.clientRegistrationRepository)
|
||||
.authorizedClientRepository(this.authorizedClientRepository)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clientRegistrationRepository() {
|
||||
return this.clientRegistrationRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository() {
|
||||
return this.authorizedClientRepository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+67
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -73,7 +73,9 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
@@ -115,6 +117,7 @@ import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
@@ -669,6 +672,30 @@ public class OAuth2LoginConfigurerTests {
|
||||
.collect(Collectors.toList())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oidcLoginWhenOAuth2ClientBeansConfiguredThenNotShared() throws Exception {
|
||||
this.spring.register(OAuth2LoginConfigWithOAuth2Client.class, JwtDecoderFactoryConfig.class).autowire();
|
||||
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, this.request, this.response);
|
||||
this.request.setParameter("code", "code123");
|
||||
this.request.setParameter("state", authorizationRequest.getState());
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain);
|
||||
Authentication authentication = this.securityContextRepository
|
||||
.loadContext(new HttpRequestResponseHolder(this.request, this.response))
|
||||
.getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).hasSize(1);
|
||||
assertThat(authentication.getAuthorities()).first()
|
||||
.isInstanceOf(OidcUserAuthority.class)
|
||||
.hasToString("OIDC_USER");
|
||||
|
||||
// Ensure shared objects set for OAuth2 Client are not used
|
||||
ClientRegistrationRepository clientRegistrationRepository = this.spring.getContext()
|
||||
.getBean(ClientRegistrationRepository.class);
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext()
|
||||
.getBean(OAuth2AuthorizedClientRepository.class);
|
||||
verifyNoInteractions(clientRegistrationRepository, authorizedClientRepository);
|
||||
}
|
||||
|
||||
private void loadConfig(Class<?>... configs) {
|
||||
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
|
||||
applicationContext.register(configs);
|
||||
@@ -1192,6 +1219,45 @@ public class OAuth2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginConfigWithOAuth2Client extends CommonLambdaSecurityFilterChainConfig {
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository = mock(
|
||||
ClientRegistrationRepository.class);
|
||||
|
||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository = mock(
|
||||
OAuth2AuthorizedClientRepository.class);
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.oauth2Login((oauth2Login) -> oauth2Login
|
||||
.clientRegistrationRepository(
|
||||
new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION))
|
||||
.authorizedClientRepository(new HttpSessionOAuth2AuthorizedClientRepository())
|
||||
)
|
||||
.oauth2Client((oauth2Client) -> oauth2Client
|
||||
.clientRegistrationRepository(this.clientRegistrationRepository)
|
||||
.authorizedClientRepository(this.authorizedClientRepository)
|
||||
);
|
||||
// @formatter:on
|
||||
return super.configureFilterChain(http);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clientRegistrationRepository() {
|
||||
return this.clientRegistrationRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository() {
|
||||
return this.authorizedClientRepository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract static class CommonSecurityFilterChainConfig {
|
||||
|
||||
SecurityFilterChain configureFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -1560,12 +1560,15 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
|
||||
defaultBearerTokenResolver.setAllowUriQueryParameter(true);
|
||||
http
|
||||
.authorizeRequests()
|
||||
.requestMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.bearerTokenResolver(defaultBearerTokenResolver)
|
||||
.jwt()
|
||||
.jwkSetUri(this.jwkSetUri);
|
||||
return http.build();
|
||||
|
||||
+1
@@ -484,6 +484,7 @@ public class Saml2LogoutConfigurerTests {
|
||||
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
|
||||
}
|
||||
|
||||
// gh-11363
|
||||
@Test
|
||||
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
|
||||
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
|
||||
|
||||
@@ -336,6 +336,43 @@ public class CsrfConfigTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postWhenUsingCsrfAndXorCsrfTokenRequestAttributeHandlerThenCsrfAuthenticationStrategyUses()
|
||||
throws Exception {
|
||||
this.spring.configLocations(this.xml("WithXorCsrfTokenRequestAttributeHandler"), this.xml("shared-controllers"))
|
||||
.autowire();
|
||||
// @formatter:off
|
||||
MvcResult mvcResult1 = this.mvc.perform(get("/csrf"))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
MockHttpServletRequest request1 = mvcResult1.getRequest();
|
||||
MockHttpSession session = (MockHttpSession) request1.getSession();
|
||||
CsrfTokenRepository repository = WebTestUtils.getCsrfTokenRepository(request1);
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder login = post("/login")
|
||||
.param("username", "user")
|
||||
.param("password", "password")
|
||||
.session(session)
|
||||
.with(csrf());
|
||||
this.mvc.perform(login)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/"));
|
||||
// @formatter:on
|
||||
assertThat(repository.loadToken(request1)).isNull();
|
||||
// @formatter:off
|
||||
MvcResult mvcResult2 = this.mvc.perform(get("/csrf").session(session))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
MockHttpServletRequest request2 = mvcResult2.getRequest();
|
||||
CsrfToken csrfToken = repository.loadToken(request2);
|
||||
CsrfToken csrfTokenAttribute = (CsrfToken) request2.getAttribute(CsrfToken.class.getName());
|
||||
assertThat(csrfTokenAttribute).isNotNull();
|
||||
assertThat(csrfTokenAttribute.getToken()).isNotBlank();
|
||||
assertThat(csrfTokenAttribute.getToken()).isNotEqualTo(csrfToken.getToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postWhenHasCsrfTokenButSessionExpiresThenRequestIsCancelledAfterSuccessfulAuthentication()
|
||||
throws Exception {
|
||||
|
||||
+17
@@ -63,6 +63,7 @@ import org.springframework.web.util.UriUtils;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -380,6 +381,22 @@ public class Saml2LogoutBeanDefinitionParserTests {
|
||||
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
|
||||
}
|
||||
|
||||
// gh-11363
|
||||
@Test
|
||||
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
|
||||
this.spring.configLocations(this.xml("CustomComponents")).autowire();
|
||||
RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest(this.rpLogoutRequest)
|
||||
.id(this.rpLogoutRequestId)
|
||||
.relayState(this.rpLogoutRequestRelayState)
|
||||
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature))
|
||||
.build();
|
||||
given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest);
|
||||
this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf()));
|
||||
verify(getBean(Saml2LogoutRequestRepository.class)).saveLogoutRequest(eq(logoutRequest), any(), any());
|
||||
}
|
||||
|
||||
private <T> T getBean(Class<T> clazz) {
|
||||
return this.spring.getContext().getBean(clazz);
|
||||
}
|
||||
|
||||
+10
-4
@@ -50,6 +50,7 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
@@ -99,6 +100,7 @@ import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
@@ -197,7 +199,10 @@ public class OidcLogoutSpecTests {
|
||||
.body(BodyInserters.fromFormData("logout_token", "invalid"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest();
|
||||
.isBadRequest()
|
||||
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
|
||||
})
|
||||
.value(hasValue("invalid_request"));
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@@ -264,9 +269,10 @@ public class OidcLogoutSpecTests {
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest()
|
||||
.expectBody(String.class)
|
||||
.value(containsString("partial_logout"))
|
||||
.value(containsString("not all sessions were terminated"));
|
||||
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
|
||||
})
|
||||
.value(hasValue("partial_logout"))
|
||||
.value(hasValue(containsString("not all sessions were terminated")));
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -25,10 +25,15 @@
|
||||
|
||||
<c:property-placeholder local-override="true"/>
|
||||
|
||||
<b:bean id="bearerTokenResolver"
|
||||
class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver">
|
||||
<b:property name="allowUriQueryParameter" value="true"/>
|
||||
</b:bean>
|
||||
|
||||
<http>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<intercept-url pattern="/requires-read-scope" access="hasAuthority('SCOPE_message:read')"/>
|
||||
<oauth2-resource-server>
|
||||
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver">
|
||||
<jwt jwk-set-uri="${jwk-set-uri:https://idp.example.org}"/>
|
||||
</oauth2-resource-server>
|
||||
</http>
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
testImplementation 'org.springframework:spring-test'
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
task springVersion(type: org.gradle.api.tasks.WriteProperties) {
|
||||
|
||||
+17
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -42,6 +42,7 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authorization.AuthorizationProxyFactory;
|
||||
@@ -75,7 +76,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class AuthorizationAdvisorProxyFactory
|
||||
implements AuthorizationProxyFactory, Iterable<AuthorizationAdvisor> {
|
||||
implements AuthorizationProxyFactory, Iterable<AuthorizationAdvisor>, SmartInitializingSingleton {
|
||||
|
||||
private static final boolean isReactivePresent = ClassUtils.isPresent("reactor.core.publisher.Mono", null);
|
||||
|
||||
@@ -108,7 +109,9 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize());
|
||||
advisors.add(new PreFilterAuthorizationMethodInterceptor());
|
||||
advisors.add(new PostFilterAuthorizationMethodInterceptor());
|
||||
return new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AnnotationAwareOrderComparator.sort(factory.advisors);
|
||||
return factory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +126,14 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize());
|
||||
advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor());
|
||||
advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor());
|
||||
return new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AnnotationAwareOrderComparator.sort(factory.advisors);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
AnnotationAwareOrderComparator.sort(this.advisors);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +156,6 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
*/
|
||||
@Override
|
||||
public Object proxy(Object target) {
|
||||
AnnotationAwareOrderComparator.sort(this.advisors);
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -155,9 +164,9 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
return proxied;
|
||||
}
|
||||
ProxyFactory factory = new ProxyFactory(target);
|
||||
for (Advisor advisor : this.advisors) {
|
||||
factory.addAdvisors(advisor);
|
||||
}
|
||||
List<Advisor> advisors = new ArrayList<>(this.advisors);
|
||||
AnnotationAwareOrderComparator.sort(advisors);
|
||||
factory.addAdvisors(advisors);
|
||||
factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers()));
|
||||
return factory.getProxy();
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -48,7 +48,7 @@ public final class AuthorizeReturnObjectMethodInterceptor implements Authorizati
|
||||
private int order = AuthorizationInterceptorsOrder.SECURE_RESULT.getOrder();
|
||||
|
||||
public AuthorizeReturnObjectMethodInterceptor(AuthorizationProxyFactory authorizationProxyFactory) {
|
||||
Assert.notNull(authorizationProxyFactory, "authorizationManager cannot be null");
|
||||
Assert.notNull(authorizationProxyFactory, "authorizationProxyFactory cannot be null");
|
||||
this.authorizationProxyFactory = authorizationProxyFactory;
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -19,6 +19,7 @@ package org.springframework.security.authorization.method;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
||||
|
||||
@@ -43,9 +44,24 @@ final class ExpressionUtils {
|
||||
"SpEL expression must return either a Boolean or an AuthorizationDecision");
|
||||
}
|
||||
catch (EvaluationException ex) {
|
||||
AuthorizationDeniedException denied = findAuthorizationException(ex);
|
||||
if (denied != null) {
|
||||
throw denied;
|
||||
}
|
||||
throw new IllegalArgumentException("Failed to evaluate expression '" + expr.getExpressionString() + "'",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
static AuthorizationDeniedException findAuthorizationException(EvaluationException ex) {
|
||||
Throwable cause = ex.getCause();
|
||||
while (cause != null) {
|
||||
if (cause instanceof AuthorizationDeniedException denied) {
|
||||
return denied;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,6 +38,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
@@ -336,6 +337,32 @@ public class AuthorizationAdvisorProxyFactoryTests {
|
||||
assertThat(factory.proxy(35)).isEqualTo(35);
|
||||
}
|
||||
|
||||
// gh-16819
|
||||
@Test
|
||||
void advisorsWhenWithDefaultsThenAreSorted() {
|
||||
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
|
||||
AnnotationAwareOrderComparator comparator = AnnotationAwareOrderComparator.INSTANCE;
|
||||
AuthorizationAdvisor previous = null;
|
||||
for (AuthorizationAdvisor advisor : proxyFactory) {
|
||||
boolean ordered = previous == null || comparator.compare(previous, advisor) < 0;
|
||||
assertThat(ordered).isTrue();
|
||||
previous = advisor;
|
||||
}
|
||||
}
|
||||
|
||||
// gh-16819
|
||||
@Test
|
||||
void advisorsWhenWithReactiveDefaultsThenAreSorted() {
|
||||
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withReactiveDefaults();
|
||||
AnnotationAwareOrderComparator comparator = AnnotationAwareOrderComparator.INSTANCE;
|
||||
AuthorizationAdvisor previous = null;
|
||||
for (AuthorizationAdvisor advisor : proxyFactory) {
|
||||
boolean ordered = previous == null || comparator.compare(previous, advisor) < 0;
|
||||
assertThat(ordered).isTrue();
|
||||
previous = advisor;
|
||||
}
|
||||
}
|
||||
|
||||
private Authentication authenticated(String user, String... authorities) {
|
||||
return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
|
||||
}
|
||||
|
||||
+15
@@ -22,9 +22,11 @@ import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
public class ExpressionUtilsTests {
|
||||
|
||||
@@ -48,10 +50,23 @@ public class ExpressionUtilsTests {
|
||||
assertThat(ExpressionUtils.evaluate(expression, context)).isInstanceOf(ExpressionAuthorizationDecision.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateWhenExpressionThrowsAuthorizationDeniedExceptionThenPropagates() {
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expression = parser.parseExpression("#root.throwException()");
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(this);
|
||||
assertThatExceptionOfType(AuthorizationDeniedException.class)
|
||||
.isThrownBy(() -> ExpressionUtils.evaluate(expression, context));
|
||||
}
|
||||
|
||||
public AuthorizationDecision returnDecision() {
|
||||
return new AuthorizationDecisionDetails(false, this.details);
|
||||
}
|
||||
|
||||
public Object throwException() {
|
||||
throw new AuthorizationDeniedException("denied", new AuthorizationDecision(false));
|
||||
}
|
||||
|
||||
public boolean returnResult() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ final class Argon2EncodingUtils {
|
||||
case "argon2d" -> new Argon2Parameters.Builder(Argon2Parameters.ARGON2_d);
|
||||
case "argon2i" -> new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i);
|
||||
case "argon2id" -> new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id);
|
||||
default -> throw new IllegalArgumentException("Invalid algorithm type: " + parts[0]);
|
||||
default -> throw new IllegalArgumentException("Invalid algorithm type: " + parts[1]);
|
||||
};
|
||||
if (parts[currentPart].startsWith("v=")) {
|
||||
paramsBuilder.withVersion(Integer.parseInt(parts[currentPart].substring(2)));
|
||||
|
||||
@@ -611,6 +611,10 @@ public class BCrypt {
|
||||
int rounds, off;
|
||||
StringBuilder rs = new StringBuilder();
|
||||
|
||||
// Enforce max length for new passwords only
|
||||
if (!for_check && passwordb.length > 72) {
|
||||
throw new IllegalArgumentException("password cannot be more than 72 bytes");
|
||||
}
|
||||
if (salt == null) {
|
||||
throw new IllegalArgumentException("salt cannot be null");
|
||||
}
|
||||
|
||||
+4
-2
@@ -94,8 +94,10 @@ public class Argon2EncodingUtilsTests {
|
||||
|
||||
@Test
|
||||
public void decodeWhenNonexistingAlgorithmThenThrowException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> Argon2EncodingUtils
|
||||
.decode("$argon2x$v=19$m=1024,t=3,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs"));
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> Argon2EncodingUtils.decode(
|
||||
"$argon2x$v=19$m=1024,t=3,p=2$Y1JkRmJDdzIzZ3oyTWx4aw$cGE5Cbd/cx7micVhXVBdH5qTr66JI1iUyuNNVAnErXs"))
|
||||
.withMessageContaining("argon2x");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+31
@@ -222,4 +222,35 @@ public class BCryptPasswordEncoderTests {
|
||||
assertThat(encoder.matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenPasswordOverMaxLengthThenThrowIllegalArgumentException() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
|
||||
String password72chars = "123456789012345678901234567890123456789012345678901234567890123456789012";
|
||||
encoder.encode(password72chars);
|
||||
|
||||
String password73chars = password72chars + "3";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> encoder.encode(password73chars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenPasswordOverMaxLengthThenAllowToMatch() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
|
||||
String password71chars = "12345678901234567890123456789012345678901234567890123456789012345678901";
|
||||
String encodedPassword71chars = "$2a$10$jx3x2FaF.iX5QZ9i3O424Os2Ou5P5JrnedmWYHuDyX8JKA4Unp4xq";
|
||||
assertThat(encoder.matches(password71chars, encodedPassword71chars)).isTrue();
|
||||
|
||||
String password72chars = password71chars + "2";
|
||||
String encodedPassword72chars = "$2a$10$oXYO6/UvbsH5rQEraBkl6uheccBqdB3n.RaWbrimog9hS2GX4lo/O";
|
||||
assertThat(encoder.matches(password72chars, encodedPassword72chars)).isTrue();
|
||||
|
||||
// Max length is 72 bytes, however, we need to ensure backwards compatibility
|
||||
// for previously encoded passwords that are greater than 72 bytes and allow the
|
||||
// match to be performed.
|
||||
String password73chars = password72chars + "3";
|
||||
String encodedPassword73chars = "$2a$10$1l9.kvQTsqNLiCYFqmKtQOHkp.BrgIrwsnTzWo9jdbQRbuBYQ/AVK";
|
||||
assertThat(encoder.matches(password73chars, encodedPassword73chars)).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,4 +15,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ urls:
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
**** xref:servlet/saml2/login/authentication.adoc[SAML2 Authentication Responses]
|
||||
*** xref:servlet/saml2/logout.adoc[SAML2 Logout]
|
||||
*** xref:servlet/saml2/metadata.adoc[SAML2 Metadata]
|
||||
*** xref:servlet/saml2/saml-extension-migration.adoc[Migrating from Spring Security SAML Extension]
|
||||
** xref:servlet/exploits/index.adoc[Protection Against Exploits]
|
||||
*** xref:servlet/exploits/csrf.adoc[]
|
||||
*** xref:servlet/exploits/headers.adoc[]
|
||||
@@ -128,7 +129,7 @@
|
||||
*** xref:servlet/appendix/faq.adoc[FAQ]
|
||||
* xref:reactive/index.adoc[Reactive Applications]
|
||||
** xref:reactive/getting-started.adoc[Getting Started]
|
||||
** Authentication
|
||||
** xref:reactive/authentication/index.adoc[Authentication]
|
||||
*** xref:reactive/authentication/x509.adoc[X.509 Authentication]
|
||||
*** xref:reactive/authentication/logout.adoc[Logout]
|
||||
*** Session Management
|
||||
|
||||
@@ -8,4 +8,4 @@ Once authentication is performed we know the identity and can perform authorizat
|
||||
|
||||
Spring Security provides built-in support for authenticating users.
|
||||
This section is dedicated to generic authentication support that applies in both Servlet and WebFlux environments.
|
||||
Refer to the sections on authentication for xref:servlet/authentication/index.adoc#servlet-authentication[Servlet] and xref:servlet/authentication/index.adoc[WebFlux] for details on what is supported for each stack.
|
||||
Refer to the sections on authentication for xref:servlet/authentication/index.adoc[Servlet] and xref:reactive/authentication/index.adoc[WebFlux] for details on what is supported for each stack.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
= Spring Security
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Spring Security's documentation can be https://docs.spring.io/spring-security/reference/spring-security-docs.zip[downloaded] as a zip file.
|
||||
====
|
||||
|
||||
Spring Security is a framework that provides xref:features/authentication/index.adoc[authentication], xref:features/authorization/index.adoc[authorization], and xref:features/exploits/index.adoc[protection against common attacks].
|
||||
With first class support for securing both xref:servlet/index.adoc[imperative] and xref:reactive/index.adoc[reactive] applications, it is the de-facto standard for securing Spring-based applications.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[[webflux-authentication]]
|
||||
= Authentication
|
||||
:page-section-summary-toc: 1
|
||||
@@ -29,7 +29,7 @@ spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
opaque-token:
|
||||
opaquetoken:
|
||||
introspection-uri: https://idp.example.com/introspect
|
||||
client-id: client
|
||||
client-secret: secret
|
||||
@@ -616,7 +616,7 @@ spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
opaque-token:
|
||||
opaquetoken:
|
||||
introspection-uri: https://idp.example.org/introspection
|
||||
client-id: client
|
||||
client-secret: secret
|
||||
|
||||
@@ -649,7 +649,7 @@ class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
|
||||
|
||||
You would then add a bean of this type to your application context and inject it into the `LdapAuthenticationProvider`. This is covered in the section on configuring LDAP by using explicit Spring beans in the LDAP chapter of the reference manual.
|
||||
Note that you cannot use the namespace for configuration in this case.
|
||||
You should also consult the security-api-url[Javadoc] for the relevant classes and interfaces.
|
||||
You should also consult the {security-api-url}[Javadoc] for the relevant classes and interfaces.
|
||||
|
||||
|
||||
[[appendix-faq-namespace-post-processor]]
|
||||
|
||||
@@ -34,7 +34,7 @@ The attributes on the `<http>` element control some of the properties on the cor
|
||||
Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true)
|
||||
|
||||
[[nsa-http-authorization-manager-ref]]
|
||||
* **access-decision-manager-ref**
|
||||
* **use-authorization-manager**
|
||||
Use this AuthorizationManager instead of deriving one from <intercept-url> elements
|
||||
|
||||
[[nsa-http-access-decision-manager-ref]]
|
||||
|
||||
@@ -14,6 +14,7 @@ The following image shows the typical layering of the handlers for a single HTTP
|
||||
|
||||
.FilterChain
|
||||
[[servlet-filterchain-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/filterchain.png[]
|
||||
|
||||
The client sends a request to the application, and the container creates a `FilterChain`, which contains the `Filter` instances and `Servlet` that should process the `HttpServletRequest`, based on the path of the request URI.
|
||||
@@ -66,6 +67,7 @@ Here is a picture of how `DelegatingFilterProxy` fits into the <<servlet-filters
|
||||
|
||||
.DelegatingFilterProxy
|
||||
[[servlet-delegatingfilterproxy-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/delegatingfilterproxy.png[]
|
||||
|
||||
`DelegatingFilterProxy` looks up __Bean Filter~0~__ from the `ApplicationContext` and then invokes __Bean Filter~0~__.
|
||||
@@ -113,6 +115,7 @@ The following image shows the role of `FilterChainProxy`.
|
||||
|
||||
.FilterChainProxy
|
||||
[[servlet-filterchainproxy-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/filterchainproxy.png[]
|
||||
|
||||
[[servlet-securityfilterchain]]
|
||||
@@ -124,6 +127,7 @@ The following image shows the role of `SecurityFilterChain`.
|
||||
|
||||
.SecurityFilterChain
|
||||
[[servlet-securityfilterchain-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/securityfilterchain.png[]
|
||||
|
||||
The <<servlet-security-filters,Security Filters>> in `SecurityFilterChain` are typically Beans, but they are registered with `FilterChainProxy` instead of <<servlet-delegatingfilterproxy>>.
|
||||
@@ -145,6 +149,7 @@ The following image shows multiple `SecurityFilterChain` instances:
|
||||
|
||||
.Multiple SecurityFilterChain
|
||||
[[servlet-multi-securityfilterchain-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/multi-securityfilterchain.png[]
|
||||
|
||||
In the <<servlet-multi-securityfilterchain-figure>> figure, `FilterChainProxy` decides which `SecurityFilterChain` should be used.
|
||||
@@ -398,6 +403,7 @@ The {security-api-url}org/springframework/security/web/access/ExceptionTranslati
|
||||
|
||||
The following image shows the relationship of `ExceptionTranslationFilter` to other components:
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/exceptiontranslationfilter.png[]
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ This also gives a good idea of the high level flow of authentication and how pie
|
||||
At the heart of Spring Security's authentication model is the `SecurityContextHolder`.
|
||||
It contains the <<servlet-authentication-securitycontext>>.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/securitycontextholder.png[]
|
||||
|
||||
The `SecurityContextHolder` is where Spring Security stores the details of who is xref:features/authentication/index.adoc#authentication[authenticated].
|
||||
@@ -175,6 +176,7 @@ While the implementation of `AuthenticationManager` could be anything, the most
|
||||
Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide.
|
||||
If none of the configured `AuthenticationProvider` instances can authenticate, authentication fails with a `ProviderNotFoundException`, which is a special `AuthenticationException` that indicates that the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/providermanager.png[]
|
||||
|
||||
In practice each `AuthenticationProvider` knows how to perform a specific type of authentication.
|
||||
@@ -184,11 +186,13 @@ This lets each `AuthenticationProvider` do a very specific type of authenticatio
|
||||
`ProviderManager` also allows configuring an optional parent `AuthenticationManager`, which is consulted in the event that no `AuthenticationProvider` can perform authentication.
|
||||
The parent can be any type of `AuthenticationManager`, but it is often an instance of `ProviderManager`.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/providermanager-parent.png[]
|
||||
|
||||
In fact, multiple `ProviderManager` instances might share the same parent `AuthenticationManager`.
|
||||
This is somewhat common in scenarios where there are multiple xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] instances that have some authentication in common (the shared parent `AuthenticationManager`), but also different authentication mechanisms (the different `ProviderManager` instances).
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/providermanagers-parent.png[]
|
||||
|
||||
[[servlet-authentication-providermanager-erasing-credentials]]
|
||||
@@ -234,6 +238,7 @@ Before the credentials can be authenticated, Spring Security typically requests
|
||||
|
||||
Next, the `AbstractAuthenticationProcessingFilter` can authenticate any authentication requests that are submitted to it.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/abstractauthenticationprocessingfilter.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] When the user submits their credentials, the `AbstractAuthenticationProcessingFilter` creates an <<servlet-authentication-authentication,`Authentication`>> from the `HttpServletRequest` to be authenticated.
|
||||
|
||||
@@ -259,7 +259,7 @@ Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter());
|
||||
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directives.ALL));
|
||||
http
|
||||
.logout((logout) -> logout.addLogoutHandler(clearSiteData))
|
||||
----
|
||||
@@ -268,7 +268,7 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter())
|
||||
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directives.ALL))
|
||||
http {
|
||||
logout {
|
||||
addLogoutHandler(clearSiteData)
|
||||
@@ -402,7 +402,7 @@ SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
|
||||
@PostMapping("/my/logout")
|
||||
public String performLogout(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||
// .. perform logout
|
||||
this.logoutHandler.doLogout(request, response, authentication);
|
||||
this.logoutHandler.logout(request, response, authentication);
|
||||
return "redirect:/home";
|
||||
}
|
||||
----
|
||||
@@ -416,7 +416,7 @@ val logoutHandler = SecurityContextLogoutHandler()
|
||||
@PostMapping("/my/logout")
|
||||
fun performLogout(val authentication: Authentication, val request: HttpServletRequest, val response: HttpServletResponse): String {
|
||||
// .. perform logout
|
||||
this.logoutHandler.doLogout(request, response, authentication)
|
||||
this.logoutHandler.logout(request, response, authentication)
|
||||
return "redirect:/home"
|
||||
}
|
||||
----
|
||||
|
||||
@@ -9,6 +9,7 @@ This section describes how HTTP Basic Authentication works within Spring Securit
|
||||
First, we see the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
|
||||
|
||||
.Sending WWW-Authenticate Header
|
||||
[.invert-dark]
|
||||
image::{figures}/basicauthenticationentrypoint.png[]
|
||||
|
||||
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
@@ -26,6 +27,7 @@ The following image shows the flow for the username and password being processed
|
||||
|
||||
[[servlet-authentication-basicauthenticationfilter]]
|
||||
.Authenticating Username and Password
|
||||
[.invert-dark]
|
||||
image::{figures}/basicauthenticationfilter.png[]
|
||||
|
||||
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ This section examines how `DaoAuthenticationProvider` works within Spring Securi
|
||||
The following figure explains the workings of the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section.
|
||||
|
||||
.`DaoAuthenticationProvider` Usage
|
||||
[.invert-dark]
|
||||
image::{figures}/daoauthenticationprovider.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] The authentication `Filter` from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section passes a `UsernamePasswordAuthenticationToken` to the `AuthenticationManager`, which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
|
||||
|
||||
@@ -10,6 +10,7 @@ This section examines how form-based login works within Spring Security.
|
||||
First, we see how the user is redirected to the login form:
|
||||
|
||||
.Redirecting to the Login Page
|
||||
[.invert-dark]
|
||||
image::{figures}/loginurlauthenticationentrypoint.png[]
|
||||
|
||||
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
@@ -30,6 +31,7 @@ When the username and password are submitted, the `UsernamePasswordAuthenticatio
|
||||
The `UsernamePasswordAuthenticationFilter` extends xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[AbstractAuthenticationProcessingFilter], so the following diagram should look pretty similar:
|
||||
|
||||
.Authenticating Username and Password
|
||||
[.invert-dark]
|
||||
image::{figures}/usernamepasswordauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
@@ -191,6 +191,7 @@ In Spring Security 6, the example shown above is the default configuration.
|
||||
|
||||
The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/securitycontextpersistencefilter.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
@@ -212,6 +213,7 @@ To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `
|
||||
|
||||
The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/securitycontextholderfilter.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
|
||||
@@ -534,6 +534,13 @@ public class MaximumSessionsPreventLoginTests {
|
||||
If you are using a customized authentication filter for form-based login, then you have to configure concurrent session control support explicitly.
|
||||
You can try it using the {gh-samples-url}/servlet/spring-boot/java/session-management/maximum-sessions-prevent-login[Maximum Sessions Prevent Login sample].
|
||||
|
||||
[NOTE]
|
||||
=====
|
||||
If you are using a custom implementation of `UserDetails`, ensure you override the **equals()** and **hashCode()** methods.
|
||||
The default `SessionRegistry` implementation in Spring Security relies on an in-memory Map that uses these methods to correctly identify and manage user sessions.
|
||||
Failing to override them may lead to issues where session tracking and user comparison behave unexpectedly.
|
||||
=====
|
||||
|
||||
== Detecting Timeouts
|
||||
|
||||
Sessions expire on their own, and there is nothing that needs to be done to ensure that a security context gets removed.
|
||||
|
||||
@@ -8,14 +8,12 @@ The browser automatically checks that the certificate presented by a server has
|
||||
You can also use SSL with "`mutual authentication`". The server then requests a valid certificate from the client as part of the SSL handshake.
|
||||
The server authenticates the client by checking that its certificate is signed by an acceptable authority.
|
||||
If a valid certificate has been provided, it can be obtained through the servlet API in an application.
|
||||
The Spring Security X.509 module extracts the certificate by using a filter.
|
||||
It maps the certificate to an application user and loads that user's set of granted authorities for use with the standard Spring Security infrastructure.
|
||||
|
||||
You can also use SSL with "`mutual authentication`". The server then requests a valid certificate from the client as part of the SSL handshake.
|
||||
The server authenticates the client by checking that its certificate is signed by an acceptable authority.
|
||||
For example, if you use Tomcat, you should read the https://tomcat.apache.org/tomcat-10.1-doc/ssl-howto.html[Tomcat SSL instructions].
|
||||
You should get this working before trying it out with Spring Security.
|
||||
|
||||
The Spring Security X.509 module extracts the certificate by using a filter.
|
||||
It maps the certificate to an application user and loads that user's set of granted authorities for use with the standard Spring Security infrastructure.
|
||||
|
||||
|
||||
== Adding X.509 Authentication to Your Web Application
|
||||
Enabling X.509 client authentication is very straightforward.
|
||||
|
||||
@@ -126,6 +126,7 @@ For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` a
|
||||
|
||||
[[authz-authorization-manager-implementations]]
|
||||
.Authorization Manager Implementations
|
||||
[.invert-dark]
|
||||
image::{figures}/authorizationhierarchy.png[]
|
||||
|
||||
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
|
||||
@@ -342,6 +343,7 @@ The following image shows the `AccessDecisionManager` interface:
|
||||
|
||||
[[authz-access-voting]]
|
||||
.Voting Decision Manager
|
||||
[.invert-dark]
|
||||
image::{figures}/access-decision-voting.png[]
|
||||
|
||||
By using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
|
||||
@@ -402,6 +404,7 @@ For example, on the Spring web site, you can find a https://spring.io/blog/2009/
|
||||
|
||||
[[authz-after-invocation]]
|
||||
.After Invocation Implementation
|
||||
[.invert-dark]
|
||||
image::{figures}/after-invocation.png[]
|
||||
|
||||
Like many other parts of Spring Security, `AfterInvocationManager` has a single concrete implementation, `AfterInvocationProviderManager`, which polls a list of ``AfterInvocationProvider``s.
|
||||
|
||||
@@ -65,6 +65,7 @@ In many cases, your authorization rules will be more sophisticated than that, so
|
||||
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works at the request level in Servlet-based applications.
|
||||
|
||||
.Authorize HttpServletRequest
|
||||
[.invert-dark]
|
||||
image::{figures}/authorizationfilter.png[]
|
||||
|
||||
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` constructs a `Supplier` that retrieves an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
|
||||
@@ -1034,8 +1035,8 @@ public class SecurityConfig {
|
||||
http
|
||||
.securityMatcher("/api/**") <1>
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/user/**").hasRole("USER") <2>
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") <3>
|
||||
.requestMatchers("/api/user/**").hasRole("USER") <2>
|
||||
.requestMatchers("/api/admin/**").hasRole("ADMIN") <3>
|
||||
.anyRequest().authenticated() <4>
|
||||
)
|
||||
.formLogin(withDefaults());
|
||||
@@ -1057,8 +1058,8 @@ open class SecurityConfig {
|
||||
http {
|
||||
securityMatcher("/api/**") <1>
|
||||
authorizeHttpRequests {
|
||||
authorize("/user/**", hasRole("USER")) <2>
|
||||
authorize("/admin/**", hasRole("ADMIN")) <3>
|
||||
authorize("/api/user/**", hasRole("USER")) <2>
|
||||
authorize("/api/admin/**", hasRole("ADMIN")) <3>
|
||||
authorize(anyRequest, authenticated) <4>
|
||||
}
|
||||
}
|
||||
@@ -1070,8 +1071,8 @@ open class SecurityConfig {
|
||||
======
|
||||
|
||||
<1> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`
|
||||
<2> Allow access to URLs that start with `/user/` to users with the `USER` role
|
||||
<3> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role
|
||||
<2> Allow access to URLs that start with `/api/user/` to users with the `USER` role
|
||||
<3> Allow access to URLs that start with `/api/admin/` to users with the `ADMIN` role
|
||||
<4> Any other request that doesn't match the rules above, will require authentication
|
||||
|
||||
The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If {spring-framework-reference-url}web.html#spring-web[Spring MVC] is in the classpath, then {security-api-url}org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.html[`MvcRequestMatcher`] will be used, otherwise, {security-api-url}org/springframework/security/web/servlet/util/matcher/AntPathRequestMatcher.html[`AntPathRequestMatcher`] will be used.
|
||||
@@ -1097,8 +1098,8 @@ public class SecurityConfig {
|
||||
http
|
||||
.securityMatcher(antMatcher("/api/**")) <2>
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers(antMatcher("/user/**")).hasRole("USER") <3>
|
||||
.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") <4>
|
||||
.requestMatchers(antMatcher("/api/user/**")).hasRole("USER") <3>
|
||||
.requestMatchers(regexMatcher("/api/admin/.*")).hasRole("ADMIN") <4>
|
||||
.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") <5>
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
@@ -1132,8 +1133,8 @@ open class SecurityConfig {
|
||||
http {
|
||||
securityMatcher(antMatcher("/api/**")) <2>
|
||||
authorizeHttpRequests {
|
||||
authorize(antMatcher("/user/**"), hasRole("USER")) <3>
|
||||
authorize(regexMatcher("/admin/**"), hasRole("ADMIN")) <4>
|
||||
authorize(antMatcher("/api/user/**"), hasRole("USER")) <3>
|
||||
authorize(regexMatcher("/api/admin/**"), hasRole("ADMIN")) <4>
|
||||
authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR")) <5>
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
@@ -1147,8 +1148,8 @@ open class SecurityConfig {
|
||||
|
||||
<1> Import the static factory methods from `AntPathRequestMatcher` and `RegexRequestMatcher` to create `RequestMatcher` instances.
|
||||
<2> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`, using `AntPathRequestMatcher`
|
||||
<3> Allow access to URLs that start with `/user/` to users with the `USER` role, using `AntPathRequestMatcher`
|
||||
<4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
|
||||
<3> Allow access to URLs that start with `/api/user/` to users with the `USER` role, using `AntPathRequestMatcher`
|
||||
<4> Allow access to URLs that start with `/api/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
|
||||
<5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher`
|
||||
|
||||
== Further Reading
|
||||
|
||||
@@ -115,6 +115,7 @@ open class MyCustomerService {
|
||||
|
||||
A given invocation to `MyCustomerService#readCustomer` may look something like this when Method Security <<activate-method-security,is activated>>:
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/methodsecurity.png[]
|
||||
|
||||
1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PreAuthorize` pointcut>>
|
||||
|
||||
@@ -82,6 +82,7 @@ To learn more about CSRF protection for your application, consider the following
|
||||
CSRF protection is provided by several components that are composed within the {security-api-url}org/springframework/security/web/csrf/CsrfFilter.html[`CsrfFilter`]:
|
||||
|
||||
.`CsrfFilter` Components
|
||||
[.invert-dark]
|
||||
image::{figures}/csrf.png[]
|
||||
|
||||
CSRF protection is divided into two parts:
|
||||
@@ -90,6 +91,7 @@ CSRF protection is divided into two parts:
|
||||
2. Determine if the request requires CSRF protection, load and validate the token, and <<csrf-access-denied-handler,handle `AccessDeniedException`>>.
|
||||
|
||||
.`CsrfFilter` Processing
|
||||
[.invert-dark]
|
||||
image::{figures}/csrf-processing.png[]
|
||||
|
||||
* image:{icondir}/number_1.png[] First, the {security-api-url}org/springframework/security/web/csrf/DeferredCsrfToken.html[`DeferredCsrfToken`] is loaded, which holds a reference to the <<csrf-token-repository,`CsrfTokenRepository`>> so that the persisted `CsrfToken` can be loaded later (in image:{icondir}/number_4.png[]).
|
||||
|
||||
@@ -205,6 +205,78 @@ This will ensure that:
|
||||
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
|
||||
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
|
||||
|
||||
[[migrating-spel-expressions]]
|
||||
=== Migrating SpEL Expressions
|
||||
|
||||
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
|
||||
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
|
||||
|
||||
However, to ease migration, you can also use a class like the following:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
|
||||
|
||||
private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
|
||||
|
||||
private Expression expression;
|
||||
|
||||
public MessageExpressionAuthorizationManager(String expressionString) {
|
||||
Assert.hasText(expressionString, "expressionString cannot be empty");
|
||||
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
|
||||
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
|
||||
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
|
||||
return new ExpressionAuthorizationDecision(granted, this.expression);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
And specify an instance for each matcher that you cannot get migrate:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
|
||||
messages
|
||||
// ...
|
||||
.simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
|
||||
// ...
|
||||
|
||||
return messages.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class WebSocketSecurityConfig {
|
||||
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
|
||||
messages
|
||||
// ..
|
||||
.simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
|
||||
// ...
|
||||
|
||||
return messages.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[websocket-authorization-notes]]
|
||||
=== WebSocket Authorization Notes
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ Now we can consider how Bearer Token Authentication works within Spring Security
|
||||
First, we see that, as with xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
|
||||
|
||||
.Sending WWW-Authenticate Header
|
||||
[.invert-dark]
|
||||
image::{figures}/bearerauthenticationentrypoint.png[]
|
||||
|
||||
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
@@ -38,6 +39,7 @@ The following image shows the flow for the bearer token being processed:
|
||||
|
||||
[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
|
||||
.Authenticating Bearer Token
|
||||
[.invert-dark]
|
||||
image::{figures}/bearertokenauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
@@ -92,6 +92,7 @@ Let's take a look at how `JwtAuthenticationProvider` works within Spring Securit
|
||||
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
|
||||
|
||||
.`JwtAuthenticationProvider` Usage
|
||||
[.invert-dark]
|
||||
image::{figures}/jwtauthenticationprovider.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
|
||||
|
||||
@@ -28,7 +28,7 @@ spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
opaque-token:
|
||||
opaquetoken:
|
||||
introspection-uri: https://idp.example.com/introspect
|
||||
client-id: client
|
||||
client-secret: secret
|
||||
@@ -88,6 +88,7 @@ Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring
|
||||
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
|
||||
|
||||
.`OpaqueTokenAuthenticationProvider` Usage
|
||||
[.invert-dark]
|
||||
image::{figures}/opaquetokenauthenticationprovider.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
|
||||
@@ -781,7 +782,7 @@ spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
opaque-token:
|
||||
opaquetoken:
|
||||
introspection-uri: https://idp.example.org/introspection
|
||||
client-id: client
|
||||
client-secret: secret
|
||||
|
||||
@@ -66,10 +66,11 @@ spring:
|
||||
security:
|
||||
saml2:
|
||||
relyingparty:
|
||||
okta:
|
||||
identityprovider:
|
||||
entity-id: ...
|
||||
singlesignon.sign-request: false
|
||||
registration:
|
||||
okta:
|
||||
assertingparty:
|
||||
entity-id: ...
|
||||
singlesignon.sign-request: false
|
||||
----
|
||||
|
||||
Java::
|
||||
|
||||
@@ -7,6 +7,7 @@ First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes
|
||||
It does this through a series of redirects:
|
||||
|
||||
.Redirecting to Asserting Party Authentication
|
||||
[.invert-dark]
|
||||
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
|
||||
|
||||
[NOTE]
|
||||
@@ -34,6 +35,7 @@ The following image shows how Spring Security authenticates a `<saml2:Response>`
|
||||
|
||||
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
|
||||
.Authenticating a `<saml2:Response>`
|
||||
[.invert-dark]
|
||||
image::{figures}/saml2webssoauthenticationfilter.png[]
|
||||
|
||||
[NOTE]
|
||||
@@ -123,7 +125,7 @@ spring:
|
||||
relyingparty:
|
||||
registration:
|
||||
adfs:
|
||||
identityprovider:
|
||||
assertingparty:
|
||||
entity-id: https://idp.example.com/issuer
|
||||
verification.credentials:
|
||||
- certificate-location: "classpath:idp.crt"
|
||||
@@ -833,16 +835,17 @@ spring:
|
||||
security:
|
||||
saml2:
|
||||
relyingparty:
|
||||
okta:
|
||||
signing.credentials: &relying-party-credentials
|
||||
- private-key-location: classpath:rp.key
|
||||
certificate-location: classpath:rp.crt
|
||||
identityprovider:
|
||||
entity-id: ...
|
||||
azure:
|
||||
signing.credentials: *relying-party-credentials
|
||||
identityprovider:
|
||||
entity-id: ...
|
||||
registration:
|
||||
okta:
|
||||
signing.credentials: &relying-party-credentials
|
||||
- private-key-location: classpath:rp.key
|
||||
certificate-location: classpath:rp.crt
|
||||
assertingparty:
|
||||
entity-id: ...
|
||||
azure:
|
||||
signing.credentials: *relying-party-credentials
|
||||
assertingparty:
|
||||
entity-id: ...
|
||||
----
|
||||
|
||||
Second, in a database, you need not replicate the model of `RelyingPartyRegistration`.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
= SAML 2.0 Extension Migration
|
||||
|
||||
This document contains guidance for moving SAML 2.0 Service Providers from Spring Security SAML Extensions 1.x to Spring Security Since Spring Security doesn’t provide Identity Provider support, migrating a Spring Security SAML Extensions Identity Provider is out of scope for this document.
|
||||
|
||||
Because the two approaches are as different as they are, this document will tend to cover patterns more than precise search-and-replace steps.
|
||||
|
||||
[[saml2-login-logout]]
|
||||
== Login & Logout
|
||||
|
||||
=== Changes In Approach
|
||||
|
||||
https://github.com/spring-projects/spring-security[Spring Security] takes a slightly different approach from https://github.com/spring-projects/spring-security-saml[Spring Security SAML Extensions] in a few notable ways.
|
||||
|
||||
==== Simplified Enablement
|
||||
|
||||
Spring Security SAML Extensions support for Service Providers is provided by a series of filters enabled by adding each filter manually in the correct order to various Spring Security filter chains.
|
||||
|
||||
Spring Security’s SAML 2.0 Service Provider support is enabled via the Spring Security DSL methods:
|
||||
xref:servlet/saml2/login/index.adoc[`saml2Login`],
|
||||
xref:servlet/saml2/logout.adoc[`saml2Logout`], and
|
||||
xref:servlet/saml2/metadata.adoc[`saml2Metadata`]. It selects the correct filters to add and puts them in the appropriate places in the filter chain.
|
||||
|
||||
==== Stronger Encapsulation
|
||||
|
||||
Like Spring Security SAML Extensions, Spring Security bases it’s SAML support on OpenSAML. The Extensions project exposes OpenSAML over public interfaces, blurring the lines between the two projects, effectively requiring OpenSAML, and making upgrades to later versions of OpenSAML more complicated.
|
||||
|
||||
Spring Security provides stronger encapsulation. No public interfaces expose OpenSAML components and any class that exposes OpenSAML in its public API is named with an `OpenSaml` prefix for additional clarity.
|
||||
|
||||
==== Out-of-the-box Multitenancy
|
||||
|
||||
Spring Security SAML Extensions offered some lightweight support for declaring more than one Identity Provider and accessing it at login time using the `idp` request parameter. This was limiting as far as changing things at runtime was concerned and also doesn’t allow for a many-to-many relationship between relying and asserting parties.
|
||||
|
||||
Spring Security builds SAML 2.0 multitenancy into its default URLs and basic components in the form of a `RelyingPartyRegistration`. This component acts as a link between a Relying Party’s metadata and an Asserting Party’s metadata, and all pairs are available for lookup in a `RelyingPartyRegistrationRepository`. Each URL represents a unique registration pair to be retrieved.
|
||||
|
||||
Whether it’s AuthnRequests, Responses, LogoutRequests, LogoutResponses, or EntityDescriptors, each filter is based off of `RelyingPartyRegistrationRepository` and so is fundamentally multi-tenant.
|
||||
|
||||
=== Examples Matrix
|
||||
|
||||
Both Spring Security and Spring Security SAML Extensions have examples for how to configure the Service Provider:
|
||||
|
||||
[options="header"]
|
||||
|===
|
||||
| Use case | Spring Security | Spring Security SAML Extension
|
||||
|
||||
| Login & Logout | https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/saml2/login[Sample] |
|
||||
https://github.com/jzheaux/spring-security-saml-migrate/tree/main/login-logout[Sample]
|
||||
| Login using SAML Extension URLs | https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/saml2/custom-urls[Sample] | -
|
||||
| Metadata support | https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/saml2/refreshable-metadata[Sample] | -
|
||||
|===
|
||||
|
||||
You can also see a showcase example in https://github.com/spring-projects/spring-security-saml/tree/main/sample[Spring Security SAML Extension]'s GitHub project.
|
||||
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Spring Security does not support HTTP-Redirect binding for SAML 2.0 Responses.
|
||||
According to the SAML specification, the HTTP-Redirect binding is not permitted for SAML Responses due to URL length and signature limitations. Attempting to use this binding may result in unexpected errors.
|
||||
Use HTTP-POST binding instead when configuring your identity provider.
|
||||
====
|
||||
|
||||
[[saml2-unported]]
|
||||
== Unported Features
|
||||
|
||||
There are some features that are not yet ported over and there are as yet no plans to do so:
|
||||
|
||||
* HTTP-Redirect binding for SAML 2.0 Responses
|
||||
* Artifact binding support
|
||||
@@ -2,4 +2,4 @@
|
||||
= Spring MVC Test Integration
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
Spring Security provides comprehensive integration with https://docs.spring.io/spring/docs/current/spring-framework-reference/html/testing.html#spring-mvc-test-framework[Spring MVC Test]
|
||||
Spring Security provides comprehensive integration with {spring-framework-reference-url}testing/mockmvc.html[Spring MVC Test]
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.6",
|
||||
"antora": "3.2.0-alpha.8",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-beta.5",
|
||||
"@antora/collector-extension": "1.0.1",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
|
||||
"@springio/antora-extensions": "1.14.4",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=3.1.1
|
||||
version=6.3.5-SNAPSHOT
|
||||
version=6.3.10
|
||||
samplesBranch=6.3.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+17
-17
@@ -1,40 +1,40 @@
|
||||
[versions]
|
||||
com-squareup-okhttp3 = "3.14.9"
|
||||
io-rsocket = "1.1.4"
|
||||
io-spring-javaformat = "0.0.43"
|
||||
io-rsocket = "1.1.5"
|
||||
io-spring-javaformat = "0.0.46"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.1.1"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-apache-maven-resolver = "1.9.22"
|
||||
org-aspectj = "1.9.22.1"
|
||||
org-apache-maven-resolver = "1.9.23"
|
||||
org-aspectj = "1.9.24"
|
||||
org-bouncycastle = "1.78.1"
|
||||
org-eclipse-jetty = "11.0.24"
|
||||
org-eclipse-jetty = "11.0.25"
|
||||
org-jetbrains-kotlin = "1.9.25"
|
||||
org-jetbrains-kotlinx = "1.8.1"
|
||||
org-mockito = "5.11.0"
|
||||
org-opensaml = "4.3.2"
|
||||
org-springframework = "6.1.15"
|
||||
org-springframework = "6.1.20"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.12"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.18"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.3"
|
||||
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.3"
|
||||
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:9.43.4"
|
||||
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:9.43.6"
|
||||
com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "com-squareup-okhttp3" }
|
||||
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
|
||||
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.13"
|
||||
io-mockk = "io.mockk:mockk:1.13.13"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.12"
|
||||
io-mockk = "io.mockk:mockk:1.13.17"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.19"
|
||||
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.3"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.6"
|
||||
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"
|
||||
@@ -53,7 +53,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.9"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.10"
|
||||
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" }
|
||||
@@ -83,9 +83,9 @@ org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:3
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.6"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.8"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.12"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.12"
|
||||
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"
|
||||
|
||||
@@ -95,11 +95,11 @@ net-sourceforge-saxon-saxon = "net.sourceforge.saxon:saxon:9.1.0.8"
|
||||
org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33"
|
||||
org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.3.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.5"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.6"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.14"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.22"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.24"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969"
|
||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
|
||||
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -115,7 +114,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -206,7 +205,7 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+2
-2
@@ -70,11 +70,11 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -22,6 +22,7 @@ dependencies {
|
||||
|
||||
testRuntimeOnly project(':spring-security-config')
|
||||
testRuntimeOnly 'org.aspectj:aspectjweaver'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
System.setProperty('python.cachedir.skip', 'true')
|
||||
|
||||
+2
@@ -25,4 +25,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+2
@@ -25,4 +25,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+2
@@ -18,4 +18,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -17,4 +17,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+2
@@ -18,4 +18,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ dependencies {
|
||||
|
||||
testRuntimeOnly project(':spring-security-config')
|
||||
testRuntimeOnly project(':spring-security-ldap')
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
|
||||
@@ -35,6 +35,8 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
testImplementation 'org.skyscreamer:jsonassert'
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
|
||||
@@ -27,4 +27,5 @@ dependencies {
|
||||
testImplementation "ch.qos.logback:logback-classic"
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
provided 'jakarta.servlet:jakarta.servlet-api'
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -80,7 +80,13 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
|
||||
if (registration == null) {
|
||||
return null;
|
||||
}
|
||||
return (T) this.authorizedClients.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
|
||||
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients
|
||||
.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
|
||||
if (cachedAuthorizedClient == null) {
|
||||
return null;
|
||||
}
|
||||
return (T) new OAuth2AuthorizedClient(registration, cachedAuthorizedClient.getPrincipalName(),
|
||||
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+10
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -62,8 +62,15 @@ public final class InMemoryReactiveOAuth2AuthorizedClientService implements Reac
|
||||
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
|
||||
Assert.hasText(principalName, "principalName cannot be empty");
|
||||
return (Mono<T>) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
||||
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
|
||||
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
|
||||
.mapNotNull((clientRegistration) -> {
|
||||
OAuth2AuthorizedClientId id = new OAuth2AuthorizedClientId(clientRegistrationId, principalName);
|
||||
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients.get(id);
|
||||
if (cachedAuthorizedClient == null) {
|
||||
return null;
|
||||
}
|
||||
return new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(),
|
||||
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.client.registration;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -49,6 +50,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* @author Rob Winch
|
||||
* @author Josh Cummings
|
||||
* @author Rafiullah Hamedy
|
||||
* @author Evgeniy Cheban
|
||||
* @since 5.1
|
||||
*/
|
||||
public final class ClientRegistrations {
|
||||
@@ -211,6 +213,7 @@ public final class ClientRegistrations {
|
||||
private static ClientRegistration.Builder getBuilder(String issuer,
|
||||
Supplier<ClientRegistration.Builder>... suppliers) {
|
||||
String errorMessage = "Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"";
|
||||
List<String> errors = new ArrayList<>();
|
||||
for (Supplier<ClientRegistration.Builder> supplier : suppliers) {
|
||||
try {
|
||||
return supplier.get();
|
||||
@@ -219,6 +222,7 @@ public final class ClientRegistrations {
|
||||
if (!ex.getStatusCode().is4xxClientError()) {
|
||||
throw ex;
|
||||
}
|
||||
errors.add(ex.getMessage());
|
||||
// else try another endpoint
|
||||
}
|
||||
catch (IllegalArgumentException | IllegalStateException ex) {
|
||||
@@ -228,6 +232,9 @@ public final class ClientRegistrations {
|
||||
throw new IllegalArgumentException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalArgumentException(errorMessage + ", errors: " + errors);
|
||||
}
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -245,7 +245,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
* be used to create an Authentication for saving.</li>
|
||||
* </ul>
|
||||
* @param authorizedClient the {@link OAuth2AuthorizedClient} to use.
|
||||
* @return the {@link Consumer} to populate the
|
||||
* @return the {@link Consumer} to populate the attributes
|
||||
*/
|
||||
public static Consumer<Map<String, Object>> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) {
|
||||
return (attributes) -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user