Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93019f889e | |||
| dfd440ad64 | |||
| 25bb4a1cd4 | |||
| 66a149066f | |||
| ea365cab06 | |||
| ae8ff1af43 | |||
| a298a2b8c9 | |||
| aac1e2e2fc | |||
| 556b139a02 | |||
| fbd2cce3ba | |||
| 40d0d19713 | |||
| 3fb2c68ff7 | |||
| 5900348174 | |||
| 307b460838 | |||
| 3b528e0c4c | |||
| c7f01cb974 | |||
| e4a5e21486 | |||
| b851af3e02 | |||
| 100e0407ee | |||
| 56835a04a1 | |||
| 1cbb3fc10d | |||
| 1f0c0fe204 | |||
| 5b923c78ea | |||
| 65a1099ccb | |||
| 9fdd2dc675 | |||
| 2aa3c1ab2e | |||
| 17c58ef088 | |||
| 63306ad8ee | |||
| 24aec1d002 | |||
| 863753580c | |||
| fa92ce3758 | |||
| 561f1bba0f | |||
| 268fce2616 | |||
| c4c547b73b | |||
| d487048224 | |||
| 1d15210ae2 | |||
| 5f377e5ec2 | |||
| e107329766 | |||
| 905b4786e8 | |||
| 36e7420bd0 | |||
| f03e612a14 | |||
| 2c4db99229 | |||
| 0f6f453ea0 | |||
| 1eef373ca0 | |||
| 7a03cd5a55 | |||
| e4fafce066 | |||
| 2104c49597 | |||
| c5b1f78118 | |||
| d2366650ec | |||
| 5338b7a05b | |||
| bd86d0b233 | |||
| f8442794a8 | |||
| 57c2af4900 | |||
| 6da4ea0d0d | |||
| 8f69721f24 | |||
| acd131c3d1 | |||
| 73b077790f | |||
| 62c4d60524 | |||
| d884913175 | |||
| fd5dae5e4d | |||
| d29c761135 | |||
| 700a4533aa | |||
| 3a394c633f | |||
| 79f9bec818 | |||
| 5b019365b7 | |||
| 038da0289e | |||
| f9b4afdfc5 | |||
| 65aaedbc83 | |||
| f325ddbdaa | |||
| bc9bdbaf83 | |||
| 6da29433cc | |||
| 4adfdf66b9 | |||
| e91b81a5aa | |||
| 0949d3b3ea | |||
| d44efc899d | |||
| c5870a746a | |||
| 58bca0e895 | |||
| 1bc3fd9ea9 | |||
| e84b949719 | |||
| 33bd1abb9d | |||
| ea62b200f7 | |||
| 5eb41b8a06 | |||
| 8f0c077fc7 | |||
| 430b9bb8c6 | |||
| 0fa81bfb6d | |||
| 54e9575391 | |||
| e3ad551ab3 | |||
| 7fc97c6ee2 | |||
| 241bf2c148 | |||
| cf06871200 | |||
| e50c2a6a74 | |||
| b175e4487e | |||
| 356131b1ea | |||
| 97a49aa3bf | |||
| e4a78c26d6 | |||
| c50e4c02bb | |||
| a14c9d66b1 | |||
| 2924dd29f7 | |||
| 9d4d9065b4 | |||
| 3f1a794cd9 | |||
| c5de4096b4 | |||
| 95d375c8d8 | |||
| b486d2695a | |||
| 8b34113c2a | |||
| fe2e52f646 | |||
| 0ea87ca3bf | |||
| 71fd23429c | |||
| 85514a9b34 | |||
| d039e0191f | |||
| cfdadfea01 | |||
| f70aa75445 | |||
| 86e0f1341e | |||
| cea6618265 | |||
| 5bf8f641f1 | |||
| 8b9f556d8f | |||
| d9b8a8ec54 | |||
| 4e04c266be | |||
| dc5c65eaf1 | |||
| 0353616be3 | |||
| 47853e1314 | |||
| 90e1658afe | |||
| e1d94d1e06 | |||
| a07c67a365 | |||
| 16cb219e4a | |||
| 944149ba71 | |||
| 9b14465243 | |||
| fdc0fd23df | |||
| 931dfbe8e7 | |||
| b075f0df02 | |||
| 8720a282ce | |||
| 6343002b32 | |||
| 0a9d4dc8fc |
@@ -0,0 +1,7 @@
|
||||
name: Build Release
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Build Release
|
||||
shell: bash
|
||||
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository publishAllPublicationsToDeploymentRepository
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Test Release
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Test Release
|
||||
shell: bash
|
||||
run: ./gradlew build
|
||||
@@ -1,2 +0,0 @@
|
||||
require:
|
||||
members: false
|
||||
@@ -1,113 +0,0 @@
|
||||
version: 2
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
shibboleth:
|
||||
type: maven-repository
|
||||
url: https://build.shibboleth.net/maven/releases
|
||||
updates:
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.5.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
- shibboleth
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.apache.directory.shared:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.4.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
- shibboleth
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.apache.directory.shared:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
|
||||
- package-ecosystem: gradle
|
||||
target-branch: main
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
- shibboleth
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.apache.directory.shared:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: com.gradle.enterprise
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
|
||||
- package-ecosystem: npm
|
||||
target-branch: docs-build
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
labels:
|
||||
- 'type: task'
|
||||
- 'in: build'
|
||||
|
||||
- package-ecosystem: npm
|
||||
target-branch: main
|
||||
directory: /docs
|
||||
schedule:
|
||||
interval: weekly
|
||||
labels:
|
||||
- 'type: task'
|
||||
- 'in: build'
|
||||
@@ -0,0 +1,17 @@
|
||||
workflow:
|
||||
generator:
|
||||
project:
|
||||
java:
|
||||
versions:
|
||||
primary: 17
|
||||
workflows:
|
||||
release-train:
|
||||
build:
|
||||
env:
|
||||
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
|
||||
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
|
||||
test:
|
||||
env:
|
||||
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
|
||||
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
snapshot-test:
|
||||
name: Test Against Snapshots
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- java-version: 21-ea
|
||||
toolchain: 21
|
||||
- java-version: 17
|
||||
toolchain: 17
|
||||
with:
|
||||
java-version: ${{ matrix.java-version }}
|
||||
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot,https://oss.sonatype.org/content/repositories/snapshots -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=7.+ -PreactorVersion=2025.+ -PspringDataVersion=2025.+ --stacktrace
|
||||
secrets: inherit
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ snapshot-test ]
|
||||
if: ${{ !success() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Clean build artifacts
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
permissions:
|
||||
contents: none
|
||||
steps:
|
||||
- name: Delete artifacts in cron job
|
||||
env:
|
||||
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: |
|
||||
echo "Running clean build artifacts logic"
|
||||
output=$(curl -X GET -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts | grep '"id"' | cut -d : -f2 | sed 's/,*$//g')
|
||||
echo Output is $output
|
||||
for id in $output; do curl -X DELETE -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts/$id; done;
|
||||
@@ -1,17 +0,0 @@
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule
|
||||
- cron: '0 5 * * *'
|
||||
permissions: read-all
|
||||
jobs:
|
||||
codeql-analysis-call:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@e415dadd0910c901e7a7fabd67bbb355b2324500 # 1
|
||||
@@ -8,55 +8,40 @@ on:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
jdk: [ 17 ]
|
||||
with:
|
||||
runs-on: ${{ matrix.os }}
|
||||
java-version: ${{ matrix.jdk }}
|
||||
distribution: temurin
|
||||
uses: spring-projects/spring-security-commercial/.github/workflows/build.yml@workflows/v1
|
||||
secrets: inherit
|
||||
test:
|
||||
name: Test Against Snapshots
|
||||
uses: spring-projects/spring-security-commercial/.github/workflows/test.yml@workflows/v1
|
||||
with:
|
||||
java-version: '17'
|
||||
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=17 -PspringFrameworkVersion=7.0.+ -PreactorVersion=2025.0.+ -PspringDataVersion=2025.1.+ -PmicrometerVersion=1.16.+ --stacktrace
|
||||
secrets: inherit
|
||||
compute-version:
|
||||
name: Compute Version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
snapshot: ${{ steps.project-version.outputs.snapshot }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0
|
||||
- id: project-version
|
||||
name: Extract Project Version
|
||||
uses: spring-io/spring-release-actions/compute-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
default-publish-milestones-central: true
|
||||
secrets: inherit
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
perform-release:
|
||||
name: Perform Release
|
||||
needs: [ deploy-artifacts, deploy-schema ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
|
||||
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
|
||||
milestone-repo-url: https://repo1.maven.org/maven2
|
||||
release-repo-url: https://repo1.maven.org/maven2
|
||||
artifact-path: org/springframework/security/spring-security-core
|
||||
slack-announcing-id: spring-security-announcing
|
||||
needs: [ build, test, compute-version ]
|
||||
if: needs.compute-version.outputs.snapshot == 'true'
|
||||
uses: spring-projects/spring-security-commercial/.github/workflows/deploy-artifacts.yml@workflows/v1
|
||||
secrets: inherit
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ perform-release ]
|
||||
needs: [ deploy-artifacts ]
|
||||
if: ${{ !success() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
name: Defer Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
defer-issues:
|
||||
name: Defer Issues
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Compute Version
|
||||
id: compute-version
|
||||
uses: spring-io/spring-release-actions/compute-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
- name: Get Today's Release Version
|
||||
id: todays-release
|
||||
uses: spring-io/spring-release-actions/get-todays-release-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
snapshot-version: ${{ steps.compute-version.outputs.version }}
|
||||
milestone-repository: ${{ github.repository }}
|
||||
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compute Next Version
|
||||
id: next-version
|
||||
uses: spring-io/spring-release-actions/compute-next-version@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
version: ${{ steps.todays-release.outputs.release-version }}
|
||||
- name: Schedule Next Milestone
|
||||
uses: spring-io/spring-release-actions/schedule-milestone@2420148725bebe44bd59a575a9b1961ca4459b0b # 0.0.4
|
||||
with:
|
||||
version: ${{ steps.next-version.outputs.version }}
|
||||
version-date: ${{ steps.next-version.outputs.version-date }}
|
||||
repository: ${{ github.repository }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Move Open Issues to Next Milestone
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CURRENT_MILESTONE: ${{ steps.todays-release.outputs.release-version }}
|
||||
NEXT_MILESTONE: ${{ steps.next-version.outputs.version }}
|
||||
run: |
|
||||
current_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
|
||||
--jq ".[] | select(.title == \"$CURRENT_MILESTONE\") | .number")
|
||||
if [ -z "$current_milestone_number" ]; then
|
||||
echo "No milestone found for $CURRENT_MILESTONE"
|
||||
exit 0
|
||||
fi
|
||||
next_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
|
||||
--jq ".[] | select(.title == \"$NEXT_MILESTONE\") | .number")
|
||||
if [ -z "$next_milestone_number" ]; then
|
||||
echo "No milestone found for $NEXT_MILESTONE"
|
||||
exit 1
|
||||
fi
|
||||
echo "Moving open issues from milestone '$CURRENT_MILESTONE' (#$current_milestone_number) to '$NEXT_MILESTONE' (#$next_milestone_number)"
|
||||
page=1
|
||||
while true; do
|
||||
issues=$(gh api "repos/${{ github.repository }}/issues?milestone=$current_milestone_number&state=open&per_page=100&page=$page" \
|
||||
--jq '.[].number')
|
||||
if [ -z "$issues" ]; then
|
||||
break
|
||||
fi
|
||||
for issue in $issues; do
|
||||
echo "Moving issue/PR #$issue to milestone $NEXT_MILESTONE"
|
||||
gh api repos/${{ github.repository }}/issues/$issue \
|
||||
--method PATCH \
|
||||
--field milestone=$next_milestone_number \
|
||||
--silent
|
||||
done
|
||||
page=$((page + 1))
|
||||
done
|
||||
echo "Done."
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Dependabot PR Build
|
||||
|
||||
on: pull_request_target
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
uses: spring-projects/spring-security-commercial/.github/workflows/build-pull-request.yml@workflows/v1
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
secrets: inherit
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "gh-pages"
|
||||
- "dependabot/**"
|
||||
tags: '**'
|
||||
repository_dispatch:
|
||||
types: request-build-reference # legacy
|
||||
#schedule:
|
||||
#- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
- name: Dispatch (partial build)
|
||||
if: github.ref_type == 'branch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
|
||||
- name: Dispatch (full build)
|
||||
if: github.ref_type == 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Finalize Release
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
inputs:
|
||||
version:
|
||||
description: The Spring Security release to finalize (e.g. 7.0.0-RC2)
|
||||
required: true
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
perform-release:
|
||||
name: Perform Release
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@ed473b4dafba053c63a453d2d88a89df3b3e18b3 # v1
|
||||
with:
|
||||
should-perform-release: true
|
||||
project-version: ${{ inputs.version }}
|
||||
milestone-repo-url: https://repo1.maven.org/maven2
|
||||
release-repo-url: https://repo1.maven.org/maven2
|
||||
artifact-path: org/springframework/security/spring-security-core
|
||||
slack-announcing-id: spring-security-announcing
|
||||
secrets: inherit
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Execute Gradle Wrapper Upgrade
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 2am UTC
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
jobs:
|
||||
upgrade_wrapper:
|
||||
name: Execution
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Git configuration
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config --global url."https://unused-username:${TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
- name: Upgrade Wrappers
|
||||
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
|
||||
env:
|
||||
WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Merge Dependabot PR
|
||||
|
||||
on: pull_request_target
|
||||
|
||||
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
merge-dependabot-pr:
|
||||
name: Merge Dependabot PR
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security-commercial' }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set Milestone to Dependabot Pull Request
|
||||
id: set-milestone
|
||||
run: |
|
||||
if test -f pom.xml
|
||||
then
|
||||
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
|
||||
else
|
||||
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
|
||||
fi
|
||||
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
|
||||
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
|
||||
|
||||
if [ -z $MILESTONE ]
|
||||
then
|
||||
gh run cancel ${{ github.run_id }}
|
||||
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
|
||||
else
|
||||
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
|
||||
echo mergeEnabled=true >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Merge Dependabot pull request
|
||||
if: steps.set-milestone.outputs.mergeEnabled
|
||||
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ merge-dependabot-pr ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Check Milestone
|
||||
on:
|
||||
milestone:
|
||||
types: [created, opened, edited]
|
||||
env:
|
||||
DUE_ON: ${{ github.event.milestone.due_on }}
|
||||
TITLE: ${{ github.event.milestone.title }}
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
spring-releasetrain-checks:
|
||||
name: Check DueOn is on a Release Date
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
permissions:
|
||||
contents: none
|
||||
steps:
|
||||
- name: Print Milestone Being Checked
|
||||
run: echo "Validating DueOn '$DUE_ON' for milestone '$TITLE'"
|
||||
- name: Validate DueOn
|
||||
if: env.DUE_ON != ''
|
||||
run: |
|
||||
export TOOL_VERSION=0.1.1
|
||||
wget "https://repo.maven.apache.org/maven2/io/spring/releasetrain/spring-release-train-tools/$TOOL_VERSION/spring-release-train-tools-$TOOL_VERSION.jar"
|
||||
java -cp "spring-release-train-tools-$TOOL_VERSION.jar" io.spring.releasetrain.CheckMilestoneDueOnMain --dueOn "$DUE_ON" --expectedDayOfWeek MONDAY --expectedMondayCount 3
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ spring-releasetrain-checks ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
@@ -8,44 +8,6 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue --scan
|
||||
generate-docs:
|
||||
name: Generate Docs
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@c8668747d7c264864c8c7f7026d0d277d14a78dc # v2.0.6
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Run Antora
|
||||
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
|
||||
- name: Upload Docs
|
||||
id: upload
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
overwrite: true
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ build, generate-docs ]
|
||||
if: ${{ failure() && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
uses: spring-projects/spring-security-commercial/.github/workflows/build-pull-request.yml@workflows/v1
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Release Announcements - Stage
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
- '[0-9]+.[0-9]+.[0-9]+.[0-9]+'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: The version to stage
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
stage-release-announcements:
|
||||
name: Stage Release Announcements
|
||||
uses: spring-projects/spring-security-commercial/.github/workflows/release-announcements-stage.yml@workflows/v1
|
||||
with:
|
||||
version: ${{ inputs.version || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Release Scheduler
|
||||
on:
|
||||
schedule:
|
||||
- cron: '15 15 * * MON' # Every Monday at 3:15pm UTC
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
jobs:
|
||||
dispatch_scheduled_releases:
|
||||
name: Dispatch scheduled releases
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
strategy:
|
||||
matrix:
|
||||
# List of active maintenance branches.
|
||||
branch: [ main, 6.5.x, 6.4.x, 6.3.x ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Dispatch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run update-scheduled-release-version.yml -r ${{ matrix.branch }}
|
||||
@@ -0,0 +1,92 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Build"
|
||||
run-name: "${{ inputs.callback-ref }} – Build"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
callback:
|
||||
description: "Repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
callback-ref:
|
||||
description: "Ref in the callback repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-maven-repository-url:
|
||||
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "read"
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
jobs:
|
||||
build-release:
|
||||
name: "Build Release"
|
||||
runs-on: "ubuntu22-2-8"
|
||||
steps:
|
||||
- name: "Prevent Re-runs"
|
||||
id: "prevent-re-runs"
|
||||
run: |-
|
||||
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
|
||||
echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry build failures"
|
||||
exit 1
|
||||
fi
|
||||
- name: "Set up Java"
|
||||
id: "set-up-java"
|
||||
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
|
||||
with:
|
||||
distribution: "liberica"
|
||||
java-version: "17"
|
||||
- name: "Check Out Code"
|
||||
id: "check-out-code"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
- name: "Build Release"
|
||||
id: "build-release"
|
||||
uses: "./.github/actions/release-train-build"
|
||||
env:
|
||||
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
|
||||
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
|
||||
- name: "Upload Deployment Repository"
|
||||
id: "upload-deployment-repository"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
name: "deployment-repository"
|
||||
path: "deployment-repository/**"
|
||||
- name: "Upload Deployment Spec"
|
||||
id: "upload-deployment-spec"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
archive: "false"
|
||||
if-no-files-found: "ignore"
|
||||
name: "deployment-spec"
|
||||
path: ".github/actions/release-train-build/deployment-spec.yml"
|
||||
- name: "Save Build System Caches"
|
||||
id: "save-build-system-caches"
|
||||
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
|
||||
with:
|
||||
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
|
||||
path: |-
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
- name: "Send Callback"
|
||||
id: "send-callback"
|
||||
if: "${{ !cancelled() }}"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run callback \
|
||||
--repo ${{ inputs.callback }} \
|
||||
--ref ${{ inputs.callback-ref }} \
|
||||
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
|
||||
--field deployment-repository-artifact-identifier=${{ steps.upload-deployment-repository.outputs.artifact-id }} \
|
||||
--field deployment-spec-artifact-identifier=${{ steps.upload-deployment-spec.outputs.artifact-id }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }} \
|
||||
--field result=${{ job.status == 'success' && 'built' || 'build-failed' }} \
|
||||
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@@ -0,0 +1,55 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Join"
|
||||
run-name: "${{ inputs.release-train }} – Join"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
deployment-destination:
|
||||
description: "Destination to which the release should be deployed"
|
||||
options:
|
||||
- "Maven Central"
|
||||
- "Spring Enterprise"
|
||||
required: true
|
||||
type: "choice"
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
join-release-train:
|
||||
name: "Join Release Train"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Join Release Train"
|
||||
id: "join-release-train"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
run_url=$(
|
||||
gh workflow run join \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field commit-hash=${{ github.sha }} \
|
||||
--field deployment-destination=${{ inputs.deployment-destination == 'Maven Central' && 'maven-central' || 'spring-enterprise' }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
)
|
||||
echo "Dispatched workflow run. Waiting for $run_url to complete."
|
||||
run_id=${run_url##*/}
|
||||
watch_exit_code=0
|
||||
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
|
||||
if [[ $watch_exit_code -eq 0 ]]; then
|
||||
echo "Workflow run succeeded."
|
||||
else
|
||||
echo "Workflow run failed."
|
||||
fi
|
||||
exit $watch_exit_code
|
||||
@@ -0,0 +1,46 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Leave"
|
||||
run-name: "${{ inputs.release-train }} – Leave"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
leave:
|
||||
name: "Leave"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Leave"
|
||||
id: "leave"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
run_url=$(
|
||||
gh workflow run leave \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
)
|
||||
echo "Dispatched workflow run. Waiting for $run_url to complete."
|
||||
run_id=${run_url##*/}
|
||||
watch_exit_code=0
|
||||
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
|
||||
if [[ $watch_exit_code -eq 0 ]]; then
|
||||
echo "Workflow run succeeded."
|
||||
else
|
||||
echo "Workflow run failed."
|
||||
fi
|
||||
exit $watch_exit_code
|
||||
@@ -0,0 +1,47 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Ready"
|
||||
run-name: "${{ inputs.release-train }} – Ready"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
ready:
|
||||
name: "Ready"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Ready"
|
||||
id: "ready"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
run_url=$(
|
||||
gh workflow run ready \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field commit-hash=${{ github.sha }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
)
|
||||
echo "Dispatched workflow run. Waiting for $run_url to complete."
|
||||
run_id=${run_url##*/}
|
||||
watch_exit_code=0
|
||||
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
|
||||
if [[ $watch_exit_code -eq 0 ]]; then
|
||||
echo "Workflow run succeeded."
|
||||
else
|
||||
echo "Workflow run failed."
|
||||
fi
|
||||
exit $watch_exit_code
|
||||
@@ -0,0 +1,34 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Retry"
|
||||
run-name: "${{ inputs.release-train }} – Retry"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
trigger-retry:
|
||||
name: "Trigger Retry"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Trigger Retry"
|
||||
id: "trigger-retry"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run retry \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
@@ -0,0 +1,83 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Test"
|
||||
run-name: "${{ inputs.callback-ref }} – Test"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
callback:
|
||||
description: "Repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
callback-ref:
|
||||
description: "Ref in the callback repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-maven-repository-url:
|
||||
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "read"
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
jobs:
|
||||
test-release:
|
||||
name: "Test Release"
|
||||
runs-on: "ubuntu22-2-8"
|
||||
steps:
|
||||
- name: "Prevent Re-runs"
|
||||
id: "prevent-re-runs"
|
||||
run: |-
|
||||
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
|
||||
echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry test failures"
|
||||
exit 1
|
||||
fi
|
||||
- name: "Set up Java"
|
||||
id: "set-up-java"
|
||||
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
|
||||
with:
|
||||
distribution: "liberica"
|
||||
java-version: "17"
|
||||
- name: "Check Out Code"
|
||||
id: "check-out-code"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
- name: "Restore Build System Caches"
|
||||
id: "restore-build-system-caches"
|
||||
uses: "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
|
||||
with:
|
||||
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
|
||||
path: |-
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
- name: "Test Release"
|
||||
id: "test-release"
|
||||
uses: "./.github/actions/release-train-test"
|
||||
env:
|
||||
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
|
||||
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
|
||||
- name: "Send Callback"
|
||||
id: "send-callback"
|
||||
if: "${{ !cancelled() }}"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run callback \
|
||||
--repo ${{ inputs.callback }} \
|
||||
--ref ${{ inputs.callback-ref }} \
|
||||
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }} \
|
||||
--field result=${{ job.status == 'success' && 'tested' || 'test-failed' }} \
|
||||
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
- name: "Upload Build System Reports"
|
||||
id: "upload-build-system-reports"
|
||||
if: "${{ failure() }}"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
name: "build-system-reports"
|
||||
path: "**/build/reports"
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Update Antora UI Spring
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-antora-ui-spring:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on Supported Branches
|
||||
strategy:
|
||||
matrix:
|
||||
branch: [ '6.4.x', '6.5.x', 'main' ]
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: ${{ matrix.branch }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
antora-file-path: 'docs/antora-playbook.yml'
|
||||
update-antora-ui-spring-docs-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on docs-build
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@415e2b11a766ba64799fffb5c97a4f7e17f677cf # v0.0.22
|
||||
name: Update
|
||||
with:
|
||||
docs-branch: 'docs-build'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Update Scheduled Release Version
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-scheduled-release-version:
|
||||
name: Update Scheduled Release Version
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
secrets: inherit
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ update-scheduled-release-version ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
+5
-8
@@ -25,6 +25,7 @@ apply plugin: 'org.jetbrains.kotlin.jvm'
|
||||
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
|
||||
apply plugin: 'org.springframework.security.check-expected-branch-version'
|
||||
apply plugin: 'io.spring.security.release'
|
||||
apply from: 'commercial-settings.gradle'
|
||||
|
||||
group = 'org.springframework.security'
|
||||
description = 'Spring Security'
|
||||
@@ -39,10 +40,12 @@ repositories {
|
||||
}
|
||||
|
||||
springRelease {
|
||||
repositoryOwner = "spring-projects"
|
||||
repositoryName = "spring-security-commercial"
|
||||
weekOfMonth = 3
|
||||
dayOfWeek = 1
|
||||
referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html"
|
||||
apiDocUrl = "https://docs.spring.io/spring-security/reference/{version}/api/java/index.html"
|
||||
referenceDocUrl = "https://docs.spring.vmware.com/spring-security/reference/{version}/index.html"
|
||||
apiDocUrl = "https://docs.spring.vmware.com/spring-security/reference/{version}/api/java/index.html"
|
||||
replaceSnapshotVersionInReferenceDocUrl = true
|
||||
}
|
||||
|
||||
@@ -70,12 +73,6 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
develocity {
|
||||
buildScan {
|
||||
termsOfUseUrl = 'https://gradle.com/help/legal-terms-of-use'
|
||||
termsOfUseAgree = 'yes'
|
||||
}
|
||||
}
|
||||
|
||||
nohttp {
|
||||
source.exclude "buildSrc/build/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**"
|
||||
|
||||
+20
-1
@@ -12,7 +12,26 @@ java {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
maven { url = 'https://repo.spring.io/snapshot' }
|
||||
if (project.hasProperty("artifactoryUsername") && project.hasProperty("artifactoryPassword")) {
|
||||
maven {
|
||||
name "spring-commercial-release"
|
||||
url "https://usw1.packages.broadcom.com/spring-enterprise-maven-prod-local"
|
||||
credentials {
|
||||
username project.artifactoryUsername
|
||||
password project.artifactoryPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
if (System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL") != null) {
|
||||
maven {
|
||||
name = "Release Train"
|
||||
url = System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL")
|
||||
credentials {
|
||||
username = System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME")
|
||||
password = System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
+10
@@ -43,6 +43,16 @@ class RepositoryConventionPlugin implements Plugin<Project> {
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
if (System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL") != null) {
|
||||
maven {
|
||||
name = "Release Train"
|
||||
url = System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL")
|
||||
credentials {
|
||||
username = System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME")
|
||||
password = System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSnapshot) {
|
||||
maven {
|
||||
name = 'artifactory-snapshot'
|
||||
|
||||
+6
@@ -22,6 +22,12 @@ public class MavenPublishingConventionsPlugin implements Plugin<Project> {
|
||||
@Override
|
||||
public void execute(MavenPublishPlugin mavenPublish) {
|
||||
PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
|
||||
if (project.hasProperty("deploymentRepository")) {
|
||||
publishing.getRepositories().maven((mavenRepository) -> {
|
||||
mavenRepository.setUrl(project.property("deploymentRepository"));
|
||||
mavenRepository.setName("deployment");
|
||||
});
|
||||
}
|
||||
publishing.getPublications().withType(MavenPublication.class)
|
||||
.all((mavenPublication) -> MavenPublishingConventionsPlugin.this.customizePom(mavenPublication.getPom(), project));
|
||||
MavenPublishingConventionsPlugin.this.customizeJavaPlugin(project);
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
|
||||
protected CasAuthenticationToken(Builder<?> builder) {
|
||||
super(builder);
|
||||
Assert.isTrue(!"".equals(builder.principal), "principal cannot be null or empty");
|
||||
Assert.notNull(!"".equals(builder.credentials), "credentials cannot be null or empty");
|
||||
Assert.isTrue(!"".equals(builder.credentials), "credentials cannot be null or empty");
|
||||
Assert.notNull(builder.userDetails, "userDetails cannot be null");
|
||||
Assert.notNull(builder.assertion, "assertion cannot be null");
|
||||
this.keyHash = builder.keyHash;
|
||||
|
||||
+35
@@ -34,6 +34,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests {@link CasAuthenticationToken}.
|
||||
@@ -182,4 +183,38 @@ public class CasAuthenticationTokenTests {
|
||||
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toBuilderWhenPrincipalIsEmpty() {
|
||||
final Assertion assertion = new AssertionImpl("test");
|
||||
CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES,
|
||||
makeUserDetails(), assertion);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> token.toBuilder().principal(null).build());
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> token.toBuilder().principal("").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toBuilderWhenPrincipalIsNotEmpty() {
|
||||
final Assertion assertion = new AssertionImpl("test");
|
||||
CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES,
|
||||
makeUserDetails(), assertion);
|
||||
assertThatNoException().isThrownBy(() -> token.toBuilder().principal("principal").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toBuilderWhenCredentialsIsEmpty() {
|
||||
final Assertion assertion = new AssertionImpl("test");
|
||||
CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES,
|
||||
makeUserDetails(), assertion);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> token.toBuilder().credentials(null).build());
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> token.toBuilder().credentials("").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toBuilderWhenCredentialsIsNotEmpty() {
|
||||
final Assertion assertion = new AssertionImpl("test");
|
||||
CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES,
|
||||
makeUserDetails(), assertion);
|
||||
assertThatNoException().isThrownBy(() -> token.toBuilder().credentials("credentials").build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
subprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
def repoUsername = project.findProperty("artifactoryUsername") ?: System.getenv("COMMERCIAL_REPO_USERNAME")
|
||||
def repoPassword = project.findProperty("artifactoryPassword") ?: System.getenv("COMMERCIAL_REPO_PASSWORD")
|
||||
if (repoUsername && repoPassword) {
|
||||
maven {
|
||||
name "spring-commercial-release"
|
||||
url "https://usw1.packages.broadcom.com/spring-enterprise-maven-prod-local"
|
||||
credentials {
|
||||
username repoUsername
|
||||
password repoPassword
|
||||
}
|
||||
}
|
||||
if ("$version".endsWith("-SNAPSHOT")) {
|
||||
maven {
|
||||
name "spring-commercial-snapshot"
|
||||
url "https://usw1.packages.broadcom.com/spring-enterprise-maven-dev-local"
|
||||
credentials {
|
||||
username repoUsername
|
||||
password repoPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.config.annotation.authentication.configurers.ldap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
|
||||
@@ -590,7 +591,7 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
|
||||
}
|
||||
|
||||
private int getDefaultPort() {
|
||||
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT, 50, InetAddress.getLoopbackAddress())) {
|
||||
return serverSocket.getLocalPort();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
|
||||
+13
@@ -39,6 +39,9 @@ import org.springframework.util.ClassUtils;
|
||||
@Deprecated
|
||||
final class GlobalMethodSecuritySelector implements ImportSelector {
|
||||
|
||||
private static final boolean isAccessPresent = ClassUtils.isPresent(
|
||||
"org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor", null);
|
||||
|
||||
@Override
|
||||
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
|
||||
Class<EnableGlobalMethodSecurity> annoType = EnableGlobalMethodSecurity.class;
|
||||
@@ -59,16 +62,26 @@ final class GlobalMethodSecuritySelector implements ImportSelector {
|
||||
boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled");
|
||||
List<String> classNames = new ArrayList<>(4);
|
||||
if (isProxy) {
|
||||
assertAccessModulePresent();
|
||||
classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName());
|
||||
}
|
||||
classNames.add(autoProxyClassName);
|
||||
if (!skipMethodSecurityConfiguration) {
|
||||
assertAccessModulePresent();
|
||||
classNames.add(GlobalMethodSecurityConfiguration.class.getName());
|
||||
}
|
||||
if (jsr250Enabled) {
|
||||
assertAccessModulePresent();
|
||||
classNames.add(Jsr250MetadataSourceConfiguration.class.getName());
|
||||
}
|
||||
return classNames.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private static void assertAccessModulePresent() {
|
||||
Assert.state(isAccessPresent,
|
||||
() -> "@EnableGlobalMethodSecurity requires the spring-security-access dependency on the "
|
||||
+ "classpath. Please add spring-security-access, or migrate to @EnableMethodSecurity "
|
||||
+ "which does not require it.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -26,6 +26,7 @@ import org.springframework.context.annotation.AutoProxyRegistrar;
|
||||
import org.springframework.context.annotation.ImportSelector;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -35,6 +36,8 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class ReactiveMethodSecuritySelector implements ImportSelector {
|
||||
|
||||
private static final String METHOD_SECURITY_METADATA_SOURCE_ADVISOR = "org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor";
|
||||
|
||||
private static final boolean isDataPresent = ClassUtils
|
||||
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
|
||||
|
||||
@@ -56,6 +59,11 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
|
||||
imports.add(ReactiveAuthorizationManagerMethodSecurityConfiguration.class.getName());
|
||||
}
|
||||
else {
|
||||
Assert.state(
|
||||
ClassUtils.isPresent(METHOD_SECURITY_METADATA_SOURCE_ADVISOR, ClassUtils.getDefaultClassLoader()),
|
||||
() -> "@EnableReactiveMethodSecurity(useAuthorizationManager = false) requires the "
|
||||
+ "spring-security-access dependency on the classpath. Please add spring-security-access, "
|
||||
+ "or use the default useAuthorizationManager = true which does not require it.");
|
||||
imports.add(ReactiveMethodSecurityConfiguration.class.getName());
|
||||
}
|
||||
if (isDataPresent) {
|
||||
|
||||
+3
-1
@@ -2035,7 +2035,9 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
*/
|
||||
public HttpSecurity securityMatcher(String... patterns) {
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
PathPatternRequestMatcher.Builder builder = getSharedObject(PathPatternRequestMatcher.Builder.class);
|
||||
ApplicationContext context = getSharedObject(ApplicationContext.class);
|
||||
PathPatternRequestMatcher.Builder builder = context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
|
||||
.getIfUnique(() -> getSharedObject(PathPatternRequestMatcher.Builder.class));
|
||||
for (String pattern : patterns) {
|
||||
matchers.add(builder.matcher(pattern));
|
||||
}
|
||||
|
||||
+2
-3
@@ -379,9 +379,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
|
||||
}
|
||||
if (filter instanceof AuthorizationFilter authorization) {
|
||||
AuthorizationManager<HttpServletRequest> authorizationManager = authorization.getAuthorizationManager();
|
||||
builder.add(securityFilterChain::matches,
|
||||
(authentication, context) -> (AuthorizationDecision) authorizationManager
|
||||
.authorize(authentication, context.getRequest()));
|
||||
builder.add(securityFilterChain::matches, (authentication, context) -> authorizationManager
|
||||
.authorize(authentication, context.getRequest()));
|
||||
mappings = true;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -238,9 +238,7 @@ class HttpSecurityConfiguration {
|
||||
Map<Class<?>, Object> sharedObjects = new HashMap<>();
|
||||
sharedObjects.put(ApplicationContext.class, this.context);
|
||||
sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy);
|
||||
sharedObjects.put(PathPatternRequestMatcher.Builder.class,
|
||||
this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
|
||||
.getIfUnique(() -> constructRequestMatcherBuilder(this.context)));
|
||||
sharedObjects.put(PathPatternRequestMatcher.Builder.class, constructRequestMatcherBuilder(this.context));
|
||||
return sharedObjects;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -79,8 +79,10 @@ final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
@Override
|
||||
public void configure(B http) {
|
||||
DPoPAuthenticationProvider authenticationProvider = new DPoPAuthenticationProvider(
|
||||
getTokenAuthenticationManager(http));
|
||||
http.authenticationProvider(postProcess(authenticationProvider));
|
||||
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
||||
http.authenticationProvider(new DPoPAuthenticationProvider(getTokenAuthenticationManager(http)));
|
||||
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager,
|
||||
getAuthenticationConverter());
|
||||
authenticationFilter.setRequestMatcher(getRequestMatcher());
|
||||
|
||||
+3
@@ -33,6 +33,7 @@ import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
@@ -298,6 +299,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
|
||||
if (dPoPAuthenticationAvailable) {
|
||||
DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
|
||||
dPoPAuthenticationConfigurer.withObjectPostProcessor(
|
||||
(ObjectPostProcessor<Object>) OAuth2ResourceServerConfigurer.this::postProcess);
|
||||
dPoPAuthenticationConfigurer.configure(http);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.config.ldap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
@@ -188,7 +189,7 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
|
||||
private String getDefaultPort() {
|
||||
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT, 50, InetAddress.getLoopbackAddress())) {
|
||||
return String.valueOf(serverSocket.getLocalPort());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
|
||||
+2
-4
@@ -301,8 +301,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
private static String getCurrentVersion() {
|
||||
String version = System.getProperty("springSecurityVersion");
|
||||
String[] parts = version.split("\\.");
|
||||
parts[2] = "x";
|
||||
return String.join(".", parts);
|
||||
return parts[0] + "." + parts[1] + ".x";
|
||||
}
|
||||
|
||||
private static String getPreviousVersion() {
|
||||
@@ -315,8 +314,7 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
parts[0] = String.valueOf(Integer.parseInt(parts[0]) - 1);
|
||||
parts[1] = "5"; // FIXME: this should not be hard coded
|
||||
}
|
||||
parts[2] = "x";
|
||||
return String.join(".", parts);
|
||||
return parts[0] + "." + parts[1] + ".x";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.AdviceMode;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.test.support.ClassPathExclusions;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for gh-19441: {@code spring-security-access} moved
|
||||
* {@link org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor}
|
||||
* out of {@code spring-security-core} and into the optional
|
||||
* {@code spring-security-access} module. {@link EnableMethodSecurity} and
|
||||
* {@link EnableReactiveMethodSecurity}'s default (AuthorizationManager-based) mode never
|
||||
* needed that class and continue to work without {@code spring-security-access} on the
|
||||
* classpath, but the deprecated legacy method security annotations do need it and
|
||||
* previously failed with a confusing {@link NoClassDefFoundError} instead of an
|
||||
* actionable message.
|
||||
*/
|
||||
@ClassPathExclusions("spring-security-access-*.jar")
|
||||
public class Gh19441Tests {
|
||||
|
||||
@Test
|
||||
public void enableMethodSecurityWhenAccessModuleAbsentThenContextStartsCleanly() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(EnableMethodSecurityConfig.class);
|
||||
context.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableReactiveMethodSecurityWhenAccessModuleAbsentThenContextStartsCleanly() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(EnableReactiveMethodSecurityConfig.class);
|
||||
context.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableGlobalMethodSecurityWhenProxyModeAndAccessModuleAbsentThenClearException() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(EnableGlobalMethodSecurityProxyConfig.class);
|
||||
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("spring-security-access");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableGlobalMethodSecurityWhenAspectJModeAndAccessModuleAbsentThenClearException() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(EnableGlobalMethodSecurityAspectJConfig.class);
|
||||
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("spring-security-access");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableReactiveMethodSecurityWhenUseAuthorizationManagerFalseAndAccessModuleAbsentThenClearException() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(EnableReactiveMethodSecurityLegacyConfig.class);
|
||||
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("spring-security-access");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableGlobalMethodSecurityWhenAspectJModeAndJsr250EnabledAndConfigurationSubclassedAndAccessModuleAbsentThenClearException() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(EnableGlobalMethodSecurityAspectJJsr250SubclassedConfig.class);
|
||||
assertThatExceptionOfType(Exception.class).isThrownBy(context::refresh)
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("spring-security-access");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
static class EnableMethodSecurityConfig {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
static class EnableReactiveMethodSecurityConfig {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
static class EnableGlobalMethodSecurityProxyConfig {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.ASPECTJ)
|
||||
static class EnableGlobalMethodSecurityAspectJConfig {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager = false)
|
||||
static class EnableReactiveMethodSecurityLegacyConfig {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableGlobalMethodSecurity(jsr250Enabled = true, mode = AdviceMode.ASPECTJ)
|
||||
static class EnableGlobalMethodSecurityAspectJJsr250SubclassedConfig extends GlobalMethodSecurityConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -37,6 +37,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
@@ -110,6 +111,21 @@ public class WebSecurityTests {
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void ignoringWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception {
|
||||
loadConfig(IgnoringBuilderBeanConfig.class);
|
||||
this.request.setServletPath("/spring");
|
||||
this.request.setRequestURI("/spring/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
setup();
|
||||
this.request.setServletPath("");
|
||||
this.request.setRequestURI("/path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public void loadConfig(Class<?>... configs) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.register(configs);
|
||||
@@ -201,6 +217,52 @@ public class WebSecurityTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
static class IgnoringBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers("/path");
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.httpBasic(withDefaults())
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().denyAll());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class PathController {
|
||||
|
||||
@RequestMapping("/path")
|
||||
String path() {
|
||||
return "path";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class RequestRejectedHandlerConfig {
|
||||
|
||||
+43
@@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.AuthenticationTestConfiguration;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextChangedListener;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
@@ -153,6 +154,17 @@ public class FormLoginConfigurerTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void loginWhenBuilderBeanWithBasePathThenLoginProcessingUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(FormLoginBuilderBeanConfig.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(formLogin().user("invalid"))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?error"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenFormLoginConfiguredThenHasDefaultSuccessUrl() throws Exception {
|
||||
this.spring.register(FormLoginConfig.class).autowire();
|
||||
@@ -519,6 +531,37 @@ public class FormLoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class FormLoginBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.formLogin(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class FormLoginInLambdaConfig {
|
||||
|
||||
+35
@@ -35,6 +35,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
@@ -127,6 +128,17 @@ public class LogoutConfigurerTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void logoutWhenBuilderBeanWithBasePathThenLogoutUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(LogoutBuilderBeanConfig.class).autowire();
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/logout").with(csrf()))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// SEC-2311
|
||||
@Test
|
||||
public void logoutWhenGetRequestAndCsrfDisabledThenRedirectsToLogin() throws Exception {
|
||||
@@ -524,6 +536,29 @@ public class LogoutConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class LogoutBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CsrfDisabledConfig {
|
||||
|
||||
+34
@@ -26,6 +26,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@@ -66,6 +67,16 @@ public class PasswordManagementConfigurerTests {
|
||||
.andExpect(redirectedUrl("/custom-change-password-page"));
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void changePasswordWhenBuilderBeanWithBasePathThenChangePasswordUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(PasswordManagementBuilderBeanConfig.class).autowire();
|
||||
|
||||
this.mvc.perform(get("/.well-known/change-password"))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/change-password"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenSettingNullChangePasswordPage() {
|
||||
PasswordManagementConfigurer configurer = new PasswordManagementConfigurer();
|
||||
@@ -102,6 +113,29 @@ public class PasswordManagementConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class PasswordManagementBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
return http
|
||||
.passwordManagement(withDefaults())
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class PasswordManagementWithCustomChangePasswordPageConfig {
|
||||
|
||||
+41
@@ -34,6 +34,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
|
||||
@@ -185,6 +186,21 @@ public class RequestCacheConfigurerTests {
|
||||
this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void getWhenBuilderBeanWithBasePathThenSavedRequestMatcherIgnoresBasePath() throws Exception {
|
||||
this.spring.register(RequestCacheBuilderBeanConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
|
||||
// @formatter:off
|
||||
MockHttpSession session = (MockHttpSession) this.mvc.perform(request)
|
||||
.andExpect(redirectedUrl("/login"))
|
||||
.andReturn()
|
||||
.getRequest()
|
||||
.getSession();
|
||||
// @formatter:on
|
||||
this.mvc.perform(formLogin(session)).andExpect(RequestCacheResultMatcher.redirectToCachedRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
@@ -401,6 +417,31 @@ public class RequestCacheConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.formLogin(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDisabledConfig {
|
||||
|
||||
+38
@@ -56,6 +56,7 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli
|
||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.context.DelegatingApplicationListener;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -440,6 +441,18 @@ public class OAuth2LoginConfigurerTests {
|
||||
then(redirectStrategy).should().sendRedirect(any(), any(), anyString());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void oauth2LoginWhenBuilderBeanWithBasePathThenLoginProcessingUrlIgnoresBasePath() throws Exception {
|
||||
loadConfig(OAuth2LoginBuilderBeanConfig.class);
|
||||
String requestUri = "/login/oauth2/code/google";
|
||||
this.request = get(requestUri).build();
|
||||
this.request.setParameter("code", "code123");
|
||||
this.request.setParameter("state", "state123");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThat(this.response.getRedirectedUrl()).endsWith("/login?error");
|
||||
}
|
||||
|
||||
// gh-5347
|
||||
@Test
|
||||
public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception {
|
||||
@@ -788,6 +801,31 @@ public class OAuth2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginBuilderBeanConfig extends CommonSecurityFilterChainConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.oauth2Login((login) -> login
|
||||
.clientRegistrationRepository(
|
||||
new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)));
|
||||
// @formatter:on
|
||||
return super.configureFilterChain(http);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginConfigFormLogin extends CommonSecurityFilterChainConfig {
|
||||
|
||||
+31
-2
@@ -22,6 +22,7 @@ import java.security.interfaces.ECPrivateKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
@@ -30,6 +31,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
@@ -47,20 +49,26 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.DPoPProofContext;
|
||||
import org.springframework.security.oauth2.jwt.DPoPProofJwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.jwt.DPoPProofReplayValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -245,8 +253,10 @@ public class DPoPAuthenticationConfigurerTests {
|
||||
.requestMatchers("/resource2").hasAnyAuthority("SCOPE_resource2.read", "SCOPE_resource2.write")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer((oauth2) -> oauth2
|
||||
.jwt(Customizer.withDefaults()));
|
||||
.oauth2ResourceServer((oauth2ResourceServer) ->
|
||||
oauth2ResourceServer
|
||||
.jwt(Customizer.withDefaults())
|
||||
.withObjectPostProcessor(dPoPProofVerifierFactoryCustomizer()));
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
@@ -256,6 +266,25 @@ public class DPoPAuthenticationConfigurerTests {
|
||||
return NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build();
|
||||
}
|
||||
|
||||
private ObjectPostProcessor<DPoPAuthenticationProvider> dPoPProofVerifierFactoryCustomizer() {
|
||||
return new ObjectPostProcessor<>() {
|
||||
@Override
|
||||
public <O extends DPoPAuthenticationProvider> O postProcess(O authenticationProvider) {
|
||||
DPoPProofReplayValidator.InMemoryCache inMemoryCache = new DPoPProofReplayValidator.InMemoryCache();
|
||||
inMemoryCache.setMaxSize(50_000);
|
||||
inMemoryCache.setMaxRequestsPerKey(500);
|
||||
DPoPProofReplayValidator dPoPProofReplayValidator = new DPoPProofReplayValidator(inMemoryCache);
|
||||
dPoPProofReplayValidator.setClockSkew(Duration.ofSeconds(60));
|
||||
Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = DPoPProofJwtDecoderFactory
|
||||
.createDefaultJwtValidatorFactory(Collections.singletonList(dPoPProofReplayValidator));
|
||||
DPoPProofJwtDecoderFactory dPoPProofJwtDecoderFactory = new DPoPProofJwtDecoderFactory();
|
||||
dPoPProofJwtDecoderFactory.setJwtValidatorFactory(jwtValidatorFactory);
|
||||
authenticationProvider.setDPoPProofVerifierFactory(dPoPProofJwtDecoderFactory);
|
||||
return authenticationProvider;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
|
||||
+49
@@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
@@ -98,6 +99,19 @@ public class OneTimeTokenLoginConfigurerTests {
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
void oneTimeTokenWhenBuilderBeanWithBasePathThenGenerateAndLoginUrlsIgnoreBasePath() throws Exception {
|
||||
this.spring.register(OneTimeTokenBuilderBeanConfig.class).autowire();
|
||||
this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf()))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
|
||||
|
||||
String token = getLastToken().getTokenValue();
|
||||
|
||||
this.mvc.perform(post("/login/ott").param("token", token).with(csrf()))
|
||||
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneTimeTokenWhenDifferentAuthenticationUrlsThenCanAuthenticate() throws Exception {
|
||||
this.spring.register(OneTimeTokenDifferentUrlsConfig.class).autowire();
|
||||
@@ -296,6 +310,41 @@ public class OneTimeTokenLoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
@Import(UserDetailsServiceConfig.class)
|
||||
static class OneTimeTokenBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http,
|
||||
OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oneTimeTokenLogin((ott) -> ott
|
||||
.tokenGenerationSuccessHandler(ottSuccessHandler)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
|
||||
return new TestOneTimeTokenGenerationSuccessHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
@Import(UserDetailsServiceConfig.class)
|
||||
|
||||
+36
@@ -57,6 +57,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
@@ -244,6 +245,14 @@ public class Saml2LoginConfigurerTests {
|
||||
performSaml2Login("ROLE_AUTH_MANAGER");
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void saml2LoginWhenBuilderBeanWithBasePathThenAuthenticateUriIgnoresBasePath() throws Exception {
|
||||
this.spring.register(Saml2LoginBuilderBeanConfig.class).autowire();
|
||||
MvcResult result = this.mvc.perform(get("/saml2/authenticate/registration-id")).andReturn();
|
||||
assertThat(result.getResponse().getRedirectedUrl()).contains("SAMLRequest");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticationRequestWhenAuthenticationRequestResolverBeanThenUses() throws Exception {
|
||||
this.spring.register(CustomAuthenticationRequestResolverBean.class).autowire();
|
||||
@@ -522,6 +531,33 @@ public class Saml2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
static class Saml2LoginBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authz) -> authz
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.saml2Login(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
|
||||
+38
@@ -49,6 +49,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.saml2.core.Saml2Utils;
|
||||
@@ -196,6 +197,17 @@ public class Saml2LogoutConfigurerTests {
|
||||
verify(logoutHandler).logout(any(), any(), any());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
public void saml2LogoutWhenBuilderBeanWithBasePathThenLogoutUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(Saml2LogoutBuilderBeanConfig.class).autowire();
|
||||
MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf()))
|
||||
.andExpect(status().isFound())
|
||||
.andReturn();
|
||||
String location = result.getResponse().getHeader("Location");
|
||||
assertThat(location).startsWith("https://ap.example.org/logout/saml2/request");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception {
|
||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
||||
@@ -577,6 +589,32 @@ public class Saml2LogoutConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
static class Saml2LogoutBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.saml2Login(withDefaults())
|
||||
.saml2Logout(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(Saml2LoginConfigBeans.class)
|
||||
|
||||
+35
@@ -30,6 +30,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
|
||||
import org.springframework.security.saml2.provider.service.metadata.OpenSaml5MetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
|
||||
@@ -124,6 +125,15 @@ public class Saml2MetadataConfigurerTests {
|
||||
this.mvc.perform(get("/saml2/metadata")).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@Test
|
||||
void saml2MetadataWhenBuilderBeanWithBasePathThenMetadataUrlIgnoresBasePath() throws Exception {
|
||||
this.spring.register(MetadataBuilderBeanConfig.class).autowire();
|
||||
this.mvc.perform(get("/saml/metadata"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("md:EntityDescriptor")));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@Import(RelyingPartyRegistrationConfig.class)
|
||||
@@ -141,6 +151,31 @@ public class Saml2MetadataConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-19128
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@Import(RelyingPartyRegistrationConfig.class)
|
||||
static class MetadataBuilderBeanConfig {
|
||||
|
||||
@Bean
|
||||
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||
bean.setBasePath("/spring");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata"));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@Import(RelyingPartyRegistrationConfig.class)
|
||||
|
||||
+1
-3
@@ -78,9 +78,7 @@ public abstract class AbstractSecurityExpressionHandler<T>
|
||||
public final EvaluationContext createEvaluationContext(@Nullable Authentication authentication, T invocation) {
|
||||
SecurityExpressionOperations root = createSecurityExpressionRoot(authentication, invocation);
|
||||
StandardEvaluationContext ctx = createEvaluationContextInternal(authentication, invocation);
|
||||
if (this.beanResolver != null) {
|
||||
ctx.setBeanResolver(this.beanResolver);
|
||||
}
|
||||
ctx.setBeanResolver(this.beanResolver);
|
||||
ctx.setRootObject(root);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
+1
-3
@@ -23,7 +23,6 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -89,13 +88,12 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway") // FIXME: Dataflow analysis limitation
|
||||
public EvaluationContext createEvaluationContext(Supplier<? extends @Nullable Authentication> authentication,
|
||||
MethodInvocation mi) {
|
||||
MethodSecurityExpressionOperations root = createSecurityExpressionRoot(authentication, mi);
|
||||
MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(root, mi,
|
||||
getParameterNameDiscoverer());
|
||||
Optional.ofNullable(getBeanResolver()).ifPresent(ctx::setBeanResolver);
|
||||
ctx.setBeanResolver(getBeanResolver());
|
||||
return ctx;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager
|
||||
* @since 5.2
|
||||
*/
|
||||
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
|
||||
Assert.notNull(this.postAuthenticationChecks, "postAuthenticationChecks cannot be null");
|
||||
Assert.notNull(postAuthenticationChecks, "postAuthenticationChecks cannot be null");
|
||||
this.postAuthenticationChecks = postAuthenticationChecks;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ public class KeyBasedPersistenceTokenService implements TokenService, Initializi
|
||||
// Verification
|
||||
String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInfo.toString();
|
||||
String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
|
||||
Assert.isTrue(expectedSha512Hex.equals(sha1Hex), "Key verification failure");
|
||||
Assert.isTrue(Utf8.isEqual(expectedSha512Hex, sha1Hex), "Key verification failure");
|
||||
return new DefaultToken(key, creationTime, extendedInfo.toString());
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
|
||||
else {
|
||||
this.logger.debug("No authentication manager set. Password won't be re-checked.");
|
||||
}
|
||||
MutableUserDetails user = this.users.get(username);
|
||||
MutableUserDetails user = this.users.get(username.toLowerCase(Locale.ROOT));
|
||||
Assert.state(user != null, "Current user doesn't exist in database.");
|
||||
user.setPassword(newPassword);
|
||||
}
|
||||
|
||||
+6
@@ -90,6 +90,12 @@ public class UserDetailsRepositoryReactiveAuthenticationManagerTests {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> this.manager.setScheduler(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPostAuthenticationChecksWhenNullThenIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.manager.setPostAuthenticationChecks(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenCustomSchedulerThenUsed() {
|
||||
given(this.scheduler.schedule(any())).willAnswer((a) -> {
|
||||
|
||||
+16
@@ -123,6 +123,22 @@ public class InMemoryUserDetailsManagerTests {
|
||||
verify(strategy).getContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changePasswordWhenCurrentUsernameIsNotInLowercaseThenChangesPassword() {
|
||||
UserDetails userNotLowerCase = User.withUserDetails(PasswordEncodedUser.user()).username("User").build();
|
||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(userNotLowerCase);
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken("User", userNotLowerCase.getPassword(),
|
||||
userNotLowerCase.getAuthorities());
|
||||
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
|
||||
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
|
||||
manager.setSecurityContextHolderStrategy(strategy);
|
||||
|
||||
String newPassword = "newPassword";
|
||||
manager.changePassword(userNotLowerCase.getPassword(), newPassword);
|
||||
|
||||
assertThat(manager.loadUserByUsername("User").getPassword()).isEqualTo(newPassword);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUserWhenUserAlreadyExistsThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.manager.createUser(this.user))
|
||||
|
||||
@@ -21,6 +21,9 @@ import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* UTF-8 Charset encoder/decoder.
|
||||
@@ -66,4 +69,21 @@ public final class Utf8 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant time comparison to prevent against timing attacks.
|
||||
* @param expected the expected {@link CharSequence}
|
||||
* @param actual the actual {@link CharSequence}
|
||||
* @return true if {@code expected} and {@code actual} are equal, false otherwise
|
||||
* @since 5.7.26
|
||||
*/
|
||||
public static boolean isEqual(@Nullable CharSequence expected, @Nullable CharSequence actual) {
|
||||
byte[] expectedBytes = bytesUtf8(expected);
|
||||
byte[] actualBytes = bytesUtf8(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
private static byte @Nullable [] bytesUtf8(@Nullable CharSequence s) {
|
||||
return (s != null) ? Utf8.encode(s) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -37,7 +37,9 @@ import org.springframework.security.crypto.util.EncodingUtils;
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Dave Syer
|
||||
* @deprecated Use {@link AesCbcBytesEncryptor} or {@link AesGcmBytesEncryptor} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class AesBytesEncryptor implements BytesEncryptor {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.crypto.encrypt;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
import org.springframework.security.crypto.util.EncodingUtils;
|
||||
|
||||
/**
|
||||
* {@link BytesEncryptor} that uses 256-bit AES/CBC/PKCS5Padding with a random 16-byte
|
||||
* initialization vector. The IV is prepended to the ciphertext on encrypt and stripped on
|
||||
* decrypt.
|
||||
*
|
||||
* <p>
|
||||
* Note that CBC mode provides confidentiality but <em>not</em> integrity or authenticity.
|
||||
* Applications that require authenticated encryption should prefer
|
||||
* {@link AesGcmBytesEncryptor}. See the <a href=
|
||||
* "https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html">
|
||||
* OWASP Cryptographic Storage Cheat Sheet</a> for guidance on choosing a cipher mode.
|
||||
*
|
||||
* <p>
|
||||
* When key derivation is used via {@link #withPassword(String, CharSequence)}, the key is
|
||||
* derived using PBKDF2WithHmacSHA256 with {@code DEFAULT_PBKDF2_ITERATIONS} iterations
|
||||
* per the <a href=
|
||||
* "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">
|
||||
* OWASP Password Storage Cheat Sheet</a>. Because derivation is intentionally expensive,
|
||||
* the encryptor instance should be created once and reused rather than constructed
|
||||
* per-operation.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.7.26
|
||||
* @see AesGcmBytesEncryptor
|
||||
* @see AesBytesEncryptor
|
||||
*/
|
||||
public final class AesCbcBytesEncryptor implements BytesEncryptor {
|
||||
|
||||
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||
|
||||
private static final int IV_LENGTH_BYTES = 16;
|
||||
|
||||
private static final int DEFAULT_PBKDF2_ITERATIONS = 600_000;
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
private final Cipher encryptor;
|
||||
|
||||
private final Cipher decryptor;
|
||||
|
||||
private final BytesKeyGenerator ivGenerator;
|
||||
|
||||
private AesCbcBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator) {
|
||||
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
|
||||
this.encryptor = CipherUtils.newCipher(ALGORITHM);
|
||||
this.decryptor = CipherUtils.newCipher(ALGORITHM);
|
||||
this.ivGenerator = ivGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encryptor that derives its key from the given password and hex-encoded
|
||||
* salt using PBKDF2WithHmacSHA1.
|
||||
* @param password the password value
|
||||
* @param salt the hex-encoded salt value
|
||||
*/
|
||||
public static Builder withPassword(String password, CharSequence salt) {
|
||||
return new Builder(password, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encryptor using the supplied {@link SecretKey}.
|
||||
* @param secretKey the secret (symmetric) key
|
||||
*/
|
||||
public static Builder withSecretKey(SecretKey secretKey) {
|
||||
return new Builder(secretKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] bytes) {
|
||||
synchronized (this.encryptor) {
|
||||
byte[] iv = this.ivGenerator.generateKey();
|
||||
CipherUtils.initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey, new IvParameterSpec(iv));
|
||||
byte[] ciphertext = CipherUtils.doFinal(this.encryptor, bytes);
|
||||
return EncodingUtils.concatenate(iv, ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] encryptedBytes) {
|
||||
int ivLength = this.ivGenerator.getKeyLength();
|
||||
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, ivLength);
|
||||
byte[] ciphertext = EncodingUtils.subArray(encryptedBytes, ivLength, encryptedBytes.length);
|
||||
synchronized (this.decryptor) {
|
||||
CipherUtils.initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey, new IvParameterSpec(iv));
|
||||
return CipherUtils.doFinal(this.decryptor, ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey deriveKey(String password, CharSequence salt) {
|
||||
return CipherUtils.newSecretKey("PBKDF2WithHmacSHA256",
|
||||
new PBEKeySpec(password.toCharArray(), Hex.decode(salt), DEFAULT_PBKDF2_ITERATIONS, 256));
|
||||
}
|
||||
|
||||
/**
|
||||
* A Builder for {@link AesCbcBytesEncryptor}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
private BytesKeyGenerator ivGenerator = KeyGenerators.secureRandom(IV_LENGTH_BYTES);
|
||||
|
||||
private Builder(SecretKey secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
private Builder(String password, CharSequence salt) {
|
||||
this.secretKey = deriveKey(password, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link BytesKeyGenerator} to use for generating the initialization
|
||||
* vector.
|
||||
* @param ivGenerator the {@link BytesKeyGenerator} to use for generating the
|
||||
* initialization vector
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder ivGenerator(BytesKeyGenerator ivGenerator) {
|
||||
Objects.requireNonNull(ivGenerator, "ivGenerator cannot be null");
|
||||
this.ivGenerator = ivGenerator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link AesCbcBytesEncryptor}.
|
||||
* @return the {@link AesCbcBytesEncryptor}
|
||||
*/
|
||||
public AesCbcBytesEncryptor build() {
|
||||
return new AesCbcBytesEncryptor(this.secretKey, this.ivGenerator);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.crypto.encrypt;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
import org.springframework.security.crypto.util.EncodingUtils;
|
||||
|
||||
/**
|
||||
* {@link BytesEncryptor} that uses 256-bit AES/GCM/NoPadding with a random 16-byte
|
||||
* initialization vector and a 128-bit authentication tag. The IV is prepended to the
|
||||
* ciphertext on encrypt and stripped on decrypt. GCM provides authenticated encryption
|
||||
* (AEAD): both confidentiality and integrity are protected, and decryption throws if the
|
||||
* ciphertext has been tampered with.
|
||||
*
|
||||
* <p>
|
||||
* This class uses a 16-byte (128-bit) IV rather than the 12-byte (96-bit) IV recommended
|
||||
* by NIST SP 800-38D for GCM. Both lengths are cryptographically valid; the 16-byte
|
||||
* choice maintains consistency with the rest of the Spring Security crypto module. For
|
||||
* additional guidance, please see the <a href=
|
||||
* "https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html">
|
||||
* OWASP Cryptographic Storage Cheat Sheet</a>.
|
||||
*
|
||||
* <p>
|
||||
* When key derivation is used via {@link #withPassword(String, CharSequence)}, the key is
|
||||
* derived using PBKDF2WithHmacSHA256 with {@code DEFAULT_PBKDF2_ITERATIONS} iterations
|
||||
* per the <a href=
|
||||
* "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">
|
||||
* OWASP Password Storage Cheat Sheet</a>. Because derivation is intentionally expensive,
|
||||
* the encryptor instance should be created once and reused rather than constructed
|
||||
* per-operation.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.7.26
|
||||
* @see AesCbcBytesEncryptor
|
||||
* @see AesBytesEncryptor
|
||||
*/
|
||||
public final class AesGcmBytesEncryptor implements BytesEncryptor {
|
||||
|
||||
private static final String ALGORITHM = "AES/GCM/NoPadding";
|
||||
|
||||
private static final int IV_LENGTH_BYTES = 16;
|
||||
|
||||
private static final int TAG_LENGTH_BITS = 128;
|
||||
|
||||
private static final int DEFAULT_PBKDF2_ITERATIONS = 600_000;
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
private final Cipher encryptor;
|
||||
|
||||
private final Cipher decryptor;
|
||||
|
||||
private final BytesKeyGenerator ivGenerator;
|
||||
|
||||
private AesGcmBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator) {
|
||||
this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
|
||||
this.encryptor = CipherUtils.newCipher(ALGORITHM);
|
||||
this.decryptor = CipherUtils.newCipher(ALGORITHM);
|
||||
this.ivGenerator = ivGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encryptor that derives its key from the given password and hex-encoded
|
||||
* salt using PBKDF2WithHmacSHA1.
|
||||
* @param password the password value
|
||||
* @param salt the hex-encoded salt value
|
||||
*/
|
||||
public static Builder withPassword(String password, CharSequence salt) {
|
||||
return new Builder(password, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encryptor using the supplied {@link SecretKey}.
|
||||
* @param secretKey the secret (symmetric) key
|
||||
*/
|
||||
public static Builder withSecretKey(SecretKey secretKey) {
|
||||
return new Builder(secretKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] bytes) {
|
||||
synchronized (this.encryptor) {
|
||||
byte[] iv = this.ivGenerator.generateKey();
|
||||
CipherUtils.initCipher(this.encryptor, Cipher.ENCRYPT_MODE, this.secretKey,
|
||||
new GCMParameterSpec(TAG_LENGTH_BITS, iv));
|
||||
byte[] ciphertext = CipherUtils.doFinal(this.encryptor, bytes);
|
||||
return EncodingUtils.concatenate(iv, ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] encryptedBytes) {
|
||||
int ivLength = this.ivGenerator.getKeyLength();
|
||||
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, ivLength);
|
||||
byte[] ciphertext = EncodingUtils.subArray(encryptedBytes, ivLength, encryptedBytes.length);
|
||||
synchronized (this.decryptor) {
|
||||
CipherUtils.initCipher(this.decryptor, Cipher.DECRYPT_MODE, this.secretKey,
|
||||
new GCMParameterSpec(TAG_LENGTH_BITS, iv));
|
||||
return CipherUtils.doFinal(this.decryptor, ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey deriveKey(String password, CharSequence salt) {
|
||||
return CipherUtils.newSecretKey("PBKDF2WithHmacSHA256",
|
||||
new PBEKeySpec(password.toCharArray(), Hex.decode(salt), DEFAULT_PBKDF2_ITERATIONS, 256));
|
||||
}
|
||||
|
||||
/**
|
||||
* A Builder for {@link AesGcmBytesEncryptor}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
private BytesKeyGenerator ivGenerator = KeyGenerators.secureRandom(IV_LENGTH_BYTES);
|
||||
|
||||
private Builder(SecretKey secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
private Builder(String password, CharSequence salt) {
|
||||
this.secretKey = deriveKey(password, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link BytesKeyGenerator} to use for generating the initialization
|
||||
* vector.
|
||||
* @param ivGenerator the {@link BytesKeyGenerator} to use for generating the
|
||||
* initialization vector
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder ivGenerator(BytesKeyGenerator ivGenerator) {
|
||||
Objects.requireNonNull(ivGenerator, "ivGenerator cannot be null");
|
||||
this.ivGenerator = ivGenerator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link AesGcmBytesEncryptor}.
|
||||
* @return the {@link AesGcmBytesEncryptor}
|
||||
*/
|
||||
public AesGcmBytesEncryptor build() {
|
||||
return new AesGcmBytesEncryptor(this.secretKey, this.ivGenerator);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.security.crypto.encrypt;
|
||||
|
||||
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
|
||||
/**
|
||||
@@ -42,9 +41,14 @@ public final class Encryptors {
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
* @deprecated Use {@link AesGcmBytesEncryptor#withPassword(String, CharSequence)}
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16), CipherAlgorithm.GCM);
|
||||
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16),
|
||||
AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,18 +57,16 @@ public final class Encryptors {
|
||||
* Function #2). Salts the password to prevent dictionary attacks against the key. The
|
||||
* provided salt is expected to be hex-encoded; it should be random and at least 8
|
||||
* bytes in length. Also applies a random 16-byte initialization vector to ensure each
|
||||
* encrypted message will be unique. Requires Java 6. NOTE: This mode is not
|
||||
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
|
||||
* and does not provide any guarantees about the authenticity of the data. For a more
|
||||
* secure alternative, users should prefer
|
||||
* {@link #stronger(CharSequence, CharSequence)}.
|
||||
* encrypted message will be unique. Requires Java 6.
|
||||
* @param password the password used to generate the encryptor's secret key; should
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see Encryptors#stronger(CharSequence, CharSequence)
|
||||
* @deprecated Use {@link AesCbcBytesEncryptor#withPassword(String, CharSequence)}
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt, KeyGenerators.secureRandom(16));
|
||||
}
|
||||
@@ -74,8 +76,12 @@ public final class Encryptors {
|
||||
* text is hex-encoded.
|
||||
* @param password the password used to generate the encryptor's secret key; should
|
||||
* not be shared
|
||||
* @deprecated Use {@link AesGcmBytesEncryptor#withPassword(String, CharSequence)}
|
||||
* instead.
|
||||
* @see Encryptors#stronger(CharSequence, CharSequence)
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public static TextEncryptor delux(CharSequence password, CharSequence salt) {
|
||||
return new HexEncodingTextEncryptor(stronger(password, salt));
|
||||
}
|
||||
@@ -85,8 +91,12 @@ public final class Encryptors {
|
||||
* text is hex-encoded.
|
||||
* @param password the password used to generate the encryptor's secret key; should
|
||||
* not be shared
|
||||
* @deprecated Use {@link AesCbcBytesEncryptor#withPassword(String, CharSequence)}
|
||||
* instead.
|
||||
* @see Encryptors#standard(CharSequence, CharSequence)
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public static TextEncryptor text(CharSequence password, CharSequence salt) {
|
||||
return new HexEncodingTextEncryptor(standard(password, salt));
|
||||
}
|
||||
|
||||
+1
-11
@@ -16,8 +16,6 @@
|
||||
|
||||
package org.springframework.security.crypto.password;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
@@ -39,15 +37,7 @@ final class PasswordEncoderUtils {
|
||||
* @return
|
||||
*/
|
||||
static boolean equals(String expected, @Nullable String actual) {
|
||||
byte[] expectedBytes = bytesUtf8(expected);
|
||||
byte[] actualBytes = bytesUtf8(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
private static byte @Nullable [] bytesUtf8(@Nullable String s) {
|
||||
// need to check if Utf8.encode() runs in constant time (probably not).
|
||||
// This may leak length of string.
|
||||
return (s != null) ? Utf8.encode(s) : null;
|
||||
return Utf8.isEqual(expected, actual);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ import com.password4j.BalloonHashingFunction;
|
||||
import com.password4j.Hash;
|
||||
import com.password4j.Password;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -140,7 +141,7 @@ public class BalloonHashingPassword4jPasswordEncoder extends AbstractValidatingP
|
||||
String expectedHash = parts[1];
|
||||
|
||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction);
|
||||
return expectedHash.equals(hash.getResult());
|
||||
return Utf8.isEqual(expectedHash, hash.getResult());
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Invalid Base64 encoding
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ import com.password4j.Hash;
|
||||
import com.password4j.PBKDF2Function;
|
||||
import com.password4j.Password;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -138,7 +139,7 @@ public class Pbkdf2Password4jPasswordEncoder extends AbstractValidatingPasswordE
|
||||
String expectedHash = parts[1];
|
||||
|
||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function);
|
||||
return expectedHash.equals(hash.getResult());
|
||||
return Utf8.isEqual(expectedHash, hash.getResult());
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Invalid Base64 encoding
|
||||
|
||||
@@ -37,4 +37,48 @@ public class Utf8Tests {
|
||||
assertThat(decoded).isEqualTo("6048b75ed560785c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenDifferentLengthThenFalse() {
|
||||
assertThat(Utf8.isEqual("abc", "a")).isFalse();
|
||||
assertThat(Utf8.isEqual("a", "abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNullAndNotEmptyThenFalse() {
|
||||
assertThat(Utf8.isEqual(null, "a")).isFalse();
|
||||
assertThat(Utf8.isEqual("a", null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNullAndNullThenTrue() {
|
||||
assertThat(Utf8.isEqual(null, null)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNullAndEmptyThenFalse() {
|
||||
assertThat(Utf8.isEqual(null, "")).isFalse();
|
||||
assertThat(Utf8.isEqual("", null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenNotEmptyAndEmptyThenFalse() {
|
||||
assertThat(Utf8.isEqual("abc", "")).isFalse();
|
||||
assertThat(Utf8.isEqual("", "abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenEmptyAndEmptyThenTrue() {
|
||||
assertThat(Utf8.isEqual("", "")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenDifferentCaseThenFalse() {
|
||||
assertThat(Utf8.isEqual("aBc", "abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEqualWhenSameThenTrue() {
|
||||
assertThat(Utf8.isEqual("abcdef", "abcdef")).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.crypto.encrypt;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm;
|
||||
import org.springframework.security.crypto.util.EncodingUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AesCbcBytesEncryptor}.
|
||||
*/
|
||||
class AesCbcBytesEncryptorTests {
|
||||
|
||||
private final String secret = "value";
|
||||
|
||||
private final String password = "password";
|
||||
|
||||
private final String hexSalt = "deadbeef";
|
||||
|
||||
@Test
|
||||
void roundtripWhenUsingPasswordAndSaltThenEncryptsAndDecrypts() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripWhenUsingSecretKeyThenEncryptsAndDecrypts() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||
SecretKey secretKey = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(secretKey).build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encryptWhenUsingMockIvThenProducesKnownCiphertext() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
BytesKeyGenerator mockGenerator = mock(BytesKeyGenerator.class);
|
||||
given(mockGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
|
||||
given(mockGenerator.getKeyLength()).willReturn(16);
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||
.ivGenerator(mockGenerator)
|
||||
.build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(Hex.encode(encrypted)).isEqualTo("4b0febebd439db7ca77153cb254520c3b7232ac29355d07869433f1ecf55fe94");
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encryptProducesUniqueOutputAndIvIsPrePended() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
byte[] first = encryptor.encrypt(this.secret.getBytes());
|
||||
byte[] second = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(first).isNotEqualTo(second);
|
||||
assertThat(first.length).isGreaterThan(16);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void migratesFromDeprecatedNullIvCbcToAesCbcBytesEncryptor() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt);
|
||||
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||
|
||||
AesCbcBytesEncryptor modern = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
BytesEncryptor migrating = new MigratingBytesEncryptor("{CBC}", modern, deprecated);
|
||||
|
||||
assertThat(new String(migrating.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
|
||||
byte[] migrated = migrating.encrypt(this.secret.getBytes());
|
||||
assertThat(migrated[0]).isEqualTo((byte) '{');
|
||||
assertThat(migrated[1]).isEqualTo((byte) 'C');
|
||||
assertThat(migrated[2]).isEqualTo((byte) 'B');
|
||||
assertThat(migrated[3]).isEqualTo((byte) 'C');
|
||||
assertThat(migrated[4]).isEqualTo((byte) '}');
|
||||
assertThat(new String(migrating.decrypt(migrated))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void withSecretWhenAesBytesEncryptorEncryptsThenDecrypts() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
|
||||
AesBytesEncryptor.CipherAlgorithm.CBC);
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(key).build();
|
||||
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void aesBytesEncryptorWhenEncryptsThenAesCbcBytesEncryptorDecrypts() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withSecretKey(key).build();
|
||||
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
|
||||
AesBytesEncryptor.CipherAlgorithm.CBC);
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(deprecated.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripWhenUsingCustomIvGeneratorThenEncryptsAndDecrypts() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
BytesKeyGenerator customIvGenerator = mock(BytesKeyGenerator.class);
|
||||
given(customIvGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
|
||||
given(customIvGenerator.getKeyLength()).willReturn(16);
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||
.ivGenerator(customIvGenerator)
|
||||
.build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void withPasswordDerivesADifferentKeyThanAesBytesEncryptor() {
|
||||
CryptoAssumptions.assumeCBCJCE();
|
||||
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt,
|
||||
KeyGenerators.secureRandom(16));
|
||||
AesCbcBytesEncryptor encryptor = AesCbcBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
|
||||
}
|
||||
|
||||
private static final class MigratingBytesEncryptor implements BytesEncryptor {
|
||||
|
||||
private final byte[] prefix;
|
||||
|
||||
private final BytesEncryptor write;
|
||||
|
||||
private final BytesEncryptor deprecated;
|
||||
|
||||
MigratingBytesEncryptor(String prefix, BytesEncryptor write, BytesEncryptor deprecated) {
|
||||
this.prefix = prefix.getBytes(StandardCharsets.US_ASCII);
|
||||
this.write = write;
|
||||
this.deprecated = deprecated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] bytes) {
|
||||
return EncodingUtils.concatenate(this.prefix, this.write.encrypt(bytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] encryptedBytes) {
|
||||
if (startsWith(encryptedBytes, this.prefix)) {
|
||||
byte[] bytes = EncodingUtils.subArray(encryptedBytes, this.prefix.length, encryptedBytes.length);
|
||||
return this.write.decrypt(bytes);
|
||||
}
|
||||
return this.deprecated.decrypt(encryptedBytes);
|
||||
}
|
||||
|
||||
private static boolean startsWith(byte[] data, byte[] prefix) {
|
||||
if (data.length < prefix.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < prefix.length; i++) {
|
||||
if (data[i] != prefix[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.crypto.encrypt;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AesGcmBytesEncryptor}.
|
||||
*/
|
||||
class AesGcmBytesEncryptorTests {
|
||||
|
||||
private final String secret = "value";
|
||||
|
||||
private final String password = "password";
|
||||
|
||||
private final String hexSalt = "deadbeef";
|
||||
|
||||
@Test
|
||||
void roundtripWhenUsingPasswordAndSaltThenEncryptsAndDecrypts() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripWhenUsingSecretKeyThenEncryptsAndDecrypts() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||
SecretKey secretKey = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(secretKey).build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encryptWhenUsingMockIvThenProducesKnownCiphertext() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
BytesKeyGenerator mockGenerator = mock(BytesKeyGenerator.class);
|
||||
given(mockGenerator.generateKey()).willReturn(Hex.decode("4b0febebd439db7ca77153cb254520c3"));
|
||||
given(mockGenerator.getKeyLength()).willReturn(16);
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||
.ivGenerator(mockGenerator)
|
||||
.build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(Hex.encode(encrypted))
|
||||
.isEqualTo("4b0febebd439db7ca77153cb254520c3e4d61ae38207b4e42b820d311dc3d4e0e2f37ed5ee");
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encryptProducesUniqueOutputAndIvIsPrepended() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
byte[] first = encryptor.encrypt(this.secret.getBytes());
|
||||
byte[] second = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(first).isNotEqualTo(second);
|
||||
assertThat(first.length).isGreaterThan(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void withSecretWhenAesBytesEncryptorEncryptsThenDecrypts() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(16),
|
||||
AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(key).build();
|
||||
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void aesBytesEncryptorWhenEncryptsThenAesGcmBytesEncryptorDecrypts() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), 1024, 256);
|
||||
SecretKey key = CipherUtils.newSecretKey(SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(), keySpec);
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withSecretKey(key)
|
||||
.ivGenerator(KeyGenerators.secureRandom(12))
|
||||
.build();
|
||||
AesBytesEncryptor deprecated = new AesBytesEncryptor(key, KeyGenerators.secureRandom(12),
|
||||
AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(deprecated.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundtripWhenUsingCustomIvGeneratorLengthThenEncryptsAndDecrypts() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt)
|
||||
.ivGenerator(KeyGenerators.secureRandom(12))
|
||||
.build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
assertThat(new String(encryptor.decrypt(encrypted))).isEqualTo(this.secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void withPasswordDerivesADifferentKeyThanAesBytesEncryptor() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
AesBytesEncryptor deprecated = new AesBytesEncryptor(this.password, this.hexSalt,
|
||||
KeyGenerators.secureRandom(16), AesBytesEncryptor.CipherAlgorithm.GCM);
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
byte[] encrypted = deprecated.encrypt(this.secret.getBytes());
|
||||
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
|
||||
}
|
||||
|
||||
@Test
|
||||
void decryptDetectsAuthenticationTagTampering() {
|
||||
CryptoAssumptions.assumeGCMJCE();
|
||||
AesGcmBytesEncryptor encryptor = AesGcmBytesEncryptor.withPassword(this.password, this.hexSalt).build();
|
||||
byte[] encrypted = encryptor.encrypt(this.secret.getBytes());
|
||||
encrypted[17] ^= 0xFF;
|
||||
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encrypted));
|
||||
}
|
||||
|
||||
}
|
||||
+4
-6
@@ -24,25 +24,23 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.opentest4j.TestAbortedException;
|
||||
|
||||
import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm;
|
||||
|
||||
public final class CryptoAssumptions {
|
||||
|
||||
private CryptoAssumptions() {
|
||||
}
|
||||
|
||||
public static void assumeGCMJCE() {
|
||||
assumeAes256(CipherAlgorithm.GCM);
|
||||
assumeAes256("GCM");
|
||||
}
|
||||
|
||||
public static void assumeCBCJCE() {
|
||||
assumeAes256(CipherAlgorithm.CBC);
|
||||
assumeAes256("CBC");
|
||||
}
|
||||
|
||||
private static void assumeAes256(CipherAlgorithm cipherAlgorithm) {
|
||||
private static void assumeAes256(String cipherAlgorithm) {
|
||||
boolean aes256Available = false;
|
||||
try {
|
||||
Cipher.getInstance(cipherAlgorithm.toString());
|
||||
Cipher.getInstance(cipherAlgorithm);
|
||||
aes256Available = Cipher.getMaxAllowedKeyLength("AES") >= 256;
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
|
||||
+8
-1
@@ -11,13 +11,16 @@ dependencies {
|
||||
def springFrameworkVersion = project.property("springFrameworkVersion")
|
||||
def reactorVersion = project.property("reactorVersion")
|
||||
def springDataVersion = project.property("springDataVersion")
|
||||
def micrometerVersion = project.property("micrometerVersion")
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:$reactorVersion")
|
||||
api platform("org.springframework.data:spring-data-bom:$springDataVersion")
|
||||
api platform("io.micrometer:micrometer-bom:$micrometerVersion")
|
||||
} else {
|
||||
api platform(libs.org.springframework.spring.framework.bom)
|
||||
api platform(libs.io.projectreactor.reactor.bom)
|
||||
api platform(libs.org.springframework.data.spring.data.bom)
|
||||
api platform(libs.io.micrometer.micrometer.bom)
|
||||
}
|
||||
api platform(libs.io.rsocket.rsocket.bom)
|
||||
api platform(libs.org.junit.junit.bom)
|
||||
@@ -72,7 +75,11 @@ dependencies {
|
||||
api libs.org.skyscreamer.jsonassert
|
||||
api libs.org.slf4j.log4j.over.slf4j
|
||||
api libs.org.slf4j.slf4j.api
|
||||
api libs.org.springframework.ldap.spring.ldap.core
|
||||
if (project.hasProperty("isOverrideVersionCatalog") && project.hasProperty("springLdapVersion")) {
|
||||
api "org.springframework.ldap:spring-ldap-core:${springLdapVersion}"
|
||||
} else {
|
||||
api libs.org.springframework.ldap.spring.ldap.core
|
||||
}
|
||||
api libs.org.synchronoss.cloud.nio.multipart.parser
|
||||
api libs.org.apache.maven.resolver.maven.resolver.connector.basic
|
||||
api libs.org.apache.maven.resolver.maven.resolver.impl
|
||||
|
||||
@@ -23,10 +23,28 @@ When using a proxy server, it is important to ensure that you have configured yo
|
||||
For example, many applications have a load balancer that responds to request for `\https://example.com/` by forwarding the request to an application server at `\https://192.168.0.107`
|
||||
Without proper configuration, the application server can not know that the load balancer exists and treats the request as though `\https://192.168.0.107:8080` was requested by the client.
|
||||
|
||||
To fix this, you can use https://tools.ietf.org/html/rfc7239[RFC 7239] to specify that a load balancer is being used.
|
||||
To make the application aware of this, you need to configure your application server to be aware of the X-Forwarded headers.
|
||||
To fix this, the proxy needs to pass on the details of the original request, and the application needs to be configured to use them.
|
||||
Two kinds of headers are used for this, and it is important to know which of them applies in your deployment:
|
||||
|
||||
* The standard `Forwarded` header, defined by https://tools.ietf.org/html/rfc7239[RFC 7239], which carries the original host, protocol, and client in a single header.
|
||||
* The non-standard `X-Forwarded-*` headers, such as `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-For`, which predate RFC 7239.
|
||||
|
||||
Most proxies still send the `X-Forwarded-*` headers rather than the standard `Forwarded` header, while Spring Framework and servers such as Reactor Netty and Jetty understand both.
|
||||
Do not assume that only one of them is in use.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Both kinds of headers are supplied by the client unless a proxy overwrites them, so an application that trusts them without a trusted proxy in front of it can be made to believe a request arrived over a different host, protocol, or client address than it really did.
|
||||
====
|
||||
|
||||
For this reason, the proxy at the edge of your network must be configured to remove or overwrite any forwarded headers that arrive from the outside, for _both_ kinds of headers.
|
||||
Dropping only the `Forwarded` header while passing through `X-Forwarded-*` (or the reverse) leaves the application open to the same spoofing through the other set.
|
||||
Only headers added by a proxy you control should reach the application.
|
||||
|
||||
Once untrusted values are handled at the edge, the application server can be configured to apply the headers.
|
||||
For example, Tomcat uses https://tomcat.apache.org/tomcat-10.1-doc/api/org/apache/catalina/valves/RemoteIpValve.html[`RemoteIpValve`] and Jetty uses https://eclipse.dev/jetty/javadoc/jetty-11/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[`ForwardedRequestCustomizer`].
|
||||
Alternatively, Spring users can use https://docs.spring.io/spring-framework/reference/web/webmvc/filters.html#filters-forwarded-headers[`ForwardedHeaderFilter`] with the Servlet stack or https://docs.spring.io/spring-framework/reference/web/webflux/reactive-spring.html#webflux-forwarded-headers[`ForwardedHeaderTransformer`] with the Reactive stack.
|
||||
Both handle the `Forwarded` header and the `X-Forwarded-*` headers, and both can be configured to remove the headers instead of applying them, which is useful when the application is not behind a proxy.
|
||||
|
||||
Spring Boot users can use the `server.forward-headers-strategy` property to configure the application.
|
||||
See the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.webserver.use-behind-a-proxy-server[Spring Boot documentation] for further details.
|
||||
|
||||
@@ -8,8 +8,8 @@ The code is distributed as part of the core module but has no dependencies on an
|
||||
|
||||
[[spring-security-crypto-encryption]]
|
||||
== Encryptors
|
||||
The javadoc:org.springframework.security.crypto.encrypt.Encryptors[] class provides factory methods for constructing symmetric encryptors.
|
||||
This class lets you create javadoc:org.springframework.security.crypto.encrypt.BytesEncryptor[] instances to encrypt data in raw `byte[]` form.
|
||||
Spring Security provides javadoc:org.springframework.security.crypto.encrypt.AesGcmBytesEncryptor[] and javadoc:org.springframework.security.crypto.encrypt.AesCbcBytesEncryptor[] for constructing symmetric encryptors.
|
||||
These can be used to encrypt data in raw `byte[]` form.
|
||||
You can also construct javadoc:org.springframework.security.crypto.encrypt.TextEncryptor[] instances to encrypt text strings.
|
||||
Encryptors are thread-safe.
|
||||
|
||||
@@ -20,7 +20,7 @@ Both `BytesEncryptor` and `TextEncryptor` are interfaces. `BytesEncryptor` has m
|
||||
|
||||
[[spring-security-crypto-encryption-bytes]]
|
||||
=== BytesEncryptor
|
||||
You can use the `Encryptors.stronger` factory method to construct a `BytesEncryptor`:
|
||||
Use `AesGcmBytesEncryptor` to construct a BytesEncryptor with authenticated encryption:
|
||||
|
||||
.BytesEncryptor
|
||||
[tabs]
|
||||
@@ -29,24 +29,22 @@ Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Encryptors.stronger("password", "salt");
|
||||
AesGcmBytesEncryptor.withPassword("password", "salt").build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
Encryptors.stronger("password", "salt")
|
||||
AesGcmBytesEncryptor.withPassword("password", "salt").build()
|
||||
----
|
||||
======
|
||||
|
||||
The `stronger` encryption method creates an encryptor by using 256-bit AES encryption with
|
||||
Galois Counter Mode (GCM).
|
||||
It derives the secret key by using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
||||
This method requires Java 6.
|
||||
The password used to generate the `SecretKey` should be kept in a secure place and should not be shared.
|
||||
The salt is used to prevent dictionary attacks against the key in the event that your encrypted data is compromised.
|
||||
A 16-byte random initialization vector is also applied so that each encrypted message is unique.
|
||||
`AesGcmBytesEncryptor` uses 256-bit AES encryption with Galois Counter Mode (GCM), providing https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated encryption] (AEAD).
|
||||
It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
||||
The password used to generate the SecretKey should be kept in a secure place and not be shared.
|
||||
The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
|
||||
A 16-byte random initialization vector is also applied so each encrypted message is unique.
|
||||
|
||||
The provided salt should be in hex-encoded String form, be random, and be at least 8 bytes in length.
|
||||
You can generate such a salt by using a `KeyGenerator`:
|
||||
@@ -69,14 +67,33 @@ val salt = KeyGenerators.string().generateKey() // generates a random 8-byte sal
|
||||
----
|
||||
======
|
||||
|
||||
You can also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
|
||||
Users who require CBC mode may use `AesCbcBytesEncryptor`:
|
||||
|
||||
.AesCbcBytesEncryptor
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
AesCbcBytesEncryptor.withPassword("password", "salt").build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
AesCbcBytesEncryptor.withPassword("password", "salt").build()
|
||||
----
|
||||
======
|
||||
|
||||
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
|
||||
guarantees about the authenticity of the data.
|
||||
For a more secure alternative, use `Encryptors.stronger`.
|
||||
For a more secure alternative, users should prefer `AesGcmBytesEncryptor`.
|
||||
|
||||
[[spring-security-crypto-encryption-text]]
|
||||
=== TextEncryptor
|
||||
You can use the `Encryptors.text` factory method to construct a standard TextEncryptor:
|
||||
Use `AesCbcBytesEncryptor` to encrypt text data:
|
||||
|
||||
.TextEncryptor
|
||||
[tabs]
|
||||
@@ -85,19 +102,22 @@ Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Encryptors.text("password", "salt");
|
||||
AesCbcBytesEncryptor.withPassword("password", "salt").build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
Encryptors.text("password", "salt")
|
||||
AesCbcBytesEncryptor.withPassword("password", "salt").build()
|
||||
----
|
||||
======
|
||||
|
||||
A `TextEncryptor` uses a standard `BytesEncryptor` to encrypt text data.
|
||||
Encrypted results are returned as hex-encoded strings for easy storage on the filesystem or in a database.
|
||||
`AesCbcBytesEncryptor` encrypts data as raw bytes.
|
||||
You can hex-encode the result for easy storage on the filesystem or in the database.
|
||||
|
||||
NOTE: Queryable text encryption (encrypting such that the same plaintext always produces the same ciphertext) is no longer recommended, as it relies on a fixed initialization vector and does not provide adequate security.
|
||||
Instead, look to your data store for a mechanism to query encrypted data.
|
||||
|
||||
[[spring-security-crypto-keygenerators]]
|
||||
== Key Generators
|
||||
|
||||
@@ -132,4 +132,11 @@ authenticationConverter.setBearerTokenResolver(myBearerTokenResolver)
|
||||
authenticationConverter.setAuthenticationDetailsSource(myAuthenticationDetailsSource)
|
||||
val filter = BearerTokenAuthenticationFilter(authenticationManager, authenticationConverter)
|
||||
----
|
||||
|
||||
== `NimbusJwtDecoder`'s Default Connect and Read Timeouts Are Now 30 Seconds
|
||||
|
||||
`NimbusJwtDecoder`'s default `RestOperations`, used to fetch a JWK Set when no `RestOperations` is otherwise configured, previously used a 500 millisecond connect and read timeout.
|
||||
This value was too short for many deployments and is now 30 seconds, aligning with the connect and read timeouts already used elsewhere in the OAuth2 Client and Resource Server support, such as xref:servlet/oauth2/client/index.adoc[`ClientRegistrations`].
|
||||
|
||||
If your application relies on the previous, shorter timeout -- for example, expecting a fast failure when the authorization server is unreachable -- you can restore it either by setting the JDK's `sun.net.client.defaultConnectTimeout` and `sun.net.client.defaultReadTimeout` system properties (in milliseconds), or by providing your own `RestOperations`, as described in xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-timeouts[Configuring Timeouts].
|
||||
======
|
||||
|
||||
@@ -695,7 +695,7 @@ class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
|
||||
|
||||
You would then add a bean of this type to your application context and inject it into the `LdapAuthenticationProvider`. This is covered in the section on configuring LDAP by using explicit Spring beans in the LDAP chapter of the reference manual.
|
||||
Note that you cannot use the namespace for configuration in this case.
|
||||
You should also consult the {security-api-url}[Javadoc] for the relevant classes and interfaces.
|
||||
You should also consult the xref:attachment$api/java/index.html[Javadoc] for the relevant classes and interfaces.
|
||||
|
||||
|
||||
[[appendix-faq-namespace-post-processor]]
|
||||
|
||||
@@ -81,6 +81,12 @@ Xml::
|
||||
|
||||
and no authorization changes are necessary since it simply adjusts the `LogoutFilter`.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The URI passed to `logoutUrl` (and to `logoutSuccessUrl`) is matched and redirected to literally.
|
||||
If you want either URI to live under your application servlet's base path, include that prefix explicitly — for example, `logoutUrl("/api/logout")`.
|
||||
====
|
||||
|
||||
[[permit-logout-endpoints]]
|
||||
However, if you stand up your own logout success endpoint (or in a rare case, <<creating-custom-logout-endpoint, your own logout endpoint>>), say using {spring-framework-reference-url}web.html#spring-web[Spring MVC], you will need to permit it in Spring Security.
|
||||
This is because Spring MVC processes your request after Spring Security does.
|
||||
|
||||
@@ -42,7 +42,7 @@ In the following sections we will explore how to configure OTT Login for your ne
|
||||
[[default-pages]]
|
||||
== Default Login Page and Default One-Time Token Submit Page
|
||||
|
||||
When the `oneTimeTokenLogin()` DSL is used, by default the One-Time Token Login Page is auto-generated by the org.springframework.security.web.authentication.ui:DefaultLoginPageGeneratingFilter[].
|
||||
When the `oneTimeTokenLogin()` DSL is used, by default the One-Time Token Login Page is auto-generated by the javadoc:org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter[].
|
||||
The DSL will also set up the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] to generate a default One-Time Token submit page.
|
||||
|
||||
[[sending-token-to-user]]
|
||||
@@ -89,7 +89,7 @@ public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTok
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(request.getRequestURL().toString())
|
||||
.replacePath(request.getContextPath())
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
@@ -147,7 +147,7 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler(
|
||||
) : OneTimeTokenGenerationSuccessHandler {
|
||||
|
||||
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
|
||||
val builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
val builder = UriComponentsBuilder.fromUriString(request.getRequestURL().toString())
|
||||
.replacePath(request.contextPath)
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
@@ -211,7 +211,7 @@ public class SecurityConfig {
|
||||
// ...
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin((ott) -> ott
|
||||
.generateTokenUrl("/ott/my-generate-url")
|
||||
.tokenGeneratingUrl("/ott/my-generate-url")
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
@@ -238,7 +238,7 @@ class SecurityConfig {
|
||||
//...
|
||||
formLogin { }
|
||||
oneTimeTokenLogin {
|
||||
generateTokenUrl = "/ott/my-generate-url"
|
||||
tokenGeneratingUrl = "/ott/my-generate-url"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
@@ -252,6 +252,13 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSucc
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The URI passed to `generateTokenUrl` is matched literally.
|
||||
If you want it to live under your application servlet's base path, include that prefix explicitly — for example, `generateTokenUrl("/api/ott/generate")`.
|
||||
The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page.
|
||||
====
|
||||
|
||||
[[changing-submit-page-url]]
|
||||
== Changing the Default Submit Page URL
|
||||
|
||||
@@ -275,7 +282,7 @@ public class SecurityConfig {
|
||||
// ...
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin((ott) -> ott
|
||||
.submitPageUrl("/ott/submit")
|
||||
.defaultSubmitPageUrl("/ott/submit")
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
@@ -302,7 +309,7 @@ class SecurityConfig {
|
||||
//...
|
||||
formLogin { }
|
||||
oneTimeTokenLogin {
|
||||
submitPageUrl = "/ott/submit"
|
||||
defaultSubmitPageUrl = "/ott/submit"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
|
||||
@@ -155,6 +155,15 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The URI passed to `loginPage` (and to `loginProcessingUrl`, if set separately) is matched and redirected to literally.
|
||||
If your custom login page is served by an application servlet mapped under a base path, include that prefix in the URI you pass to the DSL.
|
||||
For example, if your dispatcher is at `/api/*`:
|
||||
|
||||
include-code::./FormLoginServletPathConfiguration[tag=loginPage,indent=0]
|
||||
====
|
||||
|
||||
[[servlet-authentication-form-custom-html]]
|
||||
When the login page is specified in the Spring Security configuration, you are responsible for rendering the page.
|
||||
// FIXME: default login page rendered by Spring Security
|
||||
|
||||
@@ -642,6 +642,8 @@ This is because Spring Security requires all URIs to be absolute (minus the cont
|
||||
There are several other components that create request matchers for you like {spring-boot-api-url}org/springframework/boot/security/autoconfigure/web/servlet/PathRequest.html[`PathRequest#toStaticResources#atCommonLocations`]
|
||||
=====
|
||||
|
||||
If most of your authorization rules sit under the same servlet path, you can xref:servlet/integrations/mvc.adoc#mvc-requestmatcher[publish a `PathPatternRequestMatcher.Builder` bean] with that base path; Spring Security then applies it to the string overloads of `authorizeHttpRequests((authorize) -> authorize.requestMatchers(...))`, `HttpSecurity#securityMatcher(...)` / `#securityMatchers(...)`, and `WebSecurityCustomizer#ignoring().requestMatchers(...)`.
|
||||
|
||||
[[match-by-custom]]
|
||||
=== Using a Custom Matcher
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ This means that we are running our `Runnable` with the same user that was used t
|
||||
|
||||
== Spring Security Concurrency Classes
|
||||
|
||||
See the {security-api-url}index.html[Javadoc] for additional integrations with both the Java concurrent APIs and the Spring Task abstractions.
|
||||
See the xref:attachment$api/java/index.html[Javadoc] for additional integrations with both the Java concurrent APIs and the Spring Task abstractions.
|
||||
They are self-explanatory once you understand the previous code.
|
||||
|
||||
* javadoc:org.springframework.security.concurrent.DelegatingSecurityContextCallable[]
|
||||
|
||||
+3
-3
@@ -144,13 +144,13 @@ An `OAuth2Authorization` is a representation of an OAuth2 authorization, which h
|
||||
[TIP]
|
||||
The corresponding authorization model in Spring Security's OAuth2 Client support is xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-client[OAuth2AuthorizedClient].
|
||||
|
||||
After the successful completion of an authorization grant flow, an `OAuth2Authorization` is created and associates an {security-api-url}/org/springframework/security/oauth2/core/OAuth2AccessToken.html[`OAuth2AccessToken`], an (optional) {security-api-url}/org/springframework/security/oauth2/core/OAuth2RefreshToken.html[`OAuth2RefreshToken`], and additional state specific to the executed authorization grant type.
|
||||
After the successful completion of an authorization grant flow, an `OAuth2Authorization` is created and associates an javadoc:org.springframework.security.oauth2.core.OAuth2AccessToken[], an (optional) javadoc:org.springframework.security.oauth2.core.OAuth2RefreshToken[], and additional state specific to the executed authorization grant type.
|
||||
|
||||
The {security-api-url}/org/springframework/security/oauth2/core/OAuth2Token.html[`OAuth2Token`] instances associated with an `OAuth2Authorization` vary, depending on the authorization grant type.
|
||||
The javadoc:org.springframework.security.oauth2.core.OAuth2Token[] instances associated with an `OAuth2Authorization` vary, depending on the authorization grant type.
|
||||
|
||||
For the OAuth2 https://datatracker.ietf.org/doc/html/rfc6749#section-4.1[authorization_code grant], an `OAuth2AuthorizationCode`, an `OAuth2AccessToken`, and an (optional) `OAuth2RefreshToken` are associated.
|
||||
|
||||
For the OpenID Connect 1.0 https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[authorization_code grant], an `OAuth2AuthorizationCode`, an {security-api-url}/org/springframework/security/oauth2/core/oidc/OidcIdToken.html[`OidcIdToken`], an `OAuth2AccessToken`, and an (optional) `OAuth2RefreshToken` are associated.
|
||||
For the OpenID Connect 1.0 https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[authorization_code grant], an `OAuth2AuthorizationCode`, an javadoc:org.springframework.security.oauth2.core.oidc.OidcIdToken[], an `OAuth2AccessToken`, and an (optional) `OAuth2RefreshToken` are associated.
|
||||
|
||||
For the OAuth2 https://datatracker.ietf.org/doc/html/rfc6749#section-4.4[client_credentials grant], only an `OAuth2AccessToken` is associated.
|
||||
|
||||
|
||||
@@ -277,9 +277,9 @@ This is a minimal configuration for getting started quickly. To understand what
|
||||
|
||||
<1> A Spring Security filter chain for the xref:servlet/oauth2/authorization-server/protocol-endpoints.adoc[Protocol Endpoints].
|
||||
<2> A Spring Security filter chain for xref:servlet/authentication/index.adoc#servlet-authentication[authentication].
|
||||
<3> An instance of {security-api-url}/org/springframework/security/core/userdetails/UserDetailsService.html[`UserDetailsService`] for retrieving users to authenticate.
|
||||
<3> An instance of javadoc:org.springframework.security.core.userdetails.UserDetailsService[] for retrieving users to authenticate.
|
||||
<4> An instance of xref:servlet/oauth2/authorization-server/core-model-components.adoc#oauth2AuthorizationServer-registered-client-repository[`RegisteredClientRepository`] for managing clients.
|
||||
<5> An instance of `com.nimbusds.jose.jwk.source.JWKSource` for signing access tokens.
|
||||
<6> An instance of `java.security.KeyPair` with keys generated on startup used to create the `JWKSource` above.
|
||||
<7> An instance of {security-api-url}/org/springframework/security/oauth2/jwt/JwtDecoder.html[`JwtDecoder`] for decoding signed access tokens.
|
||||
<7> An instance of javadoc:org.springframework.security.oauth2.jwt.JwtDecoder[] for decoding signed access tokens.
|
||||
<8> An instance of xref:servlet/oauth2/authorization-server/configuration-model.adoc#oauth2AuthorizationServer-configuring-authorization-server-settings[`AuthorizationServerSettings`] to configure Spring Security Authorization Server.
|
||||
|
||||
@@ -944,5 +944,52 @@ For MAC-based algorithms (such as `HS256`, `HS384`, or `HS512`), the `client-sec
|
||||
If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided `ClientRegistration` to determine which algorithm to return.
|
||||
====
|
||||
|
||||
[[oauth2login-advanced-idtoken-decoder-factory]]
|
||||
== Providing a Custom JwtDecoderFactory
|
||||
|
||||
`OidcIdTokenDecoderFactory` is deliberately narrow: it exists to make it easy to support multiple `ClientRegistration` instances, each potentially needing a different `JwtDecoder`, and its configuration surface is limited to what most applications need for that purpose, such as the JWS algorithm resolver shown above.
|
||||
|
||||
If your application needs deeper control over how the ID Token's `JwtDecoder` is constructed -- for example, providing your own `RestOperations` -- you can instead provide your own `JwtDecoderFactory<ClientRegistration>` `@Bean`.
|
||||
Because OAuth2 Login does not use a resource server `JwtDecoder` bean for this purpose, this is the supported way to reach the same level of control that `NimbusJwtDecoder`'s builder already offers, without introducing additional configuration properties on `OidcIdTokenDecoderFactory` itself.
|
||||
|
||||
For a single `ClientRegistration`, this can be as simple as:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory(RestOperations rest) {
|
||||
return (clientRegistration) -> {
|
||||
String issuerUri = clientRegistration.getProviderDetails().getIssuerUri();
|
||||
NimbusJwtDecoder decoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).restOperations(rest).build();
|
||||
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(new OidcIdTokenValidator(clientRegistration)));
|
||||
decoder.setClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter());
|
||||
return decoder;
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun idTokenDecoderFactory(rest: RestOperations): JwtDecoderFactory<ClientRegistration> {
|
||||
return JwtDecoderFactory { clientRegistration ->
|
||||
val issuerUri = clientRegistration.providerDetails.issuerUri
|
||||
val decoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).restOperations(rest).build()
|
||||
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(OidcIdTokenValidator(clientRegistration)))
|
||||
decoder.setClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter())
|
||||
decoder
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If you have multiple `ClientRegistration` instances and want to avoid rebuilding a `JwtDecoder` on every request, cache the decoder per registration, for example by annotating the factory method with `@Cacheable` or by keying a `Map` on `ClientRegistration#getRegistrationId()`.
|
||||
|
||||
[[oauth2login-advanced-oidc-logout]]
|
||||
Then, you can proceed to configure xref:servlet/oauth2/login/logout.adoc[logout]
|
||||
|
||||
@@ -1582,6 +1582,7 @@ fun jwtDecoder(): JwtDecoder {
|
||||
== Configuring Timeouts
|
||||
|
||||
By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server.
|
||||
You can override these defaults without changing any code by setting the JDK's `sun.net.client.defaultConnectTimeout` and `sun.net.client.defaultReadTimeout` system properties (in milliseconds).
|
||||
|
||||
This may be too short in some scenarios.
|
||||
Further, it doesn't take into account more sophisticated patterns like back-off and discovery.
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.11",
|
||||
"antora": "3.2.0-rc.2",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||
"@antora/collector-extension": "1.0.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.11",
|
||||
"@springio/antora-extensions": "1.14.13",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ plugins {
|
||||
apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
node {
|
||||
version = "24.15.0"
|
||||
}
|
||||
|
||||
antora {
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
environment = [
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.passwords.servletauthenticationform;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class FormLoginServletPathConfiguration {
|
||||
|
||||
// tag::loginPage[]
|
||||
@Bean
|
||||
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin((form) -> form
|
||||
.loginPage("/api/login")
|
||||
.loginProcessingUrl("/api/login")
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::loginPage[]
|
||||
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.passwords.servletauthenticationform
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class FormLoginServletPathConfiguration {
|
||||
|
||||
// tag::loginPage[]
|
||||
@Bean
|
||||
fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin {
|
||||
loginPage = "/api/login"
|
||||
loginProcessingUrl = "/api/login"
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
// @formatter:on
|
||||
}
|
||||
// end::loginPage[]
|
||||
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=4.0.0-SNAPSHOT
|
||||
version=7.0.5
|
||||
version=7.0.7
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+15
-13
@@ -1,22 +1,23 @@
|
||||
[versions]
|
||||
com-squareup-okhttp3 = "3.14.9"
|
||||
io-micrometer = "1.16.7"
|
||||
io-rsocket = "1.1.5"
|
||||
io-spring-javaformat = "0.0.47"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.2.0"
|
||||
org-apache-maven-resolver = "1.9.27"
|
||||
org-aspectj = "1.9.25.1"
|
||||
org-bouncycastle = "1.80"
|
||||
org-bouncycastle = "1.80.2"
|
||||
org-eclipse-jetty = "11.0.26"
|
||||
org-jetbrains-kotlin = "2.2.21"
|
||||
org-jetbrains-kotlinx = "1.10.2"
|
||||
org-mockito = "5.17.0"
|
||||
org-opensaml5 = "5.1.6"
|
||||
org-springframework = "7.0.7"
|
||||
org-springframework = "7.0.9"
|
||||
com-password4j = "1.8.4"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.38"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.20.2"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
@@ -24,13 +25,14 @@ com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:10.4"
|
||||
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:11.26.1"
|
||||
com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "com-squareup-okhttp3" }
|
||||
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
|
||||
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:7.0.4"
|
||||
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:7.0.5"
|
||||
com-jayway-jsonpath-json-path = "com.jayway.jsonpath:json-path:2.9.0"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.4"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.14"
|
||||
io-mockk = "io.mockk:mockk:1.14.9"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.5"
|
||||
io-micrometer-micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "io-micrometer" }
|
||||
io-micrometer-micrometer-observation = { module = "io.micrometer:micrometer-observation", version.ref = "io-micrometer" }
|
||||
io-mockk = "io.mockk:mockk:1.14.11"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.7"
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
|
||||
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
|
||||
@@ -50,8 +52,8 @@ ldapsdk = "ldapsdk:ldapsdk:4.1"
|
||||
net-sourceforge-htmlunit = "net.sourceforge.htmlunit:htmlunit:2.70.0"
|
||||
org-htmlunit-htmlunit = "org.htmlunit:htmlunit:4.11.1"
|
||||
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents.client5:httpclient5:5.5.2"
|
||||
org-apache-kerby-simplekdc='org.apache.kerby:kerb-simplekdc:2.1.1'
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.15"
|
||||
org-apache-kerby-simplekdc='org.apache.kerby:kerb-simplekdc:2.1.2'
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.16"
|
||||
org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" }
|
||||
@@ -80,9 +82,9 @@ org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.5"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.3"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.18"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.7"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.5"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.4"
|
||||
@@ -102,7 +104,7 @@ org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanne
|
||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.6'
|
||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.3.RELEASE'
|
||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.6.RELEASE'
|
||||
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
|
||||
|
||||
[plugins]
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=f1771298a70f6db5a29daf62378c4e18a17fc33c9ba6b14362e0cdf40610380d
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
|
||||
distributionSha256Sum=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+20
@@ -21,9 +21,11 @@ import java.net.ServerSocket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -48,6 +50,24 @@ public class UnboundIdContainerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startLdapServerThenListenerBindsToLoopbackAddressOnly() throws Exception {
|
||||
UnboundIdContainer server = new UnboundIdContainer("dc=springframework,dc=org", null);
|
||||
server.setApplicationContext(new GenericApplicationContext());
|
||||
List<Integer> ports = getDefaultPorts(1);
|
||||
server.setPort(ports.get(0));
|
||||
|
||||
try {
|
||||
server.afterPropertiesSet();
|
||||
InMemoryDirectoryServer directoryServer = (InMemoryDirectoryServer) ReflectionTestUtils.getField(server,
|
||||
"directoryServer");
|
||||
assertThat(directoryServer.getListenAddress().isLoopbackAddress()).isTrue();
|
||||
}
|
||||
finally {
|
||||
server.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterPropertiesSetWhenPortIsZeroThenRandomPortIsSelected() throws Exception {
|
||||
UnboundIdContainer server = new UnboundIdContainer("dc=springframework,dc=org", null);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.ldap.server;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
|
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
|
||||
@@ -96,7 +97,8 @@ public class UnboundIdContainer
|
||||
try {
|
||||
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(this.defaultPartitionSuffix);
|
||||
config.addAdditionalBindCredentials("uid=admin,ou=system", "secret");
|
||||
config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", this.port));
|
||||
config.setListenerConfigs(
|
||||
InMemoryListenerConfig.createLDAPConfig("LDAP", InetAddress.getLoopbackAddress(), this.port, null));
|
||||
config.setEnforceSingleStructuralObjectClass(false);
|
||||
config.setEnforceAttributeSyntaxCompliance(true);
|
||||
DN dn = new DN(this.defaultPartitionSuffix);
|
||||
|
||||
+1
-6
@@ -20,7 +20,6 @@ import java.util.function.Supplier;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.expression.BeanResolver;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.messaging.Message;
|
||||
@@ -47,11 +46,7 @@ public class DefaultMessageSecurityExpressionHandler<T> extends AbstractSecurity
|
||||
Message<T> message) {
|
||||
MessageSecurityExpressionRoot<T> root = createSecurityExpressionRoot(authentication, message);
|
||||
StandardEvaluationContext ctx = new StandardEvaluationContext(root);
|
||||
BeanResolver beanResolver = getBeanResolver();
|
||||
if (beanResolver != null) {
|
||||
// https://github.com/spring-projects/spring-framework/issues/35371
|
||||
ctx.setBeanResolver(beanResolver);
|
||||
}
|
||||
ctx.setBeanResolver(getBeanResolver());
|
||||
return ctx;
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -175,10 +175,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
context.setRootObject(securityContext);
|
||||
context.setVariable("this", securityContext);
|
||||
if (this.beanResolver != null) {
|
||||
// https://github.com/spring-projects/spring-framework/issues/35371
|
||||
context.setBeanResolver(this.beanResolver);
|
||||
}
|
||||
context.setBeanResolver(this.beanResolver);
|
||||
Expression expression = this.parser.parseExpression(expressionToParse);
|
||||
securityContext = expression.getValue(context);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user