Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0115e8aea1 | |||
| 6b69edb9a7 | |||
| 0ba26176db | |||
| 85775fe794 | |||
| 1ca53164ee | |||
| 47a521ca26 | |||
| 751c6a762a | |||
| 7d7b59bea4 | |||
| 7149622efd | |||
| e9e0ec89fb | |||
| 596c5f9b0c | |||
| 5d335ccf8d | |||
| 5cdcdc9bcb | |||
| a5b1dec99a | |||
| ea2ec04633 | |||
| c6bc1ec969 | |||
| 9ac5ee9bdb | |||
| 31babecfbd | |||
| 4c12940304 | |||
| bb3738dbfd | |||
| 5c1a108b8b | |||
| 1c4a63387b | |||
| dab48d25b0 | |||
| caa325f92e | |||
| 174e3ef356 | |||
| 32e2735f45 | |||
| 9cfc9bb8a8 | |||
| 341f58d411 | |||
| c00197bdf5 | |||
| 01e1813020 | |||
| 65c037853e | |||
| 05256da801 | |||
| 2bdb57a8a8 | |||
| a3095527a0 | |||
| 9189916287 | |||
| 97e6ac76af | |||
| 779030b6cd | |||
| 1135ad5a5a | |||
| e3d642ce7c | |||
| 00247350d0 | |||
| 9e2cadb475 | |||
| 20332fedb0 | |||
| 2537a033f5 | |||
| f622d8e2e2 | |||
| 92cab2b678 | |||
| 254709ce0b | |||
| e9c1b7b0ae | |||
| 2b322da9ac | |||
| 6aabd768a8 | |||
| cdd626644e | |||
| 5a798e93f1 | |||
| aa88404d69 | |||
| dcb8c563e8 | |||
| e34621ec2c | |||
| 1695d03b72 | |||
| e7ea4091a0 | |||
| 235dfc4dfd | |||
| 092a63d1e0 | |||
| 896dd93313 | |||
| c7b739eb3f | |||
| 32e860523c | |||
| 58232b1172 | |||
| 0e8fd1cd6c | |||
| 1f001f8a6d | |||
| 3fdfa98172 | |||
| 90f5141880 | |||
| ab6667eb5c | |||
| 4b10c2daac | |||
| 63e24a3026 | |||
| 9027dceb71 | |||
| 1ebd663253 | |||
| 09198447af | |||
| e1ec77a7a1 | |||
| d686037419 | |||
| 5c8311b7d3 | |||
| d86545dd41 | |||
| ce995a0178 | |||
| e5ee45d568 | |||
| 3b9991fc89 | |||
| 0e211382ee | |||
| c35e107744 | |||
| 8271258618 | |||
| 1010d253a4 | |||
| c8342fcfae | |||
| 2287d67106 | |||
| 470e2c5c97 | |||
| 657760af5b | |||
| 71a51db3a1 | |||
| 17873adf1f | |||
| a608352689 | |||
| c3dbbb2d01 | |||
| 9a5319d5a2 | |||
| 0af0751cfd | |||
| 39dbd24dcb | |||
| 33ebd5405a | |||
| 2d2c2d31fa | |||
| 6c9efc2f09 | |||
| 1a163dca37 | |||
| 1b0c4d68da | |||
| b38b495630 | |||
| c8a0601e6a | |||
| 2c9dc08e43 | |||
| 5a7f12f1a9 | |||
| 3b355a0278 | |||
| a6d362fa18 | |||
| 37984c2e11 | |||
| b3a43d6154 | |||
| a87fc4ea8a | |||
| e7bff4240a | |||
| 4e6b8e4d29 | |||
| e550c48180 | |||
| a3c06d98ca | |||
| 8f42c86a57 | |||
| 5dae6da15a | |||
| e1c4177cd8 | |||
| c2e97d7661 | |||
| 94a66f7a26 | |||
| 5f80468de3 | |||
| 2f762fefe1 | |||
| 45c37c4454 | |||
| 0c70f358d5 | |||
| 7d38686e09 | |||
| 8b9beb0e1f | |||
| 6230806d56 | |||
| 2159f3a93d | |||
| dc366cf8da | |||
| d32202bce3 | |||
| 349ac07c10 | |||
| 59be5553a1 | |||
| 2bfdb0c6cd | |||
| 2ed7b857c3 | |||
| a0b1d6317c | |||
| 731cd6131b | |||
| c2a589734c | |||
| e5e67f91e9 | |||
| ccb2f06d0d | |||
| fc658d10d3 | |||
| e0fc8f37b0 | |||
| 43ced5291b | |||
| 56c5fc281e | |||
| 7c3a6a567e | |||
| 07e0b1dc37 | |||
| 05d3c4b695 | |||
| 5f80cfc705 | |||
| 7011930305 | |||
| 8a75382b2d | |||
| ca10187fd1 | |||
| 56f486588f | |||
| 3f5f79d835 | |||
| 9985580534 | |||
| 24d4abe5fd | |||
| 16dc6be3c8 | |||
| c88aaedb48 | |||
| 9203567a20 | |||
| e058b559b8 | |||
| 33800c0124 | |||
| b71962e87d | |||
| b97c310aba | |||
| 59461d94b0 | |||
| fc007aa373 | |||
| eaaa813ede |
@@ -10,9 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
COMMIT_OWNER: ${{ github.event.pusher.name }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
|
||||
@@ -57,14 +55,10 @@ jobs:
|
||||
distribution: 'adopt'
|
||||
cache: 'gradle'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/gradle-build-action@v3
|
||||
- name: Set up gradle user name
|
||||
run: echo 'systemProp.user.name=spring-builds+github' >> gradle.properties
|
||||
- name: Build with Gradle
|
||||
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 }}
|
||||
run: ./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
snapshot_tests:
|
||||
name: Test against snapshots
|
||||
@@ -79,11 +73,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Snapshot Tests
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion='5.+' -PreactorVersion='2020.0.+' -PspringDataVersion='2021.2.+' -PlocksDisabled --stacktrace
|
||||
run: ./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion='5.+' -PreactorVersion='2020.0.+' -PspringDataVersion='2021.2.+' -PlocksDisabled --stacktrace
|
||||
check_samples:
|
||||
name: Check Samples project
|
||||
needs: [prerequisites]
|
||||
@@ -102,9 +92,6 @@ jobs:
|
||||
SAMPLES_DIR: ../spring-security-samples
|
||||
VERSION: ${{ needs.prerequisites.outputs.project_version }}
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew publishMavenJavaPublicationToLocalRepository
|
||||
./gradlew cloneSamples -PcloneOutputDirectory="$SAMPLES_DIR"
|
||||
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$VERSION" :runAllTests
|
||||
@@ -121,11 +108,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Check for package tangles
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
run: ./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
deploy_artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
@@ -138,11 +121,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Deploy artifacts
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
|
||||
run: ./gradlew publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -162,11 +141,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Deploy Docs
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew deployDocs -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
|
||||
run: ./gradlew deployDocs -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
|
||||
env:
|
||||
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
|
||||
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
|
||||
@@ -183,11 +158,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Deploy Schema
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew deploySchema -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace --info
|
||||
run: ./gradlew deploySchema -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace --info
|
||||
env:
|
||||
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
|
||||
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
|
||||
@@ -236,14 +207,11 @@ jobs:
|
||||
echo "Artifacts for $REPO@$VERSION have been released to Maven Central."
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
echo "Tagging and publishing $REPO@$VERSION release on GitHub."
|
||||
./gradlew createGitHubRelease -PnextVersion=$VERSION -Pbranch=$BRANCH -PcreateRelease=true -PgitHubAccessToken=$TOKEN
|
||||
- name: Announce Release on Slack
|
||||
id: spring-security-announcing
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
@@ -267,9 +235,6 @@ jobs:
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Update to next Snapshot Version
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
echo "Updating $REPO@$VERSION to next snapshot version."
|
||||
./gradlew :updateToSnapshotVersion
|
||||
git commit -am "Next development version"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Mark Duplicate Dependabot PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
check_duplicate_prs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Dependency Name from PR Title
|
||||
id: extract
|
||||
run: |
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}')
|
||||
echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find PRs
|
||||
id: find_duplicates
|
||||
env:
|
||||
DEPENDENCY_NAME: ${{ steps.extract.outputs.dependency_name }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "$DEPENDENCY_NAME"' --json number --jq 'map(.number) | join(",")')
|
||||
echo "prs=$PRS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Label Duplicate PRs
|
||||
if: steps.find_duplicates.outputs.prs != ''
|
||||
env:
|
||||
PRS: ${{ steps.find_duplicates.outputs.prs }}
|
||||
CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
for i in ${PRS//,/ }
|
||||
do
|
||||
if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then
|
||||
echo "Marking PR $i as duplicate"
|
||||
gh pr edit "$i" --add-label "status: duplicate"
|
||||
gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER"
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,52 @@
|
||||
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.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set Milestone to Dependabot Pull Request
|
||||
id: set-milestone
|
||||
run: |
|
||||
if test -f pom.xml
|
||||
then
|
||||
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
|
||||
else
|
||||
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
|
||||
fi
|
||||
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
|
||||
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
|
||||
|
||||
if [ -z $MILESTONE ]
|
||||
then
|
||||
gh run cancel ${{ github.run_id }}
|
||||
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
|
||||
else
|
||||
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
|
||||
echo mergeEnabled=true >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Merge Dependabot pull request
|
||||
if: steps.set-milestone.outputs.mergeEnabled
|
||||
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
uses: Gamesight/slack-workflow-status@v1.2.0
|
||||
uses: Gamesight/slack-workflow-status@v1.3.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -3,9 +3,7 @@ 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 }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -23,4 +21,24 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue --scan
|
||||
run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue
|
||||
generate-docs:
|
||||
name: Generate Docs
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Run Antora
|
||||
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
|
||||
- name: Upload Docs
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
overwrite: true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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
|
||||
@@ -5,9 +5,7 @@ on:
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -34,18 +32,12 @@ jobs:
|
||||
- id: check-release-due
|
||||
name: Check Release Due
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew gitHubCheckNextVersionDueToday
|
||||
echo "is_due_today=$(cat build/github/milestones/is-due-today)" >>$GITHUB_OUTPUT
|
||||
- id: check-open-issues
|
||||
name: Check for open issues
|
||||
if: steps.check-release-due.outputs.is_due_today == 'true'
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew gitHubCheckMilestoneHasNoOpenIssues
|
||||
echo "is_open_issues=$(cat build/github/milestones/is-open-issues)" >>$GITHUB_OUTPUT
|
||||
- id: validate-release-state
|
||||
@@ -58,9 +50,6 @@ jobs:
|
||||
name: Update version and push
|
||||
if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'false'
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
./gradlew :updateProjectVersion
|
||||
@@ -72,7 +61,7 @@ jobs:
|
||||
- id: send-slack-notification
|
||||
name: Send Slack message
|
||||
if: failure()
|
||||
uses: Gamesight/slack-workflow-status@v1.2.0
|
||||
uses: Gamesight/slack-workflow-status@v1.3.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -28,3 +28,5 @@ s101plugin.state
|
||||
|
||||
!.idea/checkstyle-idea.xml
|
||||
!.idea/externalDependencies.xml
|
||||
|
||||
node_modules
|
||||
|
||||
+5
-3
@@ -25,6 +25,7 @@ apply plugin: 'org.springframework.github.milestone'
|
||||
apply plugin: 'org.springframework.github.changelog'
|
||||
apply plugin: 'org.springframework.github.release'
|
||||
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
|
||||
apply plugin: 'org.springframework.security.check-expected-branch-version'
|
||||
|
||||
group = 'org.springframework.security'
|
||||
description = 'Spring Security'
|
||||
@@ -129,10 +130,10 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,5 +149,6 @@ tasks.register('cloneSamples', IncludeRepoTask) {
|
||||
}
|
||||
|
||||
s101 {
|
||||
repository = 'https://structure101.com/binaries/latest'
|
||||
configurationDirectory = project.file("etc/s101")
|
||||
}
|
||||
|
||||
@@ -68,6 +68,10 @@ gradlePlugin {
|
||||
id = "org.springframework.security.versions.verify-dependencies-versions"
|
||||
implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin"
|
||||
}
|
||||
checkExpectedBranchVersion {
|
||||
id = "org.springframework.security.check-expected-branch-version"
|
||||
implementationClass = "org.springframework.security.CheckExpectedBranchVersionPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
|
||||
/**
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
TaskProvider<CheckExpectedBranchVersionTask> checkExpectedBranchVersionTask = project.getTasks().register("checkExpectedBranchVersion", CheckExpectedBranchVersionTask.class, (task) -> {
|
||||
task.setGroup("Build");
|
||||
task.setDescription("Check if the project version matches the branch version");
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkExpectedBranchVersionTask));
|
||||
}
|
||||
|
||||
public static class CheckExpectedBranchVersionTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Project project = getProject();
|
||||
if (project.hasProperty("skipCheckExpectedBranchVersion")) {
|
||||
return;
|
||||
}
|
||||
String version = (String) project.getVersion();
|
||||
String branchVersion = getBranchVersion(project);
|
||||
if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) {
|
||||
System.out.println("Branch version does not match *.x, ignoring");
|
||||
return;
|
||||
}
|
||||
if (!versionsMatch(version, branchVersion)) {
|
||||
throw new IllegalStateException(String.format("Project version [%s] does not match branch version [%s]. " +
|
||||
"Please verify that the branch contains the right version.", version, branchVersion));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getBranchVersion(Project project) throws IOException {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
project.exec((exec) -> {
|
||||
exec.commandLine("git", "symbolic-ref", "--short", "HEAD");
|
||||
exec.setErrorOutput(System.err);
|
||||
exec.setStandardOutput(baos);
|
||||
});
|
||||
return baos.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean versionsMatch(String projectVersion, String branchVersion) {
|
||||
String[] projectVersionParts = projectVersion.split("\\.");
|
||||
String[] branchVersionParts = branchVersion.split("\\.");
|
||||
if (projectVersionParts.length < 2 || branchVersionParts.length < 2) {
|
||||
return false;
|
||||
}
|
||||
return projectVersionParts[0].equals(branchVersionParts[0]) && projectVersionParts[1].equals(branchVersionParts[1]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -34,18 +33,11 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheFactory;
|
||||
@@ -71,6 +63,10 @@ public class S101Configurer {
|
||||
|
||||
private final Path licenseDirectory;
|
||||
|
||||
private final String repository;
|
||||
|
||||
private final String version;
|
||||
|
||||
private final Project project;
|
||||
private final Logger logger;
|
||||
|
||||
@@ -90,6 +86,9 @@ public class S101Configurer {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
this.licenseDirectory = new File(System.getProperty("user.home") + "/.Structure101/java").toPath();
|
||||
S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class);
|
||||
this.repository = extension.getRepository().get();
|
||||
this.version = extension.getVersion().get();
|
||||
}
|
||||
|
||||
public void license(String licenseId) {
|
||||
@@ -129,25 +128,7 @@ public class S101Configurer {
|
||||
|
||||
public void configure(File installationDirectory, File configurationDirectory) {
|
||||
deleteDirectory(configurationDirectory);
|
||||
String version = computeVersionFromInstallation(installationDirectory);
|
||||
configureProject(version, configurationDirectory);
|
||||
}
|
||||
|
||||
private String computeVersionFromInstallation(File installationDirectory) {
|
||||
File buildJar = new File(installationDirectory, "structure101-java-build.jar");
|
||||
try (JarInputStream input = new JarInputStream(new FileInputStream(buildJar))) {
|
||||
JarEntry entry;
|
||||
while ((entry = input.getNextJarEntry()) != null) {
|
||||
if (entry.getName().contains("structure101-build.properties")) {
|
||||
Properties properties = new Properties();
|
||||
properties.load(input);
|
||||
return properties.getProperty("s101-build");
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
throw new IllegalStateException("Unable to determine Structure101 version");
|
||||
configureProject(this.version, configurationDirectory);
|
||||
}
|
||||
|
||||
private boolean deleteDirectory(File directoryToBeDeleted) {
|
||||
@@ -161,24 +142,8 @@ public class S101Configurer {
|
||||
}
|
||||
|
||||
private String installBuildTool(File installationDirectory, File configurationDirectory) {
|
||||
String source = "https://structure101.com/binaries/v6";
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(source);
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher == null) {
|
||||
return null;
|
||||
}
|
||||
copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2));
|
||||
return matcher.group(2);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
copyZipToFilesystem(this.repository, installationDirectory, "structure101-build-java-all-" + this.version);
|
||||
return this.version;
|
||||
}
|
||||
|
||||
private void copyZipToFilesystem(String source, File destination, String name) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,12 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
@@ -25,6 +30,11 @@ import org.gradle.api.tasks.InputDirectory;
|
||||
|
||||
public class S101PluginExtension {
|
||||
private final Property<String> licenseId;
|
||||
|
||||
private final Property<String> repository;
|
||||
|
||||
private final Property<String> version;
|
||||
|
||||
private final Property<File> installationDirectory;
|
||||
private final Property<File> configurationDirectory;
|
||||
private final Property<String> label;
|
||||
@@ -65,6 +75,24 @@ public class S101PluginExtension {
|
||||
this.label.set(label);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void setRepository(String repository) {
|
||||
this.repository.set(repository);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version.set(version);
|
||||
}
|
||||
|
||||
public S101PluginExtension(Project project) {
|
||||
this.licenseId = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.licenseId")) {
|
||||
@@ -78,5 +106,31 @@ public class S101PluginExtension {
|
||||
if (project.hasProperty("s101.label")) {
|
||||
setLabel((String) project.findProperty("s101.label"));
|
||||
}
|
||||
this.repository = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.repository")) {
|
||||
setRepository((String) project.findProperty("s101.repository"));
|
||||
} else {
|
||||
setRepository("https://structure101.com/binaries/v6");
|
||||
}
|
||||
this.version = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.version")) {
|
||||
setVersion((String) project.findProperty("s101.version"));
|
||||
} else {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getRepository().get());
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher != null) {
|
||||
setVersion(matcher.group(2));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.
|
||||
@@ -149,7 +149,7 @@ public class AuthenticationManagerBuilder
|
||||
* {@link #getDefaultUserDetailsService()} method. Note that additional
|
||||
* {@link UserDetailsService}'s may override this {@link UserDetailsService} as the
|
||||
* default. See the <a href=
|
||||
* "https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#user-schema"
|
||||
* "https://docs.spring.io/spring-security/reference/servlet/appendix/database-schema.html"
|
||||
* >User Schema</a> section of the reference for the default schema.
|
||||
* </p>
|
||||
* @return a {@link JdbcUserDetailsManagerConfigurer} to allow customization of the
|
||||
|
||||
+10
-7
@@ -43,6 +43,7 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
* @since 5.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
private int advisorOrder;
|
||||
@@ -51,16 +52,17 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMethodSecurityMetadataSource source) {
|
||||
static MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMethodSecurityMetadataSource source,
|
||||
ReactiveMethodSecurityConfiguration configuration) {
|
||||
MethodSecurityMetadataSourceAdvisor advisor = new MethodSecurityMetadataSourceAdvisor(
|
||||
"securityMethodInterceptor", source, "methodMetadataSource");
|
||||
advisor.setOrder(this.advisorOrder);
|
||||
advisor.setOrder(configuration.advisorOrder);
|
||||
return advisor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
DelegatingMethodSecurityMetadataSource methodMetadataSource(
|
||||
static DelegatingMethodSecurityMetadataSource methodMetadataSource(
|
||||
MethodSecurityExpressionHandler methodSecurityExpressionHandler) {
|
||||
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
|
||||
methodSecurityExpressionHandler);
|
||||
@@ -70,7 +72,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(AbstractMethodSecurityMetadataSource source,
|
||||
static PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(AbstractMethodSecurityMetadataSource source,
|
||||
MethodSecurityExpressionHandler handler) {
|
||||
ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(handler);
|
||||
ExpressionBasedPreInvocationAdvice preAdvice = new ExpressionBasedPreInvocationAdvice();
|
||||
@@ -80,10 +82,11 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
|
||||
static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(
|
||||
ReactiveMethodSecurityConfiguration configuration) {
|
||||
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
|
||||
if (this.grantedAuthorityDefaults != null) {
|
||||
handler.setDefaultRolePrefix(this.grantedAuthorityDefaults.getRolePrefix());
|
||||
if (configuration.grantedAuthorityDefaults != null) {
|
||||
handler.setDefaultRolePrefix(configuration.grantedAuthorityDefaults.getRolePrefix());
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
+68
-69
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -43,11 +44,13 @@ import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
|
||||
/**
|
||||
@@ -322,43 +325,22 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
if (servletContext == null) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
boolean isProgrammaticApiAvailable = isProgrammaticApiAvailable(servletContext);
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
for (String pattern : patterns) {
|
||||
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
|
||||
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
|
||||
if (isProgrammaticApiAvailable) {
|
||||
matchers.add(resolve(ant, mvc, servletContext));
|
||||
}
|
||||
else {
|
||||
this.logger
|
||||
.warn("The ServletRegistration API was not available at startup time. This may be due to a misconfiguration; "
|
||||
+ "if you are using AbstractSecurityWebApplicationInitializer, please double-check the recommendations outlined in "
|
||||
+ "https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#abstractsecuritywebapplicationinitializer-with-spring-mvc");
|
||||
matchers.add(new DeferredRequestMatcher((request) -> resolve(ant, mvc, request.getServletContext()),
|
||||
mvc, ant));
|
||||
}
|
||||
matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant));
|
||||
}
|
||||
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
|
||||
}
|
||||
|
||||
private static boolean isProgrammaticApiAvailable(ServletContext servletContext) {
|
||||
try {
|
||||
servletContext.getServletRegistrations();
|
||||
return true;
|
||||
}
|
||||
catch (UnsupportedOperationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc, ServletContext servletContext) {
|
||||
Map<String, ? extends ServletRegistration> registrations = mappableServletRegistrations(servletContext);
|
||||
if (registrations.isEmpty()) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
if (!hasDispatcherServlet(registrations)) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
ServletRegistration dispatcherServlet = requireOneRootDispatcherServlet(registrations);
|
||||
if (dispatcherServlet != null) {
|
||||
@@ -452,7 +434,7 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
}
|
||||
}
|
||||
|
||||
private String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
private static String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
|
||||
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
|
||||
+ "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n"
|
||||
@@ -593,34 +575,29 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
static class DeferredRequestMatcher implements RequestMatcher {
|
||||
|
||||
final Function<HttpServletRequest, RequestMatcher> requestMatcherFactory;
|
||||
final Function<ServletContext, RequestMatcher> requestMatcherFactory;
|
||||
|
||||
final AtomicReference<String> description = new AtomicReference<>();
|
||||
|
||||
volatile RequestMatcher requestMatcher;
|
||||
final Map<ServletContext, RequestMatcher> requestMatchers = new ConcurrentHashMap<>();
|
||||
|
||||
DeferredRequestMatcher(Function<HttpServletRequest, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (request) -> {
|
||||
if (this.requestMatcher == null) {
|
||||
synchronized (this) {
|
||||
if (this.requestMatcher == null) {
|
||||
this.requestMatcher = resolver.apply(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.requestMatcher;
|
||||
};
|
||||
DeferredRequestMatcher(Function<ServletContext, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (sc) -> this.requestMatchers.computeIfAbsent(sc, resolver);
|
||||
this.description.set("Deferred " + Arrays.toString(candidates));
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(ServletContext servletContext) {
|
||||
return this.requestMatcherFactory.apply(servletContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matches(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matcher(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -630,53 +607,75 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
}
|
||||
|
||||
static class MockMvcRequestMatcher implements RequestMatcher {
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
|
||||
DispatcherServletRequestMatcher(ServletContext servletContext) {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, computeErrorMessage(this.servletContext.getServletRegistrations().values()));
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return DispatcherServlet.class.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final AntPathRequestMatcher ant;
|
||||
|
||||
private final MvcRequestMatcher mvc;
|
||||
|
||||
private final ServletContext servletContext;
|
||||
private final RequestMatcher dispatcherServlet;
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
ServletContext servletContext) {
|
||||
this(ant, mvc, new OrRequestMatcher(new MockMvcRequestMatcher(),
|
||||
new DispatcherServletRequestMatcher(servletContext)));
|
||||
}
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
RequestMatcher dispatcherServlet) {
|
||||
this.ant = ant;
|
||||
this.mvc = mvc;
|
||||
this.servletContext = servletContext;
|
||||
this.dispatcherServlet = dispatcherServlet;
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(HttpServletRequest request) {
|
||||
if (this.dispatcherServlet.matches(request)) {
|
||||
return this.mvc;
|
||||
}
|
||||
return this.ant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
return this.mvc.matches(request);
|
||||
}
|
||||
return this.ant.matches(request);
|
||||
return requestMatcher(request).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
return this.mvc.matcher(request);
|
||||
}
|
||||
return this.ant.matcher(request);
|
||||
}
|
||||
|
||||
private boolean isDispatcherServlet(ServletRegistration registration) {
|
||||
Class<?> dispatcherServlet = ClassUtils
|
||||
.resolveClassName("org.springframework.web.servlet.DispatcherServlet", null);
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return dispatcherServlet.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
return requestMatcher(request).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+19
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import org.springframework.security.openid.OpenIDAuthenticationFilter;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
@@ -38,7 +39,11 @@ import org.springframework.security.web.authentication.rememberme.RememberMeAuth
|
||||
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
|
||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
|
||||
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
|
||||
@@ -46,6 +51,7 @@ import org.springframework.security.web.session.ConcurrentSessionFilter;
|
||||
import org.springframework.security.web.session.DisableEncodeUrlFilter;
|
||||
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
|
||||
import org.springframework.security.web.session.SessionManagementFilter;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
/**
|
||||
* @param <H>
|
||||
@@ -127,15 +133,24 @@ public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
|
||||
* The ordering of the Filters is:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link ForceEagerSessionCreationFilter}</li>
|
||||
* <li>{@link DisableEncodeUrlFilter}</li>
|
||||
* <li>{@link ForceEagerSessionCreationFilter}</li>
|
||||
* <li>{@link ChannelProcessingFilter}</li>
|
||||
* <li>{@link WebAsyncManagerIntegrationFilter}</li>
|
||||
* <li>{@link SecurityContextHolderFilter}</li>
|
||||
* <li>{@link SecurityContextPersistenceFilter}</li>
|
||||
* <li>{@link HeaderWriterFilter}</li>
|
||||
* <li>{@link CorsFilter}</li>
|
||||
* <li>{@link CsrfFilter}</li>
|
||||
* <li>{@link LogoutFilter}</li>
|
||||
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter}</li>
|
||||
* <li>{@link org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter}</li>
|
||||
* <li>{@link X509AuthenticationFilter}</li>
|
||||
* <li>{@link AbstractPreAuthenticatedProcessingFilter}</li>
|
||||
* <li><a href="
|
||||
* {@docRoot}/org/springframework/security/cas/web/CasAuthenticationFilter.html">CasAuthenticationFilter</a></li>
|
||||
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter}</li>
|
||||
* <li>{@link org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter}</li>
|
||||
* <li>{@link UsernamePasswordAuthenticationFilter}</li>
|
||||
* <li>{@link OpenIDAuthenticationFilter}</li>
|
||||
* <li>{@link org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter}</li>
|
||||
@@ -149,9 +164,11 @@ public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
|
||||
* <li>{@link JaasApiIntegrationFilter}</li>
|
||||
* <li>{@link RememberMeAuthenticationFilter}</li>
|
||||
* <li>{@link AnonymousAuthenticationFilter}</li>
|
||||
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter}</li>
|
||||
* <li>{@link SessionManagementFilter}</li>
|
||||
* <li>{@link ExceptionTranslationFilter}</li>
|
||||
* <li>{@link FilterSecurityInterceptor}</li>
|
||||
* <li>{@link AuthorizationFilter}</li>
|
||||
* <li>{@link SwitchUserFilter}</li>
|
||||
* </ul>
|
||||
* @param filter the {@link Filter} to add
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
+1
-1
@@ -801,7 +801,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* replaced with "#". For example:
|
||||
*
|
||||
* <pre>
|
||||
* X-XSS-Protection: 1 ; mode=block
|
||||
* X-XSS-Protection: 1; mode=block
|
||||
* </pre>
|
||||
* @param headerValue the new header value
|
||||
* @since 5.8
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -335,7 +335,6 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result = postProcess(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
+12
-2
@@ -268,12 +268,14 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return postProcess(logoutResponseFilter);
|
||||
}
|
||||
|
||||
private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationResolver registrations) {
|
||||
private Saml2RelyingPartyInitiatedLogoutFilter createRelyingPartyLogoutFilter(
|
||||
RelyingPartyRegistrationResolver registrations) {
|
||||
LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
|
||||
Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler(
|
||||
registrations);
|
||||
logoutRequestSuccessHandler.setLogoutRequestRepository(this.logoutRequestConfigurer.logoutRequestRepository);
|
||||
LogoutFilter logoutFilter = new LogoutFilter(logoutRequestSuccessHandler, logoutHandlers);
|
||||
Saml2RelyingPartyInitiatedLogoutFilter logoutFilter = new Saml2RelyingPartyInitiatedLogoutFilter(
|
||||
logoutRequestSuccessHandler, logoutHandlers);
|
||||
logoutFilter.setLogoutRequestMatcher(createLogoutMatcher());
|
||||
return postProcess(logoutFilter);
|
||||
}
|
||||
@@ -568,4 +570,12 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
}
|
||||
|
||||
private static class Saml2RelyingPartyInitiatedLogoutFilter extends LogoutFilter {
|
||||
|
||||
Saml2RelyingPartyInitiatedLogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
|
||||
super(logoutSuccessHandler, handlers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -42,7 +42,7 @@ public class JdbcUserServiceBeanDefinitionParser extends AbstractUserDetailsServ
|
||||
@Override
|
||||
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
|
||||
String dataSource = element.getAttribute(ATT_DATA_SOURCE);
|
||||
if (dataSource != null) {
|
||||
if (StringUtils.hasText(dataSource)) {
|
||||
builder.addPropertyReference("dataSource", dataSource);
|
||||
}
|
||||
else {
|
||||
|
||||
+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();
|
||||
|
||||
+33
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.http;
|
||||
|
||||
import org.opensaml.core.Version;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.BeanMetadataElement;
|
||||
@@ -27,6 +28,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
|
||||
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -35,6 +37,8 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
final class Saml2LoginBeanDefinitionParserUtils {
|
||||
|
||||
private static final String OPEN_SAML_4_VERSION = "4";
|
||||
|
||||
private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
|
||||
|
||||
private static final String ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF = "authentication-request-repository-ref";
|
||||
@@ -78,15 +82,27 @@ final class Saml2LoginBeanDefinitionParserUtils {
|
||||
.rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
|
||||
.addConstructorArgValue(relyingPartyRegistrationRepository)
|
||||
.getBeanDefinition();
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver")
|
||||
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver")
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.OpenSamlAuthenticationRequestResolver")
|
||||
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
static BeanDefinition createAuthenticationProvider() {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider")
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider")
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider")
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
@@ -108,4 +124,17 @@ final class Saml2LoginBeanDefinitionParserUtils {
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
static String version() {
|
||||
String version = Version.getVersion();
|
||||
if (StringUtils.hasText(version)) {
|
||||
return version;
|
||||
}
|
||||
boolean openSaml4ClassPresent = ClassUtils
|
||||
.isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
|
||||
if (openSaml4ClassPresent) {
|
||||
return OPEN_SAML_4_VERSION;
|
||||
}
|
||||
throw new IllegalStateException("cannot determine OpenSAML version");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+32
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.http;
|
||||
|
||||
import org.opensaml.core.Version;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.BeanMetadataElement;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.security.saml2.provider.service.authentication.logout
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -33,6 +35,8 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
final class Saml2LogoutBeanDefinitionParserUtils {
|
||||
|
||||
private static final String OPEN_SAML_4_VERSION = "4";
|
||||
|
||||
private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
|
||||
|
||||
private static final String ATT_LOGOUT_REQUEST_VALIDATOR_REF = "logout-request-validator-ref";
|
||||
@@ -62,8 +66,14 @@ final class Saml2LogoutBeanDefinitionParserUtils {
|
||||
if (StringUtils.hasText(logoutResponseResolver)) {
|
||||
return new RuntimeBeanReference(logoutResponseResolver);
|
||||
}
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
@@ -97,10 +107,29 @@ final class Saml2LogoutBeanDefinitionParserUtils {
|
||||
if (StringUtils.hasText(logoutRequestResolver)) {
|
||||
return new RuntimeBeanReference(logoutRequestResolver);
|
||||
}
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
static String version() {
|
||||
String version = Version.getVersion();
|
||||
if (StringUtils.hasText(version)) {
|
||||
return version;
|
||||
}
|
||||
boolean openSaml4ClassPresent = ClassUtils
|
||||
.isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
|
||||
if (openSaml4ClassPresent) {
|
||||
return OPEN_SAML_4_VERSION;
|
||||
}
|
||||
throw new IllegalStateException("cannot determine OpenSAML version");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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);
|
||||
|
||||
+85
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,27 +30,35 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.TestMockHttpServletMappings;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractRequestMatcherRegistry}.
|
||||
@@ -206,18 +214,65 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
// gh-14418
|
||||
@Test
|
||||
public void requestMatchersWhenNoDispatcherServletMockMvcThenMvcRequestMatcherType() throws Exception {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
try (SpringTestContext spring = new SpringTestContext(this)) {
|
||||
spring.register(MockMvcConfiguration.class)
|
||||
.postProcessor((context) -> context.setServletContext(servletContext))
|
||||
.autowire();
|
||||
this.matcherRegistry.setApplicationContext(spring.getContext());
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(spring.getContext()).build();
|
||||
MockHttpServletRequest request = mvc.perform(get("/")).andReturn().getRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -345,6 +400,19 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
verifyNoMoreInteractions(mvc);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenNoMappingThenException() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
servletContext.addServlet("default", DispatcherServlet.class).addMapping("/");
|
||||
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
|
||||
MvcRequestMatcher mvc = mock(MvcRequestMatcher.class);
|
||||
AntPathRequestMatcher ant = mock(AntPathRequestMatcher.class);
|
||||
DispatcherServletDelegatingRequestMatcher requestMatcher = new DispatcherServletDelegatingRequestMatcher(ant,
|
||||
mvc, servletContext);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> requestMatcher.matcher(request));
|
||||
}
|
||||
|
||||
private void mockMvcIntrospector(boolean isPresent) {
|
||||
ApplicationContext context = this.matcherRegistry.getApplicationContext();
|
||||
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
|
||||
@@ -381,11 +449,13 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
return requestMatchers;
|
||||
}
|
||||
|
||||
private static List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
private List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
for (RequestMatcher requestMatcher : wrappedMatchers) {
|
||||
if (requestMatcher instanceof AbstractRequestMatcherRegistry.DeferredRequestMatcher) {
|
||||
requestMatchers.add(((DeferredRequestMatcher) requestMatcher).requestMatcher);
|
||||
if (requestMatcher instanceof DeferredRequestMatcher) {
|
||||
DeferredRequestMatcher deferred = (DeferredRequestMatcher) requestMatcher;
|
||||
WebApplicationContext web = (WebApplicationContext) getApplicationContext();
|
||||
requestMatchers.add(deferred.requestMatcher(web.getServletContext()));
|
||||
}
|
||||
else {
|
||||
requestMatchers.add(requestMatcher);
|
||||
@@ -396,4 +466,11 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class MockMvcConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -76,7 +76,7 @@ public class AuthorizeRequestsTests {
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.servletContext = spy(MockServletContext.mvc());
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(this.servletContext, "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -75,7 +75,7 @@ public class HttpSecuritySecurityMatchersTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -72,7 +72,7 @@ public class UrlAuthorizationConfigurerTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+37
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,7 +19,10 @@ package org.springframework.security.config.authentication;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Element;
|
||||
import org.xml.sax.SAXParseException;
|
||||
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.CachingUserDetailsService;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
@@ -33,6 +36,7 @@ import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||
import org.springframework.security.util.FieldUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -160,6 +164,38 @@ public class JdbcUserServiceBeanDefinitionParserTests {
|
||||
assertThat(AuthorityUtils.authorityListToSet(rod.getAuthorities())).contains("PREFIX_ROLE_SUPERVISOR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataSourceRef() {
|
||||
// @formatter:off
|
||||
String xml = "<authentication-manager>"
|
||||
+ " <authentication-provider>"
|
||||
+ " <jdbc-user-service data-source-ref=''/>"
|
||||
+ " </authentication-provider>"
|
||||
+ "</authentication-manager>";
|
||||
assertThatExceptionOfType(BeanDefinitionParsingException.class)
|
||||
.isThrownBy(() -> setContext(xml))
|
||||
.withFailMessage("Expected exception due to empty data-source-ref")
|
||||
.withMessageContaining("data-source-ref is required for jdbc-user-service");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingDataSourceRef() {
|
||||
// @formatter:off
|
||||
String xml = "<authentication-manager>"
|
||||
+ " <authentication-provider>"
|
||||
+ " <jdbc-user-service/>"
|
||||
+ " </authentication-provider>"
|
||||
+ "</authentication-manager>";
|
||||
assertThatExceptionOfType(XmlBeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> setContext(xml))
|
||||
.withFailMessage("Expected exception due to missing data-source-ref")
|
||||
.havingRootCause()
|
||||
.isInstanceOf(SAXParseException.class)
|
||||
.withMessageContaining("Attribute 'data-source-ref' must appear on element 'jdbc-user-service'");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private void setContext(String context) {
|
||||
this.appContext = new InMemoryXmlApplicationContext(context);
|
||||
}
|
||||
|
||||
+14
@@ -20,10 +20,13 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -65,4 +68,15 @@ public class PasswordEncoderParserTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatePasswordEncoderBeanDefinition() throws Exception {
|
||||
String hash = "bcrypt";
|
||||
Class<?> expectedBeanClass = BCryptPasswordEncoder.class;
|
||||
|
||||
BeanDefinition beanDefinition = PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash);
|
||||
|
||||
Class<?> actualBeanClass = Class.forName(beanDefinition.getBeanClassName());
|
||||
assertThat(actualBeanClass).isEqualTo(expectedBeanClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -75,7 +75,7 @@ public class HeaderSpecTests {
|
||||
this.expectedHeaders.add(HttpHeaders.EXPIRES, "0");
|
||||
this.expectedHeaders.add(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff");
|
||||
this.expectedHeaders.add(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, "DENY");
|
||||
this.expectedHeaders.add(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block");
|
||||
this.expectedHeaders.add(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -320,7 +320,7 @@ public class HeaderSpecTests {
|
||||
|
||||
@Test
|
||||
public void headersWhenXssProtectionValueEnabledModeBlockThenXssProtectionWritten() {
|
||||
this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block");
|
||||
this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block");
|
||||
// @formatter:off
|
||||
this.http.headers()
|
||||
.xssProtection()
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ class ServerHeadersDslTests {
|
||||
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
|
||||
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
|
||||
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ class ServerHttpSecurityDslTests {
|
||||
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
|
||||
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
|
||||
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ class ServerXssProtectionDslTests {
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
|
||||
@@ -61,7 +61,7 @@ public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
|
||||
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
private boolean isFullyAuthenticated(Authentication authentication) {
|
||||
return (!this.authenticationTrustResolver.isAnonymous(authentication)
|
||||
return authentication != null && (!this.authenticationTrustResolver.isAnonymous(authentication)
|
||||
&& !this.authenticationTrustResolver.isRememberMe(authentication));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ public final class AuthorizationManagerAfterMethodInterceptor
|
||||
PostAuthorizeAuthorizationManager authorizationManager) {
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(
|
||||
AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
|
||||
interceptor.setOrder(500);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -59,6 +59,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,6 +69,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,6 +79,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -129,6 +129,10 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
private static final String DEFAULT_ID_SUFFIX = "}";
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\"";
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER_PREFIX = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
|
||||
|
||||
private final String idPrefix;
|
||||
|
||||
private final String idSuffix;
|
||||
@@ -286,7 +290,10 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
|
||||
String id = extractId(prefixEncodedPassword);
|
||||
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
|
||||
if (id != null && !id.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
|
||||
}
|
||||
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,6 +43,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DelegatingPasswordEncoderTests {
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder bcrypt;
|
||||
|
||||
@@ -201,7 +203,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoStartingPrefixStringThenFalse() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoIdStringThenFalse() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -226,7 +228,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> passwordEncoder.matches(this.rawPassword, this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -289,4 +291,14 @@ public class DelegatingPasswordEncoderTests {
|
||||
verifyNoMoreInteractions(this.bcrypt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForTheId() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
antora:
|
||||
extensions:
|
||||
- require: '@springio/antora-extensions'
|
||||
root_component_name: 'security'
|
||||
site:
|
||||
title: Spring Security
|
||||
url: https://docs.spring.io/spring-security/reference
|
||||
robots: allow
|
||||
git:
|
||||
ensure_git_suffix: false
|
||||
content:
|
||||
sources:
|
||||
- url: https://github.com/spring-projects/spring-security
|
||||
branches: [main, '5.{{6..9},{1..9}+({0..9})}.x', '6.+({0..9}).x']
|
||||
tags: ['5.{{6..9},{1..9}+({0..9})}.{0..99}?(-RC+({0..9}))', '6.+({0..9}).+({0..9})?(-{RC,M}*)','!(5.6.{0..10}*)', '!(5.7.{0..8}*)', '!(5.8.{0..3}?({-RC,-M}+({0..9})))','!(6.0.{0..3}*)','!(6.1.0*)']
|
||||
start_path: docs
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-stackoverflow-url: https://stackoverflow.com/tags/spring-security
|
||||
page-related-doc-categories: security
|
||||
page-related-doc-projects: framework,graphql
|
||||
hide-uri-scheme: '@'
|
||||
tabs-sync-option: '@'
|
||||
extensions:
|
||||
- '@asciidoctor/tabs'
|
||||
- '@springio/asciidoctor-extensions'
|
||||
urls:
|
||||
latest_version_segment_strategy: redirect:to
|
||||
latest_version_segment: ''
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
failure_level: warn
|
||||
@@ -44,6 +44,7 @@
|
||||
***** xref:servlet/authentication/passwords/in-memory.adoc[In Memory]
|
||||
***** xref:servlet/authentication/passwords/jdbc.adoc[JDBC]
|
||||
***** xref:servlet/authentication/passwords/user-details.adoc[UserDetails]
|
||||
***** xref:servlet/authentication/passwords/credentials-container.adoc[CredentialsContainer]
|
||||
***** xref:servlet/authentication/passwords/user-details-service.adoc[UserDetailsService]
|
||||
***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder]
|
||||
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
|
||||
|
||||
@@ -20,7 +20,7 @@ Java::
|
||||
@Bean
|
||||
SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception {
|
||||
DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler(
|
||||
new WebSessionServerLogoutHandler(), new SecurityContextServerLogoutHandler()
|
||||
new SecurityContextServerLogoutHandler(), new WebSessionServerLogoutHandler()
|
||||
);
|
||||
|
||||
http
|
||||
@@ -38,7 +38,7 @@ Kotlin::
|
||||
@Bean
|
||||
fun http(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
val customLogoutHandler = DelegatingServerLogoutHandler(
|
||||
WebSessionServerLogoutHandler(), SecurityContextServerLogoutHandler()
|
||||
SecurityContextServerLogoutHandler(), WebSessionServerLogoutHandler()
|
||||
)
|
||||
|
||||
return http {
|
||||
|
||||
@@ -105,6 +105,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
@@ -113,7 +114,8 @@ static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
|
||||
[TIP]
|
||||
====
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes.
|
||||
Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
|
||||
====
|
||||
|
||||
[[jc-reactive-method-security-custom-authorization-manager]]
|
||||
|
||||
@@ -62,7 +62,10 @@ This configuration enables <<rsocket-authentication-simple,simple authentication
|
||||
|
||||
For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
|
||||
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
|
||||
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
|
||||
|
||||
Spring Boot registers it automatically in `RSocketSecurityAutoConfiguration` when you include {gh-samples-url}/reactive/rsocket/hello-security/build.gradle[the correct dependencies].
|
||||
|
||||
Or, if you are not using Boot's auto-configuration, you can register it manually in the following way:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -91,6 +94,8 @@ fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor
|
||||
----
|
||||
======
|
||||
|
||||
To customize the interceptor itself, use `RSocketSecurity` to add <<rsocket-authentication,authentication>> and <<rsocket-authorization,authorization>>.
|
||||
|
||||
[[rsocket-authentication]]
|
||||
== RSocket Authentication
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ In Spring Security this is done by saving the `HttpServletRequest` using a <<req
|
||||
|
||||
The `HttpServletRequest` is saved in the {security-api-url}org/springframework/security/web/savedrequest/RequestCache.html[`RequestCache`].
|
||||
When the user successfully authenticates, the `RequestCache` is used to replay the original request.
|
||||
The <<requestcacheawarefilter,`RequestCacheAwareFilter`>> is what uses the `RequestCache` to save the `HttpServletRequest`.
|
||||
The <<requestcacheawarefilter,`RequestCacheAwareFilter`>> uses the `RequestCache` to get the saved `HttpServletRequest` after the user authenticates, while the `ExceptionTranslationFilter` uses the `RequestCache` to save the `HttpServletRequest` after it detects `AuthenticationException`, before redirecting the user to the login endpoint.
|
||||
|
||||
By default, an `HttpSessionRequestCache` is used.
|
||||
The code below demonstrates how to customize the `RequestCache` implementation that is used to check the `HttpSession` for a saved request if the parameter named `continue` is present.
|
||||
@@ -510,7 +510,7 @@ XML::
|
||||
[[requestcacheawarefilter]]
|
||||
=== RequestCacheAwareFilter
|
||||
|
||||
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to save the `HttpServletRequest`.
|
||||
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to replay the original request.
|
||||
|
||||
[[servlet-logging]]
|
||||
== Logging
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[[servlet-authentication-credentialscontainer]]
|
||||
= CredentialsContainer
|
||||
|
||||
{security-api-url}org/springframework/security/core/CredentialsContainer.html[The `CredentialsContainer`] interface indicates that the implementing object contains sensitive data, and is used internally by Spring Security to erase the authentication credentials after a successful authentication.
|
||||
This interface is implemented by most of Spring Security internal domain classes, like {security-api-url}org/springframework/security/core/userdetails/User.html[User] and {security-api-url}org/springframework/security/authentication/UsernamePasswordAuthenticationToken.html[UsernamePasswordAuthenticationToken].
|
||||
|
||||
The `ProviderManager` manager checks whether the returned `Authentication` implements this interface.
|
||||
If so, xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager-erasing-credentials[it calls the `eraseCredentials` method] to remove the credentials from the object.
|
||||
|
||||
If you want your custom authentication objects to have their credentials erased after authentication, you should ensure that the classes implement the `CredentialsContainer` interface.
|
||||
|
||||
Users who are writing their own `AuthenticationProvider` implementations should create and return an appropriate `Authentication` object there, minus any sensitive data, rather than using this interface.
|
||||
@@ -10,6 +10,41 @@ However, despite leveraging a username/password for authentication it does not i
|
||||
There are many different scenarios for how an LDAP server may be configured so Spring Security's LDAP provider is fully configurable.
|
||||
It uses separate strategy interfaces for authentication and role retrieval and provides default implementations which can be configured to handle a wide range of situations.
|
||||
|
||||
[[servlet-authentication-ldap-required-dependencies]]
|
||||
== Required Dependencies
|
||||
|
||||
To get started, add the `spring-security-ldap` dependency to your project.
|
||||
When using Spring Boot, add the following dependencies:
|
||||
|
||||
.Spring Security LDAP Dependencies
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-ldap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-ldap</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary"]
|
||||
----
|
||||
depenendencies {
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-ldap"
|
||||
implementation "org.springframework.security:spring-security-ldap"
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[servlet-authentication-ldap-prerequisites]]
|
||||
== Prerequisites
|
||||
|
||||
@@ -81,14 +116,14 @@ dn: cn=user,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: user
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=user,ou=people,dc=springframework,dc=org
|
||||
|
||||
dn: cn=admin,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: admin
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
----
|
||||
|
||||
[[servlet-authentication-ldap-unboundid]]
|
||||
|
||||
@@ -52,9 +52,9 @@ If you have more than one in your application context, you need to specify which
|
||||
|
||||
[[remember-me-persistent-token]]
|
||||
== Persistent Token Approach
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[Improved Persistent Login Cookie Best Practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
There is a discussion on this in the comments section of this article.].
|
||||
To use the this approach with namespace configuration, you would supply a datasource reference:
|
||||
To use this approach with namespace configuration, you would supply a datasource reference:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
|
||||
@@ -234,6 +234,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
@@ -245,6 +246,7 @@ Kotlin::
|
||||
----
|
||||
companion object {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun grantedAuthorityDefaults() : GrantedAuthorityDefaults {
|
||||
return GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
@@ -265,7 +267,8 @@ Xml::
|
||||
|
||||
[TIP]
|
||||
====
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes.
|
||||
Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
|
||||
====
|
||||
|
||||
[[jc-method-security-custom-authorization-manager]]
|
||||
|
||||
@@ -339,7 +339,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
|
||||
JWTProcessor jwtProcessor(JWTClaimsSetAwareJWSKeySelector keySelector) {
|
||||
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
|
||||
new DefaultJWTProcessor();
|
||||
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
|
||||
@@ -374,29 +374,22 @@ Java::
|
||||
----
|
||||
@Component
|
||||
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private final TenantRepository tenants;
|
||||
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
|
||||
private final TenantRepository tenants;
|
||||
|
||||
public TenantJwtIssuerValidator(TenantRepository tenants) {
|
||||
this.tenants = tenants;
|
||||
}
|
||||
private final OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The iss claim is not valid",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt token) {
|
||||
return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
|
||||
.validate(token);
|
||||
}
|
||||
public TenantJwtIssuerValidator(TenantRepository tenants) {
|
||||
this.tenants = tenants;
|
||||
}
|
||||
|
||||
private String toTenant(Jwt jwt) {
|
||||
return jwt.getIssuer();
|
||||
}
|
||||
|
||||
private JwtIssuerValidator fromTenant(String tenant) {
|
||||
return Optional.ofNullable(this.tenants.findById(tenant))
|
||||
.map(t -> t.getAttribute("issuer"))
|
||||
.map(JwtIssuerValidator::new)
|
||||
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
|
||||
}
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt token) {
|
||||
if(this.tenants.findById(token.getIssuer()) != null) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
return OAuth2TokenValidatorResult.failure(this.error);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -405,32 +398,17 @@ Kotlin::
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class TenantJwtIssuerValidator(tenants: TenantRepository) : OAuth2TokenValidator<Jwt> {
|
||||
private val tenants: TenantRepository
|
||||
private val validators: MutableMap<String, JwtIssuerValidator> = ConcurrentHashMap()
|
||||
class TenantJwtIssuerValidator(private val tenants: TenantRepository) : OAuth2TokenValidator<Jwt> {
|
||||
private val error: OAuth2Error = OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The iss claim is not valid",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1")
|
||||
|
||||
override fun validate(token: Jwt): OAuth2TokenValidatorResult {
|
||||
return validators.computeIfAbsent(toTenant(token)) { tenant: String -> fromTenant(tenant) }
|
||||
.validate(token)
|
||||
}
|
||||
|
||||
private fun toTenant(jwt: Jwt): String {
|
||||
return jwt.issuer.toString()
|
||||
}
|
||||
|
||||
private fun fromTenant(tenant: String): JwtIssuerValidator {
|
||||
return Optional.ofNullable(tenants.findById(tenant))
|
||||
.map({ t -> t.getAttribute("issuer") })
|
||||
.map({ JwtIssuerValidator() })
|
||||
.orElseThrow({ IllegalArgumentException("unknown tenant") })
|
||||
}
|
||||
|
||||
init {
|
||||
this.tenants = tenants
|
||||
return if (tenants.findById(token.issuer) != null)
|
||||
OAuth2TokenValidatorResult.success() else OAuth2TokenValidatorResult.failure(error)
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`]:
|
||||
|
||||
[tabs]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.6",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-beta.2",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.12"
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,11 @@ apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
antora {
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-security'
|
||||
branch = 'docs-build'
|
||||
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
|
||||
checkLocalBranch = true
|
||||
}
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
environment = [
|
||||
'BUILD_REFNAME': 'HEAD',
|
||||
'BUILD_VERSION': project.version,
|
||||
]
|
||||
}
|
||||
|
||||
tasks.named("generateAntoraYml") {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.csrf.CsrfTokenAssert.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.AntPathRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.RegexRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.assertj.core.api.InstanceOfAssertFactories.*"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
|
||||
|
||||
@@ -12,4 +12,7 @@
|
||||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
<module name="SuppressWithPlainTextCommentFilter"/>
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value=".*[\\/]node(_modules|js)[\\/].*$"/>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=2.7.12
|
||||
version=5.8.9-SNAPSHOT
|
||||
version=5.8.14
|
||||
samplesBranch=5.8.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
com-squareup-okhttp3 = "3.14.9"
|
||||
io-r2dbc = "0.9.1.RELEASE"
|
||||
io-rsocket = "1.1.4"
|
||||
io-spring-javaformat = "0.0.40"
|
||||
io-spring-javaformat = "0.0.41"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-aspectj = "1.9.20.1"
|
||||
org-bouncycastle = "1.70"
|
||||
org-eclipse-jetty = "9.4.53.v20231009"
|
||||
org-eclipse-jetty = "9.4.55.v20240627"
|
||||
org-jetbrains-kotlin = "1.7.22"
|
||||
org-jetbrains-kotlinx = "1.6.4"
|
||||
org-junit-jupiter = "5.9.3"
|
||||
@@ -15,7 +15,7 @@ org-mockito = "4.8.1"
|
||||
org-opensaml4 = "4.1.0"
|
||||
org-opensaml3 = "3.4.6"
|
||||
org-slf4j = "1.7.36"
|
||||
org-springframework = "5.3.31"
|
||||
org-springframework = "5.3.39"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.13"
|
||||
@@ -30,9 +30,9 @@ com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:6.5.1"
|
||||
io-mockk = "io.mockk:mockk:1.13.3"
|
||||
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.40"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.39"
|
||||
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
|
||||
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.48"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.47"
|
||||
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.9.RELEASE"
|
||||
io-r2dbc-r2dbc-h2 = { module = "io.r2dbc:r2dbc-h2", version.ref = "io-r2dbc" }
|
||||
io-r2dbc-r2dbc-spi-test = { module = "io.r2dbc:r2dbc-spi-test", version.ref = "io-r2dbc" }
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
@@ -69,7 +69,7 @@ org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet",
|
||||
org-eclipse-persistence-javax-persistence = "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-hibernate-entitymanager = "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.2"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.3"
|
||||
org-jasig-cas-client-cas-client-core = "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "org-jetbrains-kotlin" }
|
||||
@@ -87,7 +87,7 @@ org-python-jython = "org.python:jython:2.5.3"
|
||||
org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit-driver:2.65.0"
|
||||
org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:3.141.59"
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = { module = "org.slf4j:log4j-over-slf4j", version.ref = "org-slf4j" }
|
||||
org-slf4j-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "org-slf4j" }
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2021.2.18"
|
||||
@@ -103,7 +103,7 @@ org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:3.0.9"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.38.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.14"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.29.4"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1"
|
||||
|
||||
+5
-3
@@ -136,9 +136,11 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
|
||||
ContextFactory contextFactory = new ContextFactory();
|
||||
|
||||
/**
|
||||
* @param domain the domain name (may be null or empty)
|
||||
* @param url an LDAP url (or multiple URLs)
|
||||
* @param rootDn the root DN (may be null or empty)
|
||||
* @param domain the domain name (can be null or empty)
|
||||
* @param url an LDAP url (or multiple space-delimited URLs).
|
||||
* @param rootDn the root DN (can be null or empty)
|
||||
* @see <a href="https://docs.oracle.com/javase/jndi/tutorial/ldap/misc/url.html">JNDI
|
||||
* URL format documentation</a>
|
||||
*/
|
||||
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) {
|
||||
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
|
||||
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.messaging.web.csrf;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Copied from
|
||||
@@ -43,26 +44,26 @@ final class XorCsrfTokenUtils {
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+69
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.messaging.web.csrf;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -141,6 +142,73 @@ public class XorCsrfChannelInterceptorTests {
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsShorterThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsLongerThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsShorterThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "a");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsLongerThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "abcde");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenActualBytesIsEmptyThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), "");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Message<String> message() {
|
||||
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
|
||||
}
|
||||
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -94,14 +94,10 @@ public final class WebSessionServerOAuth2AuthorizedClientRepository implements S
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, OAuth2AuthorizedClient> getAuthorizedClients(WebSession session) {
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = (session != null)
|
||||
? (Map<String, OAuth2AuthorizedClient>) session.getAttribute(this.sessionAttributeName) : null;
|
||||
if (authorizedClients == null) {
|
||||
authorizedClients = new HashMap<>();
|
||||
}
|
||||
return authorizedClients;
|
||||
Assert.notNull(session, "session cannot be null");
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = session.getAttribute(this.sessionAttributeName);
|
||||
return (authorizedClients != null) ? authorizedClients : new HashMap<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
@@ -24,10 +25,12 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -202,4 +205,28 @@ public class WebSessionServerOAuth2AuthorizedClientRepositoryTests {
|
||||
assertThat(loadedAuthorizedClient2).isSameAs(authorizedClient2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration1, this.principalName1,
|
||||
mock(OAuth2AccessToken.class));
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -145,20 +145,17 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Jwt> decode(String token) throws JwtException {
|
||||
JWT jwt = parse(token);
|
||||
if (jwt instanceof PlainJWT) {
|
||||
throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm());
|
||||
}
|
||||
return this.decode(jwt);
|
||||
}
|
||||
|
||||
private JWT parse(String token) {
|
||||
public Mono<Jwt> decode(String token) {
|
||||
try {
|
||||
return JWTParser.parse(token);
|
||||
JWT jwt = JWTParser.parse(token);
|
||||
if (jwt instanceof PlainJWT) {
|
||||
return Mono.error(new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()));
|
||||
}
|
||||
return this.decode(jwt);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new BadJwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex);
|
||||
return Mono.error(new BadJwtException(
|
||||
"An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-8
@@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -179,16 +180,17 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
// RFC-7662 page 7 directs users to RFC-7519 for defining the values of these
|
||||
// issuer fields.
|
||||
@@ -208,11 +210,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
// may be awkward to debug, we do not want to manipulate this value. Previous
|
||||
// versions of Spring Security
|
||||
// would *only* allow valid URLs, which is not what we wish to achieve here.
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
@@ -222,7 +224,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(converted, authorities);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-8
@@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -136,16 +137,17 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
// RFC-7662 page 7 directs users to RFC-7519 for defining the values of these
|
||||
// issuer fields.
|
||||
@@ -165,11 +167,11 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
// may be awkward to debug, we do not want to manipulate this value. Previous
|
||||
// versions of Spring Security
|
||||
// would *only* allow valid URLs, which is not what we wish to achieve here.
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
@@ -179,7 +181,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(converted, authorities);
|
||||
}
|
||||
|
||||
private OAuth2IntrospectionException onError(Throwable ex) {
|
||||
|
||||
-5
@@ -50,11 +50,6 @@ import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
* To locate the bearer token, this looks in the Reactor {@link Context} for a key of type
|
||||
* {@link Authentication}.
|
||||
*
|
||||
* Registering
|
||||
* {@see org.springframework.security.config.annotation.web.configuration.OAuth2ResourceServerConfiguration.OAuth2ResourceServerWebFluxSecurityConfiguration.BearerRequestContextSubscriberRegistrar},
|
||||
* as a {@code @Bean} will take care of this automatically, but certainly an application
|
||||
* can supply a {@link Context} of its own to override.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.2
|
||||
*/
|
||||
|
||||
@@ -96,6 +96,7 @@ project.tasks.matching { t -> t.name == "sourcesJar"}.configureEach {
|
||||
|
||||
|
||||
javadoc {
|
||||
classpath += sourceSets.opensaml3Main.runtimeClasspath + sourceSets.opensaml4Main.runtimeClasspath
|
||||
source += sourceSets.opensaml3Main.allJava + sourceSets.opensaml4Main.allJava
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -45,8 +45,8 @@ public final class RelyingPartyRegistrations {
|
||||
* Return a {@link RelyingPartyRegistration.Builder} based off of the given SAML 2.0
|
||||
* Asserting Party (IDP) metadata location.
|
||||
*
|
||||
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
|
||||
* valid endpoints might include:
|
||||
* Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
|
||||
* Some valid endpoints might include:
|
||||
*
|
||||
* <pre>
|
||||
* metadataLocation = "classpath:asserting-party-metadata.xml";
|
||||
@@ -69,8 +69,8 @@ public final class RelyingPartyRegistrations {
|
||||
* about the asserting party. Thus, you will need to remember to still populate
|
||||
* anything about the relying party, like any private keys the relying party will use
|
||||
* for signing AuthnRequests.
|
||||
* @param metadataLocation The classpath- or file-based locations or HTTP endpoints of
|
||||
* the asserting party metadata file
|
||||
* @param metadataLocation The classpath- or file-based locations or HTTPS endpoints
|
||||
* of the asserting party metadata file
|
||||
* @return the {@link RelyingPartyRegistration.Builder} for further configuration
|
||||
*/
|
||||
public static RelyingPartyRegistration.Builder fromMetadataLocation(String metadataLocation) {
|
||||
@@ -130,8 +130,8 @@ public final class RelyingPartyRegistrations {
|
||||
* Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off
|
||||
* of the given SAML 2.0 Asserting Party (IDP) metadata location.
|
||||
*
|
||||
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
|
||||
* valid endpoints might include:
|
||||
* Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
|
||||
* Some valid endpoints might include:
|
||||
*
|
||||
* <pre>
|
||||
* metadataLocation = "classpath:asserting-party-metadata.xml";
|
||||
@@ -155,7 +155,7 @@ public final class RelyingPartyRegistrations {
|
||||
* about the asserting party. Thus, you will need to remember to still populate
|
||||
* anything about the relying party, like any private keys the relying party will use
|
||||
* for signing AuthnRequests.
|
||||
* @param location The classpath- or file-based locations or HTTP endpoints of the
|
||||
* @param location The classpath- or file-based locations or HTTPS endpoints of the
|
||||
* asserting party metadata file
|
||||
* @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
|
||||
* further configuration
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -55,6 +55,7 @@ import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.ProxyRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
@@ -837,6 +838,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
});
|
||||
conditions.add(new ProxyRestrictionConditionValidator());
|
||||
subjects.add(new BearerSubjectConfirmationValidator() {
|
||||
@Override
|
||||
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
|
||||
|
||||
+16
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -55,6 +55,7 @@ import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.OneTimeUse;
|
||||
import org.opensaml.saml.saml2.core.ProxyRestriction;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
||||
@@ -63,6 +64,7 @@ import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.ProxyRestrictionBuilder;
|
||||
import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.w3c.dom.Element;
|
||||
@@ -777,6 +779,19 @@ public class OpenSaml4AuthenticationProviderTests {
|
||||
.withMessageContaining("did not match any valid issuers");
|
||||
}
|
||||
|
||||
// gh-14931
|
||||
@Test
|
||||
public void authenticateWhenAssertionHasProxyRestrictionThenParses() {
|
||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
ProxyRestriction condition = new ProxyRestrictionBuilder().buildObject();
|
||||
assertion.getConditions().getConditions().add(condition);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
|
||||
provider.authenticate(token);
|
||||
}
|
||||
|
||||
private <T extends XMLObject> T build(QName qName) {
|
||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ changelog:
|
||||
labels: ["type: bug"]
|
||||
sort: "title"
|
||||
- title: ":hammer: Dependency Upgrades"
|
||||
emoji: ":hammer:"
|
||||
labels: ["type: dependency-upgrade"]
|
||||
sort: "title"
|
||||
- title: ":nut_and_bolt: Build Updates"
|
||||
labels: ["type: task", "in: build"]
|
||||
sort: "title"
|
||||
issues:
|
||||
exclude:
|
||||
labels: ["status: duplicate"]
|
||||
|
||||
+6
-2
@@ -5,8 +5,12 @@ pluginManagement {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.gradle.enterprise" version "3.11.4"
|
||||
id "io.spring.ge.conventions" version "0.0.14"
|
||||
id "com.gradle.develocity" version "3.17.6"
|
||||
id "io.spring.ge.conventions" version "0.0.17" apply false
|
||||
}
|
||||
|
||||
if (System.getenv("DEVELOCITY_ACCESS_KEY")) {
|
||||
apply plugin: "io.spring.ge.conventions"
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
|
||||
+8
-2
@@ -30,6 +30,7 @@ import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
|
||||
@@ -67,7 +68,7 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Authorizing %s", request));
|
||||
this.logger.trace(LogMessage.format("Authorizing %s", requestLine(request)));
|
||||
}
|
||||
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
|
||||
|
||||
@@ -76,7 +77,8 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
if (matchResult.isMatch()) {
|
||||
AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager));
|
||||
this.logger.trace(
|
||||
LogMessage.format("Checking authorization on %s using %s", requestLine(request), manager));
|
||||
}
|
||||
return manager.check(authentication,
|
||||
new RequestAuthorizationContext(request, matchResult.getVariables()));
|
||||
@@ -86,6 +88,10 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String requestLine(HttpServletRequest request) {
|
||||
return request.getMethod() + " " + UrlUtils.buildRequestUrl(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
|
||||
* @return the new {@link Builder} instance
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's
|
||||
* <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's <a href=
|
||||
* "https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* Persistent Login Cookie Best Practice</a>.
|
||||
*
|
||||
* There is a slight modification to the described approach, in that the username is not
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -84,17 +84,16 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
@@ -114,9 +113,10 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -130,9 +130,13 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
|
||||
s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
|
||||
|
||||
private static final Pattern HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[[^\\p{IsControl}]||\\t]]*");
|
||||
|
||||
private static final Predicate<String> HEADER_VALUE_PREDICATE = (s) -> HEADER_VALUE_PATTERN.matcher(s).matches();
|
||||
|
||||
private Predicate<String> allowedHeaderNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedHeaderValues = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
private Predicate<String> allowedHeaderValues = HEADER_VALUE_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedParameterNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
|
||||
+2
-2
@@ -122,7 +122,7 @@ public final class XXssProtectionHeaderWriter implements HeaderWriter {
|
||||
* specify mode as blocked. The content will be replaced with "#". For example:
|
||||
*
|
||||
* <pre>
|
||||
* X-XSS-Protection: 1 ; mode=block
|
||||
* X-XSS-Protection: 1; mode=block
|
||||
* </pre>
|
||||
* @param headerValue the new header value
|
||||
* @throws IllegalArgumentException when headerValue is null
|
||||
@@ -134,7 +134,7 @@ public final class XXssProtectionHeaderWriter implements HeaderWriter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block"
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1; mode=block"
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 5.8
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -77,17 +77,16 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
@@ -107,9 +106,10 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+3
-3
@@ -122,7 +122,7 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW
|
||||
* specify mode as blocked. The content will be replaced with "#". For example:
|
||||
*
|
||||
* <pre>
|
||||
* X-XSS-Protection: 1 ; mode=block
|
||||
* X-XSS-Protection: 1; mode=block
|
||||
* </pre>
|
||||
* @param headerValue the new headerValue
|
||||
* @throws IllegalArgumentException if headerValue is null
|
||||
@@ -135,14 +135,14 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block"
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1; mode=block"
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 5.8
|
||||
*/
|
||||
public enum HeaderValue {
|
||||
|
||||
DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1 ; mode=block");
|
||||
DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1; mode=block");
|
||||
|
||||
private final String value;
|
||||
|
||||
|
||||
+56
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,6 +44,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*/
|
||||
public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -203,6 +206,58 @@ public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
assertThat(tokenValue).isEqualTo(this.token.getToken());
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), "");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
return (invocation) -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -782,6 +782,13 @@ public class StrictHttpFirewallTests {
|
||||
assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Something"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestGetHeaderWhenHorizontalTabInHeaderValueThenNoException() {
|
||||
this.request.addHeader("Something", "tab\tvalue");
|
||||
HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
|
||||
assertThat(request.getHeader("Something")).isEqualTo("tab\tvalue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestGetHeaderWhenUndefinedCharacterInHeaderValueThenException() {
|
||||
this.request.addHeader("Something", "bad\uFFFEvalue");
|
||||
|
||||
+76
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,6 +46,9 @@ import static org.mockito.Mockito.verify;
|
||||
*/
|
||||
public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -188,6 +191,78 @@ public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), ""))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
return (invocation) -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ public class XXssProtectionServerHttpHeadersWriterTests {
|
||||
this.writer.writeHttpHeaders(this.exchange);
|
||||
assertThat(this.headers).hasSize(1);
|
||||
assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION))
|
||||
.containsOnly("1 ; mode=block");
|
||||
.containsOnly("1; mode=block");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,7 +99,7 @@ public class XXssProtectionServerHttpHeadersWriterTests {
|
||||
this.writer.writeHttpHeaders(this.exchange);
|
||||
assertThat(this.headers).hasSize(1);
|
||||
assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION))
|
||||
.containsOnly("1 ; mode=block");
|
||||
.containsOnly("1; mode=block");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user