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