Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0115e8aea1 | |||
| 6b69edb9a7 | |||
| 0ba26176db | |||
| 85775fe794 | |||
| 1ca53164ee | |||
| 47a521ca26 | |||
| 751c6a762a | |||
| 7d7b59bea4 | |||
| 7149622efd | |||
| e9e0ec89fb | |||
| 596c5f9b0c | |||
| 5d335ccf8d | |||
| 5cdcdc9bcb | |||
| a5b1dec99a | |||
| ea2ec04633 | |||
| c6bc1ec969 | |||
| 9ac5ee9bdb | |||
| 31babecfbd | |||
| 4c12940304 | |||
| bb3738dbfd | |||
| 5c1a108b8b | |||
| 1c4a63387b | |||
| dab48d25b0 | |||
| caa325f92e | |||
| 174e3ef356 | |||
| 32e2735f45 | |||
| 9cfc9bb8a8 | |||
| 341f58d411 | |||
| c00197bdf5 | |||
| 01e1813020 | |||
| 65c037853e | |||
| 05256da801 | |||
| 2bdb57a8a8 | |||
| a3095527a0 | |||
| 9189916287 | |||
| 97e6ac76af | |||
| 779030b6cd | |||
| 1135ad5a5a | |||
| e3d642ce7c | |||
| 00247350d0 | |||
| 9e2cadb475 | |||
| 20332fedb0 | |||
| 2537a033f5 | |||
| f622d8e2e2 | |||
| 92cab2b678 | |||
| 254709ce0b | |||
| e9c1b7b0ae | |||
| 2b322da9ac | |||
| 6aabd768a8 | |||
| cdd626644e | |||
| 5a798e93f1 | |||
| aa88404d69 | |||
| dcb8c563e8 | |||
| e34621ec2c | |||
| 1695d03b72 | |||
| e7ea4091a0 | |||
| 235dfc4dfd | |||
| 092a63d1e0 | |||
| 896dd93313 | |||
| c7b739eb3f | |||
| 32e860523c | |||
| 58232b1172 | |||
| 0e8fd1cd6c | |||
| 1f001f8a6d | |||
| 3fdfa98172 | |||
| 90f5141880 | |||
| ab6667eb5c | |||
| 4b10c2daac | |||
| 63e24a3026 | |||
| 9027dceb71 | |||
| 1ebd663253 | |||
| 09198447af | |||
| e1ec77a7a1 | |||
| d686037419 | |||
| 5c8311b7d3 | |||
| d86545dd41 | |||
| ce995a0178 | |||
| e5ee45d568 | |||
| 3b9991fc89 | |||
| 0e211382ee | |||
| c35e107744 | |||
| 8271258618 | |||
| 1010d253a4 | |||
| c8342fcfae | |||
| 2287d67106 | |||
| 470e2c5c97 | |||
| 657760af5b | |||
| 71a51db3a1 | |||
| 17873adf1f |
@@ -10,9 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
COMMIT_OWNER: ${{ github.event.pusher.name }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
|
||||
@@ -61,10 +59,6 @@ jobs:
|
||||
- name: Set up gradle user name
|
||||
run: echo 'systemProp.user.name=spring-builds+github' >> gradle.properties
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
run: ./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
snapshot_tests:
|
||||
name: Test against snapshots
|
||||
@@ -79,11 +73,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Snapshot Tests
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion='5.+' -PreactorVersion='2020.0.+' -PspringDataVersion='2021.2.+' -PlocksDisabled --stacktrace
|
||||
run: ./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion='5.+' -PreactorVersion='2020.0.+' -PspringDataVersion='2021.2.+' -PlocksDisabled --stacktrace
|
||||
check_samples:
|
||||
name: Check Samples project
|
||||
needs: [prerequisites]
|
||||
@@ -102,9 +92,6 @@ jobs:
|
||||
SAMPLES_DIR: ../spring-security-samples
|
||||
VERSION: ${{ needs.prerequisites.outputs.project_version }}
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew publishMavenJavaPublicationToLocalRepository
|
||||
./gradlew cloneSamples -PcloneOutputDirectory="$SAMPLES_DIR"
|
||||
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$VERSION" :runAllTests
|
||||
@@ -121,11 +108,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Check for package tangles
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
run: ./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
deploy_artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
@@ -138,11 +121,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Deploy artifacts
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
|
||||
run: ./gradlew publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -162,11 +141,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Deploy Docs
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew deployDocs -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
|
||||
run: ./gradlew deployDocs -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
|
||||
env:
|
||||
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
|
||||
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
|
||||
@@ -183,11 +158,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Deploy Schema
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew deploySchema -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace --info
|
||||
run: ./gradlew deploySchema -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace --info
|
||||
env:
|
||||
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
|
||||
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
|
||||
@@ -236,14 +207,11 @@ jobs:
|
||||
echo "Artifacts for $REPO@$VERSION have been released to Maven Central."
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
echo "Tagging and publishing $REPO@$VERSION release on GitHub."
|
||||
./gradlew createGitHubRelease -PnextVersion=$VERSION -Pbranch=$BRANCH -PcreateRelease=true -PgitHubAccessToken=$TOKEN
|
||||
- name: Announce Release on Slack
|
||||
id: spring-security-announcing
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
@@ -267,9 +235,6 @@ jobs:
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Update to next Snapshot Version
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
echo "Updating $REPO@$VERSION to next snapshot version."
|
||||
./gradlew :updateToSnapshotVersion
|
||||
git commit -am "Next development version"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Mark Duplicate Dependabot PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
check_duplicate_prs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Dependency Name from PR Title
|
||||
id: extract
|
||||
run: |
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}')
|
||||
echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find PRs
|
||||
id: find_duplicates
|
||||
env:
|
||||
DEPENDENCY_NAME: ${{ steps.extract.outputs.dependency_name }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "$DEPENDENCY_NAME"' --json number --jq 'map(.number) | join(",")')
|
||||
echo "prs=$PRS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Label Duplicate PRs
|
||||
if: steps.find_duplicates.outputs.prs != ''
|
||||
env:
|
||||
PRS: ${{ steps.find_duplicates.outputs.prs }}
|
||||
CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
for i in ${PRS//,/ }
|
||||
do
|
||||
if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then
|
||||
echo "Marking PR $i as duplicate"
|
||||
gh pr edit "$i" --add-label "status: duplicate"
|
||||
gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER"
|
||||
fi
|
||||
done
|
||||
@@ -9,7 +9,7 @@ permissions: write-all
|
||||
jobs:
|
||||
merge-dependabot-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -3,9 +3,7 @@ name: PR Build
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -23,4 +21,24 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue --scan
|
||||
run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue
|
||||
generate-docs:
|
||||
name: Generate Docs
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Run Antora
|
||||
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
|
||||
- name: Upload Docs
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
overwrite: true
|
||||
|
||||
@@ -5,9 +5,7 @@ on:
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -34,18 +32,12 @@ jobs:
|
||||
- id: check-release-due
|
||||
name: Check Release Due
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew gitHubCheckNextVersionDueToday
|
||||
echo "is_due_today=$(cat build/github/milestones/is-due-today)" >>$GITHUB_OUTPUT
|
||||
- id: check-open-issues
|
||||
name: Check for open issues
|
||||
if: steps.check-release-due.outputs.is_due_today == 'true'
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew gitHubCheckMilestoneHasNoOpenIssues
|
||||
echo "is_open_issues=$(cat build/github/milestones/is-open-issues)" >>$GITHUB_OUTPUT
|
||||
- id: validate-release-state
|
||||
@@ -58,9 +50,6 @@ jobs:
|
||||
name: Update version and push
|
||||
if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'false'
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
./gradlew :updateProjectVersion
|
||||
|
||||
@@ -28,3 +28,5 @@ s101plugin.state
|
||||
|
||||
!.idea/checkstyle-idea.xml
|
||||
!.idea/externalDependencies.xml
|
||||
|
||||
node_modules
|
||||
|
||||
+4
-3
@@ -25,6 +25,7 @@ apply plugin: 'org.springframework.github.milestone'
|
||||
apply plugin: 'org.springframework.github.changelog'
|
||||
apply plugin: 'org.springframework.github.release'
|
||||
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
|
||||
apply plugin: 'org.springframework.security.check-expected-branch-version'
|
||||
|
||||
group = 'org.springframework.security'
|
||||
description = 'Spring Security'
|
||||
@@ -129,10 +130,10 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasProperty('buildScan')) {
|
||||
develocity {
|
||||
buildScan {
|
||||
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
|
||||
termsOfServiceAgree = 'yes'
|
||||
termsOfUseUrl = 'https://gradle.com/help/legal-terms-of-use'
|
||||
termsOfUseAgree = 'yes'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,10 @@ gradlePlugin {
|
||||
id = "org.springframework.security.versions.verify-dependencies-versions"
|
||||
implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin"
|
||||
}
|
||||
checkExpectedBranchVersion {
|
||||
id = "org.springframework.security.check-expected-branch-version"
|
||||
implementationClass = "org.springframework.security.CheckExpectedBranchVersionPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
|
||||
/**
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
TaskProvider<CheckExpectedBranchVersionTask> checkExpectedBranchVersionTask = project.getTasks().register("checkExpectedBranchVersion", CheckExpectedBranchVersionTask.class, (task) -> {
|
||||
task.setGroup("Build");
|
||||
task.setDescription("Check if the project version matches the branch version");
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkExpectedBranchVersionTask));
|
||||
}
|
||||
|
||||
public static class CheckExpectedBranchVersionTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Project project = getProject();
|
||||
if (project.hasProperty("skipCheckExpectedBranchVersion")) {
|
||||
return;
|
||||
}
|
||||
String version = (String) project.getVersion();
|
||||
String branchVersion = getBranchVersion(project);
|
||||
if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) {
|
||||
System.out.println("Branch version does not match *.x, ignoring");
|
||||
return;
|
||||
}
|
||||
if (!versionsMatch(version, branchVersion)) {
|
||||
throw new IllegalStateException(String.format("Project version [%s] does not match branch version [%s]. " +
|
||||
"Please verify that the branch contains the right version.", version, branchVersion));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getBranchVersion(Project project) throws IOException {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
project.exec((exec) -> {
|
||||
exec.commandLine("git", "symbolic-ref", "--short", "HEAD");
|
||||
exec.setErrorOutput(System.err);
|
||||
exec.setStandardOutput(baos);
|
||||
});
|
||||
return baos.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean versionsMatch(String projectVersion, String branchVersion) {
|
||||
String[] projectVersionParts = projectVersion.split("\\.");
|
||||
String[] branchVersionParts = branchVersion.split("\\.");
|
||||
if (projectVersionParts.length < 2 || branchVersionParts.length < 2) {
|
||||
return false;
|
||||
}
|
||||
return projectVersionParts[0].equals(branchVersionParts[0]) && projectVersionParts[1].equals(branchVersionParts[1]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+68
-69
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -43,11 +44,13 @@ import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
|
||||
/**
|
||||
@@ -322,43 +325,22 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
if (servletContext == null) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
boolean isProgrammaticApiAvailable = isProgrammaticApiAvailable(servletContext);
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
for (String pattern : patterns) {
|
||||
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
|
||||
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
|
||||
if (isProgrammaticApiAvailable) {
|
||||
matchers.add(resolve(ant, mvc, servletContext));
|
||||
}
|
||||
else {
|
||||
this.logger
|
||||
.warn("The ServletRegistration API was not available at startup time. This may be due to a misconfiguration; "
|
||||
+ "if you are using AbstractSecurityWebApplicationInitializer, please double-check the recommendations outlined in "
|
||||
+ "https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#abstractsecuritywebapplicationinitializer-with-spring-mvc");
|
||||
matchers.add(new DeferredRequestMatcher((request) -> resolve(ant, mvc, request.getServletContext()),
|
||||
mvc, ant));
|
||||
}
|
||||
matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant));
|
||||
}
|
||||
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
|
||||
}
|
||||
|
||||
private static boolean isProgrammaticApiAvailable(ServletContext servletContext) {
|
||||
try {
|
||||
servletContext.getServletRegistrations();
|
||||
return true;
|
||||
}
|
||||
catch (UnsupportedOperationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc, ServletContext servletContext) {
|
||||
Map<String, ? extends ServletRegistration> registrations = mappableServletRegistrations(servletContext);
|
||||
if (registrations.isEmpty()) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
if (!hasDispatcherServlet(registrations)) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
ServletRegistration dispatcherServlet = requireOneRootDispatcherServlet(registrations);
|
||||
if (dispatcherServlet != null) {
|
||||
@@ -452,7 +434,7 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
}
|
||||
}
|
||||
|
||||
private String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
private static String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
|
||||
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
|
||||
+ "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n"
|
||||
@@ -593,34 +575,29 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
static class DeferredRequestMatcher implements RequestMatcher {
|
||||
|
||||
final Function<HttpServletRequest, RequestMatcher> requestMatcherFactory;
|
||||
final Function<ServletContext, RequestMatcher> requestMatcherFactory;
|
||||
|
||||
final AtomicReference<String> description = new AtomicReference<>();
|
||||
|
||||
volatile RequestMatcher requestMatcher;
|
||||
final Map<ServletContext, RequestMatcher> requestMatchers = new ConcurrentHashMap<>();
|
||||
|
||||
DeferredRequestMatcher(Function<HttpServletRequest, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (request) -> {
|
||||
if (this.requestMatcher == null) {
|
||||
synchronized (this) {
|
||||
if (this.requestMatcher == null) {
|
||||
this.requestMatcher = resolver.apply(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.requestMatcher;
|
||||
};
|
||||
DeferredRequestMatcher(Function<ServletContext, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (sc) -> this.requestMatchers.computeIfAbsent(sc, resolver);
|
||||
this.description.set("Deferred " + Arrays.toString(candidates));
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(ServletContext servletContext) {
|
||||
return this.requestMatcherFactory.apply(servletContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matches(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matcher(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -630,53 +607,75 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
}
|
||||
|
||||
static class MockMvcRequestMatcher implements RequestMatcher {
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
|
||||
DispatcherServletRequestMatcher(ServletContext servletContext) {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, computeErrorMessage(this.servletContext.getServletRegistrations().values()));
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return DispatcherServlet.class.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final AntPathRequestMatcher ant;
|
||||
|
||||
private final MvcRequestMatcher mvc;
|
||||
|
||||
private final ServletContext servletContext;
|
||||
private final RequestMatcher dispatcherServlet;
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
ServletContext servletContext) {
|
||||
this(ant, mvc, new OrRequestMatcher(new MockMvcRequestMatcher(),
|
||||
new DispatcherServletRequestMatcher(servletContext)));
|
||||
}
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
RequestMatcher dispatcherServlet) {
|
||||
this.ant = ant;
|
||||
this.mvc = mvc;
|
||||
this.servletContext = servletContext;
|
||||
this.dispatcherServlet = dispatcherServlet;
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(HttpServletRequest request) {
|
||||
if (this.dispatcherServlet.matches(request)) {
|
||||
return this.mvc;
|
||||
}
|
||||
return this.ant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
return this.mvc.matches(request);
|
||||
}
|
||||
return this.ant.matches(request);
|
||||
return requestMatcher(request).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
return this.mvc.matcher(request);
|
||||
}
|
||||
return this.ant.matcher(request);
|
||||
}
|
||||
|
||||
private boolean isDispatcherServlet(ServletRegistration registration) {
|
||||
Class<?> dispatcherServlet = ClassUtils
|
||||
.resolveClassName("org.springframework.web.servlet.DispatcherServlet", null);
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return dispatcherServlet.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
return requestMatcher(request).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-6
@@ -41,8 +41,6 @@ public class PasswordEncoderParser {
|
||||
|
||||
public static final String ATT_HASH = "hash";
|
||||
|
||||
static final String ATT_BASE_64 = "base64";
|
||||
|
||||
static final String OPT_HASH_BCRYPT = "bcrypt";
|
||||
|
||||
private static final Map<String, Class<?>> ENCODER_CLASSES = Collections.singletonMap(OPT_HASH_BCRYPT,
|
||||
@@ -62,19 +60,17 @@ public class PasswordEncoderParser {
|
||||
return;
|
||||
}
|
||||
String hash = element.getAttribute(ATT_HASH);
|
||||
boolean useBase64 = StringUtils.hasText(element.getAttribute(ATT_BASE_64))
|
||||
&& Boolean.parseBoolean(element.getAttribute(ATT_BASE_64));
|
||||
String ref = element.getAttribute(ATT_REF);
|
||||
if (StringUtils.hasText(ref)) {
|
||||
this.passwordEncoder = new RuntimeBeanReference(ref);
|
||||
}
|
||||
else {
|
||||
this.passwordEncoder = createPasswordEncoderBeanDefinition(hash, useBase64);
|
||||
this.passwordEncoder = createPasswordEncoderBeanDefinition(hash);
|
||||
((RootBeanDefinition) this.passwordEncoder).setSource(parserContext.extractSource(element));
|
||||
}
|
||||
}
|
||||
|
||||
public static BeanDefinition createPasswordEncoderBeanDefinition(String hash, boolean useBase64) {
|
||||
public static BeanDefinition createPasswordEncoderBeanDefinition(String hash) {
|
||||
Class<?> beanClass = ENCODER_CLASSES.get(hash);
|
||||
BeanDefinitionBuilder beanBldr = BeanDefinitionBuilder.rootBeanDefinition(beanClass);
|
||||
return beanBldr.getBeanDefinition();
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ public class LdapProviderBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
else if (StringUtils.hasText(hash)) {
|
||||
authenticatorBuilder.addPropertyValue("passwordEncoder",
|
||||
PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash, false));
|
||||
PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash));
|
||||
}
|
||||
}
|
||||
authenticatorBuilder.addConstructorArgValue(contextSource);
|
||||
|
||||
+85
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,27 +30,35 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.TestMockHttpServletMappings;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractRequestMatcherRegistry}.
|
||||
@@ -206,18 +214,65 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
// gh-14418
|
||||
@Test
|
||||
public void requestMatchersWhenNoDispatcherServletMockMvcThenMvcRequestMatcherType() throws Exception {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
try (SpringTestContext spring = new SpringTestContext(this)) {
|
||||
spring.register(MockMvcConfiguration.class)
|
||||
.postProcessor((context) -> context.setServletContext(servletContext))
|
||||
.autowire();
|
||||
this.matcherRegistry.setApplicationContext(spring.getContext());
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(spring.getContext()).build();
|
||||
MockHttpServletRequest request = mvc.perform(get("/")).andReturn().getRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -345,6 +400,19 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
verifyNoMoreInteractions(mvc);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenNoMappingThenException() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
servletContext.addServlet("default", DispatcherServlet.class).addMapping("/");
|
||||
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
|
||||
MvcRequestMatcher mvc = mock(MvcRequestMatcher.class);
|
||||
AntPathRequestMatcher ant = mock(AntPathRequestMatcher.class);
|
||||
DispatcherServletDelegatingRequestMatcher requestMatcher = new DispatcherServletDelegatingRequestMatcher(ant,
|
||||
mvc, servletContext);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> requestMatcher.matcher(request));
|
||||
}
|
||||
|
||||
private void mockMvcIntrospector(boolean isPresent) {
|
||||
ApplicationContext context = this.matcherRegistry.getApplicationContext();
|
||||
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
|
||||
@@ -381,11 +449,13 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
return requestMatchers;
|
||||
}
|
||||
|
||||
private static List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
private List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
for (RequestMatcher requestMatcher : wrappedMatchers) {
|
||||
if (requestMatcher instanceof AbstractRequestMatcherRegistry.DeferredRequestMatcher) {
|
||||
requestMatchers.add(((DeferredRequestMatcher) requestMatcher).requestMatcher);
|
||||
if (requestMatcher instanceof DeferredRequestMatcher) {
|
||||
DeferredRequestMatcher deferred = (DeferredRequestMatcher) requestMatcher;
|
||||
WebApplicationContext web = (WebApplicationContext) getApplicationContext();
|
||||
requestMatchers.add(deferred.requestMatcher(web.getServletContext()));
|
||||
}
|
||||
else {
|
||||
requestMatchers.add(requestMatcher);
|
||||
@@ -396,4 +466,11 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class MockMvcConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -76,7 +76,7 @@ public class AuthorizeRequestsTests {
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.servletContext = spy(MockServletContext.mvc());
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(this.servletContext, "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -75,7 +75,7 @@ public class HttpSecuritySecurityMatchersTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -72,7 +72,7 @@ public class UrlAuthorizationConfigurerTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+14
@@ -20,10 +20,13 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -65,4 +68,15 @@ public class PasswordEncoderParserTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatePasswordEncoderBeanDefinition() throws Exception {
|
||||
String hash = "bcrypt";
|
||||
Class<?> expectedBeanClass = BCryptPasswordEncoder.class;
|
||||
|
||||
BeanDefinition beanDefinition = PasswordEncoderParser.createPasswordEncoderBeanDefinition(hash);
|
||||
|
||||
Class<?> actualBeanClass = Class.forName(beanDefinition.getBeanClassName());
|
||||
assertThat(actualBeanClass).isEqualTo(expectedBeanClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -129,6 +129,10 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
private static final String DEFAULT_ID_SUFFIX = "}";
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\"";
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER_PREFIX = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
|
||||
|
||||
private final String idPrefix;
|
||||
|
||||
private final String idSuffix;
|
||||
@@ -286,7 +290,10 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
|
||||
String id = extractId(prefixEncodedPassword);
|
||||
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
|
||||
if (id != null && !id.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
|
||||
}
|
||||
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,6 +43,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DelegatingPasswordEncoderTests {
|
||||
|
||||
public static final String NO_PASSWORD_ENCODER = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder bcrypt;
|
||||
|
||||
@@ -201,7 +203,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoStartingPrefixStringThenFalse() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
public void matchesWhenNoIdStringThenFalse() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -226,7 +228,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ public class DelegatingPasswordEncoderTests {
|
||||
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> passwordEncoder.matches(this.rawPassword, this.rawPassword))
|
||||
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
}
|
||||
|
||||
@@ -289,4 +291,14 @@ public class DelegatingPasswordEncoderTests {
|
||||
verifyNoMoreInteractions(this.bcrypt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForTheId() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.withMessage(NO_PASSWORD_ENCODER);
|
||||
verifyNoMoreInteractions(this.bcrypt, this.noop);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
antora:
|
||||
extensions:
|
||||
- require: '@springio/antora-extensions'
|
||||
root_component_name: 'security'
|
||||
site:
|
||||
title: Spring Security
|
||||
url: https://docs.spring.io/spring-security/reference
|
||||
robots: allow
|
||||
git:
|
||||
ensure_git_suffix: false
|
||||
content:
|
||||
sources:
|
||||
- url: https://github.com/spring-projects/spring-security
|
||||
branches: [main, '5.{{6..9},{1..9}+({0..9})}.x', '6.+({0..9}).x']
|
||||
tags: ['5.{{6..9},{1..9}+({0..9})}.{0..99}?(-RC+({0..9}))', '6.+({0..9}).+({0..9})?(-{RC,M}*)','!(5.6.{0..10}*)', '!(5.7.{0..8}*)', '!(5.8.{0..3}?({-RC,-M}+({0..9})))','!(6.0.{0..3}*)','!(6.1.0*)']
|
||||
start_path: docs
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-stackoverflow-url: https://stackoverflow.com/tags/spring-security
|
||||
page-related-doc-categories: security
|
||||
page-related-doc-projects: framework,graphql
|
||||
hide-uri-scheme: '@'
|
||||
tabs-sync-option: '@'
|
||||
extensions:
|
||||
- '@asciidoctor/tabs'
|
||||
- '@springio/asciidoctor-extensions'
|
||||
urls:
|
||||
latest_version_segment_strategy: redirect:to
|
||||
latest_version_segment: ''
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
failure_level: warn
|
||||
@@ -44,6 +44,7 @@
|
||||
***** xref:servlet/authentication/passwords/in-memory.adoc[In Memory]
|
||||
***** xref:servlet/authentication/passwords/jdbc.adoc[JDBC]
|
||||
***** xref:servlet/authentication/passwords/user-details.adoc[UserDetails]
|
||||
***** xref:servlet/authentication/passwords/credentials-container.adoc[CredentialsContainer]
|
||||
***** xref:servlet/authentication/passwords/user-details-service.adoc[UserDetailsService]
|
||||
***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder]
|
||||
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
|
||||
|
||||
@@ -105,6 +105,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
@@ -113,7 +114,8 @@ static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
|
||||
[TIP]
|
||||
====
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes.
|
||||
Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
|
||||
====
|
||||
|
||||
[[jc-reactive-method-security-custom-authorization-manager]]
|
||||
|
||||
@@ -442,7 +442,7 @@ In Spring Security this is done by saving the `HttpServletRequest` using a <<req
|
||||
|
||||
The `HttpServletRequest` is saved in the {security-api-url}org/springframework/security/web/savedrequest/RequestCache.html[`RequestCache`].
|
||||
When the user successfully authenticates, the `RequestCache` is used to replay the original request.
|
||||
The <<requestcacheawarefilter,`RequestCacheAwareFilter`>> is what uses the `RequestCache` to save the `HttpServletRequest`.
|
||||
The <<requestcacheawarefilter,`RequestCacheAwareFilter`>> uses the `RequestCache` to get the saved `HttpServletRequest` after the user authenticates, while the `ExceptionTranslationFilter` uses the `RequestCache` to save the `HttpServletRequest` after it detects `AuthenticationException`, before redirecting the user to the login endpoint.
|
||||
|
||||
By default, an `HttpSessionRequestCache` is used.
|
||||
The code below demonstrates how to customize the `RequestCache` implementation that is used to check the `HttpSession` for a saved request if the parameter named `continue` is present.
|
||||
@@ -510,7 +510,7 @@ XML::
|
||||
[[requestcacheawarefilter]]
|
||||
=== RequestCacheAwareFilter
|
||||
|
||||
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to save the `HttpServletRequest`.
|
||||
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to replay the original request.
|
||||
|
||||
[[servlet-logging]]
|
||||
== Logging
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[[servlet-authentication-credentialscontainer]]
|
||||
= CredentialsContainer
|
||||
|
||||
{security-api-url}org/springframework/security/core/CredentialsContainer.html[The `CredentialsContainer`] interface indicates that the implementing object contains sensitive data, and is used internally by Spring Security to erase the authentication credentials after a successful authentication.
|
||||
This interface is implemented by most of Spring Security internal domain classes, like {security-api-url}org/springframework/security/core/userdetails/User.html[User] and {security-api-url}org/springframework/security/authentication/UsernamePasswordAuthenticationToken.html[UsernamePasswordAuthenticationToken].
|
||||
|
||||
The `ProviderManager` manager checks whether the returned `Authentication` implements this interface.
|
||||
If so, xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager-erasing-credentials[it calls the `eraseCredentials` method] to remove the credentials from the object.
|
||||
|
||||
If you want your custom authentication objects to have their credentials erased after authentication, you should ensure that the classes implement the `CredentialsContainer` interface.
|
||||
|
||||
Users who are writing their own `AuthenticationProvider` implementations should create and return an appropriate `Authentication` object there, minus any sensitive data, rather than using this interface.
|
||||
@@ -10,6 +10,41 @@ However, despite leveraging a username/password for authentication it does not i
|
||||
There are many different scenarios for how an LDAP server may be configured so Spring Security's LDAP provider is fully configurable.
|
||||
It uses separate strategy interfaces for authentication and role retrieval and provides default implementations which can be configured to handle a wide range of situations.
|
||||
|
||||
[[servlet-authentication-ldap-required-dependencies]]
|
||||
== Required Dependencies
|
||||
|
||||
To get started, add the `spring-security-ldap` dependency to your project.
|
||||
When using Spring Boot, add the following dependencies:
|
||||
|
||||
.Spring Security LDAP Dependencies
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-ldap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-ldap</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary"]
|
||||
----
|
||||
depenendencies {
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-ldap"
|
||||
implementation "org.springframework.security:spring-security-ldap"
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[servlet-authentication-ldap-prerequisites]]
|
||||
== Prerequisites
|
||||
|
||||
@@ -81,14 +116,14 @@ dn: cn=user,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: user
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=user,ou=people,dc=springframework,dc=org
|
||||
|
||||
dn: cn=admin,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: admin
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
----
|
||||
|
||||
[[servlet-authentication-ldap-unboundid]]
|
||||
|
||||
@@ -52,9 +52,9 @@ If you have more than one in your application context, you need to specify which
|
||||
|
||||
[[remember-me-persistent-token]]
|
||||
== Persistent Token Approach
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[Improved Persistent Login Cookie Best Practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
There is a discussion on this in the comments section of this article.].
|
||||
To use the this approach with namespace configuration, you would supply a datasource reference:
|
||||
To use this approach with namespace configuration, you would supply a datasource reference:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
|
||||
@@ -234,6 +234,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
|
||||
return new GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
@@ -245,6 +246,7 @@ Kotlin::
|
||||
----
|
||||
companion object {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun grantedAuthorityDefaults() : GrantedAuthorityDefaults {
|
||||
return GrantedAuthorityDefaults("MYPREFIX_");
|
||||
}
|
||||
@@ -265,7 +267,8 @@ Xml::
|
||||
|
||||
[TIP]
|
||||
====
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
|
||||
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes.
|
||||
Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
|
||||
====
|
||||
|
||||
[[jc-method-security-custom-authorization-manager]]
|
||||
|
||||
@@ -339,7 +339,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
|
||||
JWTProcessor jwtProcessor(JWTClaimsSetAwareJWSKeySelector keySelector) {
|
||||
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
|
||||
new DefaultJWTProcessor();
|
||||
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.6",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-beta.2",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.12"
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,11 @@ apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
antora {
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-security'
|
||||
branch = 'docs-build'
|
||||
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
|
||||
checkLocalBranch = true
|
||||
}
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
environment = [
|
||||
'BUILD_REFNAME': 'HEAD',
|
||||
'BUILD_VERSION': project.version,
|
||||
]
|
||||
}
|
||||
|
||||
tasks.named("generateAntoraYml") {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.csrf.CsrfTokenAssert.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.AntPathRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.RegexRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.assertj.core.api.InstanceOfAssertFactories.*"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
|
||||
|
||||
@@ -12,4 +12,7 @@
|
||||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
<module name="SuppressWithPlainTextCommentFilter"/>
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value=".*[\\/]node(_modules|js)[\\/].*$"/>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=2.7.12
|
||||
version=5.8.12-SNAPSHOT
|
||||
version=5.8.14
|
||||
samplesBranch=5.8.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -7,7 +7,7 @@ io-spring-nohttp = "0.0.11"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-aspectj = "1.9.20.1"
|
||||
org-bouncycastle = "1.70"
|
||||
org-eclipse-jetty = "9.4.54.v20240208"
|
||||
org-eclipse-jetty = "9.4.55.v20240627"
|
||||
org-jetbrains-kotlin = "1.7.22"
|
||||
org-jetbrains-kotlinx = "1.6.4"
|
||||
org-junit-jupiter = "5.9.3"
|
||||
@@ -15,7 +15,7 @@ org-mockito = "4.8.1"
|
||||
org-opensaml4 = "4.1.0"
|
||||
org-opensaml3 = "3.4.6"
|
||||
org-slf4j = "1.7.36"
|
||||
org-springframework = "5.3.34"
|
||||
org-springframework = "5.3.39"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.13"
|
||||
@@ -30,9 +30,9 @@ com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:6.5.1"
|
||||
io-mockk = "io.mockk:mockk:1.13.3"
|
||||
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.44"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.43"
|
||||
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
|
||||
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.48"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.47"
|
||||
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.9.RELEASE"
|
||||
io-r2dbc-r2dbc-h2 = { module = "io.r2dbc:r2dbc-h2", version.ref = "io-r2dbc" }
|
||||
io-r2dbc-r2dbc-spi-test = { module = "io.r2dbc:r2dbc-spi-test", version.ref = "io-r2dbc" }
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
@@ -69,7 +69,7 @@ org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet",
|
||||
org-eclipse-persistence-javax-persistence = "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-hibernate-entitymanager = "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.2"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.3"
|
||||
org-jasig-cas-client-cas-client-core = "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "org-jetbrains-kotlin" }
|
||||
@@ -87,7 +87,7 @@ org-python-jython = "org.python:jython:2.5.3"
|
||||
org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit-driver:2.65.0"
|
||||
org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:3.141.59"
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = { module = "org.slf4j:log4j-over-slf4j", version.ref = "org-slf4j" }
|
||||
org-slf4j-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "org-slf4j" }
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2021.2.18"
|
||||
@@ -103,7 +103,7 @@ org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:3.0.9"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.38.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.14"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.29.4"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1"
|
||||
|
||||
+5
-3
@@ -136,9 +136,11 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
|
||||
ContextFactory contextFactory = new ContextFactory();
|
||||
|
||||
/**
|
||||
* @param domain the domain name (may be null or empty)
|
||||
* @param url an LDAP url (or multiple URLs)
|
||||
* @param rootDn the root DN (may be null or empty)
|
||||
* @param domain the domain name (can be null or empty)
|
||||
* @param url an LDAP url (or multiple space-delimited URLs).
|
||||
* @param rootDn the root DN (can be null or empty)
|
||||
* @see <a href="https://docs.oracle.com/javase/jndi/tutorial/ldap/misc/url.html">JNDI
|
||||
* URL format documentation</a>
|
||||
*/
|
||||
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) {
|
||||
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
|
||||
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.messaging.web.csrf;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Copied from
|
||||
@@ -43,26 +44,26 @@ final class XorCsrfTokenUtils {
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+69
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.messaging.web.csrf;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -141,6 +142,73 @@ public class XorCsrfChannelInterceptorTests {
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsShorterThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsLongerThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsShorterThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "a");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsLongerThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "abcde");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenActualBytesIsEmptyThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), "");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Message<String> message() {
|
||||
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
|
||||
}
|
||||
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -94,14 +94,10 @@ public final class WebSessionServerOAuth2AuthorizedClientRepository implements S
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, OAuth2AuthorizedClient> getAuthorizedClients(WebSession session) {
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = (session != null)
|
||||
? (Map<String, OAuth2AuthorizedClient>) session.getAttribute(this.sessionAttributeName) : null;
|
||||
if (authorizedClients == null) {
|
||||
authorizedClients = new HashMap<>();
|
||||
}
|
||||
return authorizedClients;
|
||||
Assert.notNull(session, "session cannot be null");
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = session.getAttribute(this.sessionAttributeName);
|
||||
return (authorizedClients != null) ? authorizedClients : new HashMap<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
@@ -24,10 +25,12 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -202,4 +205,28 @@ public class WebSessionServerOAuth2AuthorizedClientRepositoryTests {
|
||||
assertThat(loadedAuthorizedClient2).isSameAs(authorizedClient2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration1, this.principalName1,
|
||||
mock(OAuth2AccessToken.class));
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-5
@@ -50,11 +50,6 @@ import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
* To locate the bearer token, this looks in the Reactor {@link Context} for a key of type
|
||||
* {@link Authentication}.
|
||||
*
|
||||
* Registering
|
||||
* {@see org.springframework.security.config.annotation.web.configuration.OAuth2ResourceServerConfiguration.OAuth2ResourceServerWebFluxSecurityConfiguration.BearerRequestContextSubscriberRegistrar},
|
||||
* as a {@code @Bean} will take care of this automatically, but certainly an application
|
||||
* can supply a {@link Context} of its own to override.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.2
|
||||
*/
|
||||
|
||||
@@ -96,6 +96,7 @@ project.tasks.matching { t -> t.name == "sourcesJar"}.configureEach {
|
||||
|
||||
|
||||
javadoc {
|
||||
classpath += sourceSets.opensaml3Main.runtimeClasspath + sourceSets.opensaml4Main.runtimeClasspath
|
||||
source += sourceSets.opensaml3Main.allJava + sourceSets.opensaml4Main.allJava
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -45,8 +45,8 @@ public final class RelyingPartyRegistrations {
|
||||
* Return a {@link RelyingPartyRegistration.Builder} based off of the given SAML 2.0
|
||||
* Asserting Party (IDP) metadata location.
|
||||
*
|
||||
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
|
||||
* valid endpoints might include:
|
||||
* Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
|
||||
* Some valid endpoints might include:
|
||||
*
|
||||
* <pre>
|
||||
* metadataLocation = "classpath:asserting-party-metadata.xml";
|
||||
@@ -69,8 +69,8 @@ public final class RelyingPartyRegistrations {
|
||||
* about the asserting party. Thus, you will need to remember to still populate
|
||||
* anything about the relying party, like any private keys the relying party will use
|
||||
* for signing AuthnRequests.
|
||||
* @param metadataLocation The classpath- or file-based locations or HTTP endpoints of
|
||||
* the asserting party metadata file
|
||||
* @param metadataLocation The classpath- or file-based locations or HTTPS endpoints
|
||||
* of the asserting party metadata file
|
||||
* @return the {@link RelyingPartyRegistration.Builder} for further configuration
|
||||
*/
|
||||
public static RelyingPartyRegistration.Builder fromMetadataLocation(String metadataLocation) {
|
||||
@@ -130,8 +130,8 @@ public final class RelyingPartyRegistrations {
|
||||
* Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off
|
||||
* of the given SAML 2.0 Asserting Party (IDP) metadata location.
|
||||
*
|
||||
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
|
||||
* valid endpoints might include:
|
||||
* Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
|
||||
* Some valid endpoints might include:
|
||||
*
|
||||
* <pre>
|
||||
* metadataLocation = "classpath:asserting-party-metadata.xml";
|
||||
@@ -155,7 +155,7 @@ public final class RelyingPartyRegistrations {
|
||||
* about the asserting party. Thus, you will need to remember to still populate
|
||||
* anything about the relying party, like any private keys the relying party will use
|
||||
* for signing AuthnRequests.
|
||||
* @param location The classpath- or file-based locations or HTTP endpoints of the
|
||||
* @param location The classpath- or file-based locations or HTTPS endpoints of the
|
||||
* asserting party metadata file
|
||||
* @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
|
||||
* further configuration
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -55,6 +55,7 @@ import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.assertion.impl.ProxyRestrictionConditionValidator;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
@@ -837,6 +838,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
});
|
||||
conditions.add(new ProxyRestrictionConditionValidator());
|
||||
subjects.add(new BearerSubjectConfirmationValidator() {
|
||||
@Override
|
||||
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
|
||||
|
||||
+16
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -55,6 +55,7 @@ import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.OneTimeUse;
|
||||
import org.opensaml.saml.saml2.core.ProxyRestriction;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
||||
@@ -63,6 +64,7 @@ import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.ProxyRestrictionBuilder;
|
||||
import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.w3c.dom.Element;
|
||||
@@ -777,6 +779,19 @@ public class OpenSaml4AuthenticationProviderTests {
|
||||
.withMessageContaining("did not match any valid issuers");
|
||||
}
|
||||
|
||||
// gh-14931
|
||||
@Test
|
||||
public void authenticateWhenAssertionHasProxyRestrictionThenParses() {
|
||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
ProxyRestriction condition = new ProxyRestrictionBuilder().buildObject();
|
||||
assertion.getConditions().getConditions().add(condition);
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
|
||||
provider.authenticate(token);
|
||||
}
|
||||
|
||||
private <T extends XMLObject> T build(QName qName) {
|
||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ changelog:
|
||||
labels: ["type: bug"]
|
||||
sort: "title"
|
||||
- title: ":hammer: Dependency Upgrades"
|
||||
emoji: ":hammer:"
|
||||
labels: ["type: dependency-upgrade"]
|
||||
sort: "title"
|
||||
- title: ":nut_and_bolt: Build Updates"
|
||||
labels: ["type: task", "in: build"]
|
||||
sort: "title"
|
||||
issues:
|
||||
exclude:
|
||||
labels: ["status: duplicate"]
|
||||
|
||||
+6
-2
@@ -5,8 +5,12 @@ pluginManagement {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.gradle.enterprise" version "3.11.4"
|
||||
id "io.spring.ge.conventions" version "0.0.16"
|
||||
id "com.gradle.develocity" version "3.17.6"
|
||||
id "io.spring.ge.conventions" version "0.0.17" apply false
|
||||
}
|
||||
|
||||
if (System.getenv("DEVELOCITY_ACCESS_KEY")) {
|
||||
apply plugin: "io.spring.ge.conventions"
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
|
||||
+8
-2
@@ -30,6 +30,7 @@ import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
|
||||
@@ -67,7 +68,7 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Authorizing %s", request));
|
||||
this.logger.trace(LogMessage.format("Authorizing %s", requestLine(request)));
|
||||
}
|
||||
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
|
||||
|
||||
@@ -76,7 +77,8 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
if (matchResult.isMatch()) {
|
||||
AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager));
|
||||
this.logger.trace(
|
||||
LogMessage.format("Checking authorization on %s using %s", requestLine(request), manager));
|
||||
}
|
||||
return manager.check(authentication,
|
||||
new RequestAuthorizationContext(request, matchResult.getVariables()));
|
||||
@@ -86,6 +88,10 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String requestLine(HttpServletRequest request) {
|
||||
return request.getMethod() + " " + UrlUtils.buildRequestUrl(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
|
||||
* @return the new {@link Builder} instance
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's
|
||||
* <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's <a href=
|
||||
* "https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* Persistent Login Cookie Best Practice</a>.
|
||||
*
|
||||
* There is a slight modification to the described approach, in that the username is not
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -84,17 +84,16 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
@@ -114,9 +113,10 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -77,17 +77,16 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
@@ -107,9 +106,10 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+56
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,6 +44,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*/
|
||||
public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -203,6 +206,58 @@ public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
assertThat(tokenValue).isEqualTo(this.token.getToken());
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), "");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
return (invocation) -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
|
||||
+76
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,6 +46,9 @@ import static org.mockito.Mockito.verify;
|
||||
*/
|
||||
public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -188,6 +191,78 @@ public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), ""))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
return (invocation) -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
|
||||
Reference in New Issue
Block a user