Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b97c310aba | |||
| 59461d94b0 | |||
| fc007aa373 | |||
| eaaa813ede | |||
| 10b3a9b234 | |||
| e4cc0a4755 | |||
| ff71ef46b7 | |||
| 5593655bbf | |||
| 47812e506f | |||
| 89fd30f235 | |||
| c3c068376a | |||
| 6d68f403fc | |||
| 74d06f020d | |||
| e896b14046 | |||
| be11812fe4 | |||
| 9c44b700f5 | |||
| 40e8b20257 | |||
| c336ca49fb | |||
| c623303ca5 | |||
| a98baa7522 | |||
| bb0987db80 | |||
| d961307044 | |||
| 4ca54683ae | |||
| a7da9491d9 | |||
| 382cfd63ed | |||
| 7c98a39770 | |||
| 6afcc02ef3 | |||
| 773673ac2c | |||
| 7335c5745c | |||
| 52675c80b3 | |||
| b919ece045 | |||
| 11a21896dd | |||
| eff9814d7b | |||
| 058495d463 | |||
| ffd12ee3b9 | |||
| 3f64c6d745 | |||
| 2cd302fb1b | |||
| e8ec49b4c5 | |||
| 602d4189d1 | |||
| 884014c2fb | |||
| 47969ec7c9 | |||
| 6654479649 | |||
| bfc31bacab | |||
| ad2bea3350 | |||
| 5161712c35 | |||
| a6b872dcf3 | |||
| 70ad3bf749 | |||
| 19c4e427ee | |||
| 736f1c27cd | |||
| ec58bf0b28 | |||
| e4e31f2c90 |
@@ -31,7 +31,7 @@ jobs:
|
||||
runjobs: ${{ steps.continue.outputs.runjobs }}
|
||||
project_version: ${{ steps.continue.outputs.project_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- id: continue
|
||||
name: Determine if should continue
|
||||
run: |
|
||||
@@ -49,9 +49,9 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -72,9 +72,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -90,9 +90,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -114,9 +114,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -131,9 +131,9 @@ jobs:
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -155,9 +155,9 @@ jobs:
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -176,9 +176,9 @@ jobs:
|
||||
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -206,11 +206,11 @@ jobs:
|
||||
TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ needs.prerequisites.outputs.project_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
./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.19.0
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
@@ -287,9 +287,9 @@ jobs:
|
||||
TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ needs.prerequisites.outputs.project_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
uses: Gamesight/slack-workflow-status@v1.0.1
|
||||
uses: Gamesight/slack-workflow-status@v1.2.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -2,6 +2,11 @@ name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -11,9 +16,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
|
||||
@@ -23,11 +23,11 @@ jobs:
|
||||
steps:
|
||||
- id: checkout-source
|
||||
name: Checkout Source Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v1
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- id: send-slack-notification
|
||||
name: Send Slack message
|
||||
if: failure()
|
||||
uses: Gamesight/slack-workflow-status@v1.0.1
|
||||
uses: Gamesight/slack-workflow-status@v1.2.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
+1
-53
@@ -19,12 +19,12 @@ apply plugin: 'locks'
|
||||
apply plugin: 's101'
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
apply plugin: 'org.jetbrains.kotlin.jvm'
|
||||
apply plugin: 'org.springframework.security.update-dependencies'
|
||||
apply plugin: 'org.springframework.security.update-version'
|
||||
apply plugin: 'org.springframework.security.sagan'
|
||||
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'
|
||||
|
||||
group = 'org.springframework.security'
|
||||
description = 'Spring Security'
|
||||
@@ -86,58 +86,6 @@ tasks.named("dispatchGitHubWorkflow") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("updateDependencies") {
|
||||
// we aren't Gradle 7 compatible yet
|
||||
checkForGradleUpdate = false
|
||||
}
|
||||
|
||||
updateDependenciesSettings {
|
||||
gitHub {
|
||||
organization = "spring-projects"
|
||||
repository = "spring-security"
|
||||
}
|
||||
addFiles({
|
||||
return [
|
||||
project.file("buildSrc/src/main/groovy/io/spring/gradle/convention/CheckstylePlugin.groovy")
|
||||
]
|
||||
})
|
||||
dependencyExcludes {
|
||||
majorVersionBump()
|
||||
minorVersionBump()
|
||||
releaseCandidatesVersions()
|
||||
alphaBetaVersions()
|
||||
snapshotVersions()
|
||||
addRule { components ->
|
||||
components.withModule("org.python:jython") { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
selection.reject("jython updates break integration tests");
|
||||
}
|
||||
}
|
||||
components.withModule("com.nimbusds:nimbus-jose-jwt") { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
selection.reject("nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency");
|
||||
}
|
||||
}
|
||||
components.withModule("io.mockk:mockk") { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
selection.reject("mockk updates break tests");
|
||||
}
|
||||
}
|
||||
components.all { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
// Do not compare version due to multiple versions existing
|
||||
// will cause opensaml 3.x to be updated to 4.x
|
||||
if (candidate.getGroup().equals("org.opensaml")) {
|
||||
selection.reject("org.opensaml maintains two different versions, so it must be updated manually");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
plugins.withType(JavaPlugin) {
|
||||
project.sourceCompatibility='1.8'
|
||||
|
||||
@@ -2,7 +2,6 @@ plugins {
|
||||
id "java-gradle-plugin"
|
||||
id "java"
|
||||
id "groovy"
|
||||
id 'com.apollographql.apollo' version '2.4.5'
|
||||
}
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
@@ -41,10 +40,6 @@ gradlePlugin {
|
||||
id = "io.spring.convention.management-configuration"
|
||||
implementationClass = "io.spring.gradle.convention.ManagementConfigurationPlugin"
|
||||
}
|
||||
updateDependencies {
|
||||
id = "org.springframework.security.update-dependencies"
|
||||
implementationClass = "org.springframework.security.convention.versions.UpdateDependenciesPlugin"
|
||||
}
|
||||
updateProjectVersion {
|
||||
id = "org.springframework.security.update-version"
|
||||
implementationClass = "org.springframework.security.convention.versions.UpdateProjectVersionPlugin"
|
||||
@@ -69,6 +64,10 @@ gradlePlugin {
|
||||
id = "s101"
|
||||
implementationClass = "s101.S101Plugin"
|
||||
}
|
||||
verifyDependenciesVersions {
|
||||
id = "org.springframework.security.versions.verify-dependencies-versions"
|
||||
implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +89,6 @@ dependencies {
|
||||
implementation libs.io.github.gradle.nexus.publish.plugin
|
||||
implementation 'io.projectreactor:reactor-core'
|
||||
implementation libs.org.gretty.gretty
|
||||
implementation libs.com.apollographql.apollo.apollo.runtime
|
||||
implementation libs.com.github.ben.manes.gradle.versions.plugin
|
||||
implementation libs.com.github.spullara.mustache.java.compiler
|
||||
implementation libs.io.spring.javaformat.spring.javaformat.gradle.plugin
|
||||
@@ -99,6 +97,7 @@ dependencies {
|
||||
implementation libs.org.hidetake.gradle.ssh.plugin
|
||||
implementation libs.org.jfrog.buildinfo.build.info.extractor.gradle
|
||||
implementation libs.org.sonarsource.scanner.gradle.sonarqube.gradle.plugin
|
||||
implementation libs.com.squareup.okhttp3.okhttp
|
||||
|
||||
testImplementation platform(libs.org.junit.junit.bom)
|
||||
testImplementation platform(libs.org.mockito.mockito.bom)
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.convention.versions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Scanner;
|
||||
|
||||
class CommandLineUtils {
|
||||
static void runCommand(File dir, String... args) {
|
||||
try {
|
||||
Process process = new ProcessBuilder()
|
||||
.directory(dir)
|
||||
.command(args)
|
||||
.start();
|
||||
writeLinesTo(process.getInputStream(), System.out);
|
||||
writeLinesTo(process.getErrorStream(), System.out);
|
||||
if (process.waitFor() != 0) {
|
||||
new RuntimeException("Failed to run " + Arrays.toString(args));
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new RuntimeException("Failed to run " + Arrays.toString(args), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeLinesTo(InputStream input, PrintStream out) {
|
||||
Scanner scanner = new Scanner(input);
|
||||
while(scanner.hasNextLine()) {
|
||||
out.println(scanner.nextLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
package org.springframework.security.convention.versions;
|
||||
|
||||
import com.apollographql.apollo.ApolloCall;
|
||||
import com.apollographql.apollo.ApolloClient;
|
||||
import com.apollographql.apollo.api.Input;
|
||||
import com.apollographql.apollo.api.Response;
|
||||
import com.apollographql.apollo.exception.ApolloException;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetrySpec;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GitHubApi {
|
||||
|
||||
private final ApolloClient apolloClient;
|
||||
|
||||
public GitHubApi(String githubToken) {
|
||||
if (githubToken == null) {
|
||||
throw new IllegalArgumentException("githubToken is required. You can set it using -PgitHubAccessToken=");
|
||||
}
|
||||
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
|
||||
clientBuilder.addInterceptor(new AuthorizationInterceptor(githubToken));
|
||||
this.apolloClient = ApolloClient.builder()
|
||||
.serverUrl("https://api.github.com/graphql")
|
||||
.okHttpClient(clientBuilder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public Mono<FindCreateIssueResult> findCreateIssueInput(String owner, String name, String milestone) {
|
||||
String label = "\"type: dependency-upgrade\"";
|
||||
FindCreateIssueInputQuery findCreateIssueInputQuery = new FindCreateIssueInputQuery(owner, name, Input.optional(label), Input.optional(milestone));
|
||||
return Mono.create( sink -> this.apolloClient.query(findCreateIssueInputQuery)
|
||||
.enqueue(new ApolloCall.Callback<FindCreateIssueInputQuery.Data>() {
|
||||
@Override
|
||||
public void onResponse(@NotNull Response<FindCreateIssueInputQuery.Data> response) {
|
||||
if (response.hasErrors()) {
|
||||
sink.error(new RuntimeException(response.getErrors().stream().map(e -> e.getMessage()).collect(Collectors.joining(" "))));
|
||||
} else {
|
||||
FindCreateIssueInputQuery.Data data = response.getData();
|
||||
FindCreateIssueInputQuery.Repository repository = data.repository();
|
||||
List<String> labels = repository.labels().nodes().stream().map(FindCreateIssueInputQuery.Node::id).collect(Collectors.toList());
|
||||
if (labels.isEmpty()) {
|
||||
sink.error(new IllegalArgumentException("Could not find label for " + label));
|
||||
return;
|
||||
}
|
||||
Optional<String> firstMilestoneId = repository.milestones().nodes().stream().map(FindCreateIssueInputQuery.Node1::id).findFirst();
|
||||
if (!firstMilestoneId.isPresent()) {
|
||||
sink.error(new IllegalArgumentException("Could not find OPEN milestone id for " + milestone));
|
||||
return;
|
||||
}
|
||||
String milestoneId = firstMilestoneId.get();
|
||||
String repositoryId = repository.id();
|
||||
String assigneeId = data.viewer().id();
|
||||
sink.success(new FindCreateIssueResult(repositoryId, labels, milestoneId, assigneeId));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NotNull ApolloException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static class FindCreateIssueResult {
|
||||
private final String repositoryId;
|
||||
private final List<String> labelIds;
|
||||
private final String milestoneId;
|
||||
private final String assigneeId;
|
||||
|
||||
public FindCreateIssueResult(String repositoryId, List<String> labelIds, String milestoneId, String assigneeId) {
|
||||
this.repositoryId = repositoryId;
|
||||
this.labelIds = labelIds;
|
||||
this.milestoneId = milestoneId;
|
||||
this.assigneeId = assigneeId;
|
||||
}
|
||||
|
||||
public String getRepositoryId() {
|
||||
return repositoryId;
|
||||
}
|
||||
|
||||
public List<String> getLabelIds() {
|
||||
return labelIds;
|
||||
}
|
||||
|
||||
public String getMilestoneId() {
|
||||
return milestoneId;
|
||||
}
|
||||
|
||||
public String getAssigneeId() {
|
||||
return assigneeId;
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<RateLimitQuery.RateLimit> findRateLimit() {
|
||||
return Mono.create( sink -> this.apolloClient.query(new RateLimitQuery())
|
||||
.enqueue(new ApolloCall.Callback<RateLimitQuery.Data>() {
|
||||
@Override
|
||||
public void onResponse(@NotNull Response<RateLimitQuery.Data> response) {
|
||||
if (response.hasErrors()) {
|
||||
sink.error(new RuntimeException(response.getErrors().stream().map(e -> e.getMessage()).collect(Collectors.joining(" "))));
|
||||
} else {
|
||||
sink.success(response.getData().rateLimit());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NotNull ApolloException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public Mono<Integer> createIssue(String repositoryId, String title, List<String> labelIds, String milestoneId, String assigneeId) {
|
||||
CreateIssueInputMutation createIssue = new CreateIssueInputMutation.Builder()
|
||||
.repositoryId(repositoryId)
|
||||
.title(title)
|
||||
.labelIds(labelIds)
|
||||
.milestoneId(milestoneId)
|
||||
.assigneeId(assigneeId)
|
||||
.build();
|
||||
return Mono.create( sink -> this.apolloClient.mutate(createIssue)
|
||||
.enqueue(new ApolloCall.Callback<CreateIssueInputMutation.Data>() {
|
||||
@Override
|
||||
public void onResponse(@NotNull Response<CreateIssueInputMutation.Data> response) {
|
||||
if (response.hasErrors()) {
|
||||
String message = response.getErrors().stream().map(e -> e.getMessage() + " " + e.getCustomAttributes() + " " + e.getLocations()).collect(Collectors.joining(" "));
|
||||
if (message.contains("was submitted too quickly")) {
|
||||
sink.error(new SubmittedTooQuick(message));
|
||||
} else {
|
||||
sink.error(new RuntimeException(message));
|
||||
}
|
||||
} else {
|
||||
sink.success(response.getData().createIssue().issue().number());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NotNull ApolloException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
}))
|
||||
.retryWhen(
|
||||
RetrySpec.fixedDelay(3, Duration.ofMinutes(1))
|
||||
.filter(SubmittedTooQuick.class::isInstance)
|
||||
.doBeforeRetry(r -> System.out.println("Pausing for 1 minute and then retrying due to receiving \"submitted too quickly\" error from GitHub API"))
|
||||
)
|
||||
.cast(Integer.class);
|
||||
}
|
||||
|
||||
public static class SubmittedTooQuick extends RuntimeException {
|
||||
public SubmittedTooQuick(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthorizationInterceptor implements Interceptor {
|
||||
|
||||
private final String token;
|
||||
|
||||
public AuthorizationInterceptor(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public okhttp3.Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request().newBuilder()
|
||||
.addHeader("Authorization", "Bearer " + this.token).build();
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
package org.springframework.security.convention.versions;
|
||||
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionRulesWithCurrent;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ResolutionStrategyWithCurrent;
|
||||
import org.gradle.api.Action;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UpdateDependenciesExtension {
|
||||
private Supplier<List<File>> files;
|
||||
|
||||
private UpdateMode updateMode = UpdateMode.COMMIT;
|
||||
|
||||
private DependencyExcludes dependencyExcludes = new DependencyExcludes();
|
||||
|
||||
private GitHub gitHub = new GitHub();
|
||||
|
||||
public UpdateDependenciesExtension(Supplier<List<File>> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public void setUpdateMode(UpdateMode updateMode) {
|
||||
this.updateMode = updateMode;
|
||||
}
|
||||
|
||||
public UpdateMode getUpdateMode() {
|
||||
return updateMode;
|
||||
}
|
||||
|
||||
GitHub getGitHub() {
|
||||
return this.gitHub;
|
||||
}
|
||||
|
||||
DependencyExcludes getExcludes() {
|
||||
return dependencyExcludes;
|
||||
}
|
||||
|
||||
Supplier<List<File>> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(Supplier<List<File>> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public void addFiles(Supplier<List<File>> files) {
|
||||
Supplier<List<File>> original = this.files;
|
||||
setFiles(() -> {
|
||||
List<File> result = new ArrayList<>(original.get());
|
||||
result.addAll(files.get());
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public void dependencyExcludes(Action<DependencyExcludes> excludes) {
|
||||
excludes.execute(this.dependencyExcludes);
|
||||
}
|
||||
|
||||
public void gitHub(Action<GitHub> gitHub) {
|
||||
gitHub.execute(this.gitHub);
|
||||
}
|
||||
|
||||
public enum UpdateMode {
|
||||
COMMIT,
|
||||
GITHUB_ISSUE
|
||||
}
|
||||
|
||||
public class GitHub {
|
||||
private String organization;
|
||||
|
||||
private String repository;
|
||||
|
||||
private String accessToken;
|
||||
|
||||
private String milestone;
|
||||
|
||||
public String getOrganization() {
|
||||
return organization;
|
||||
}
|
||||
|
||||
public void setOrganization(String organization) {
|
||||
this.organization = organization;
|
||||
}
|
||||
|
||||
public String getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void setRepository(String repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public String getMilestone() {
|
||||
return milestone;
|
||||
}
|
||||
|
||||
public void setMilestone(String milestone) {
|
||||
this.milestone = milestone;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider creating some Predicates instead since they are composible
|
||||
*/
|
||||
public class DependencyExcludes {
|
||||
private List<Action<ComponentSelectionWithCurrent>> actions = new ArrayList<>();
|
||||
private List<Action<ComponentSelectionRulesWithCurrent>> components = new ArrayList<>();
|
||||
|
||||
List<Action<ComponentSelectionWithCurrent>> getActions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
public List<Action<ComponentSelectionRulesWithCurrent>> getComponents() {
|
||||
return components;
|
||||
}
|
||||
|
||||
public DependencyExcludes alphaBetaVersions() {
|
||||
this.actions.add(excludeVersionWithRegex("(?i).*?(alpha|beta).*", "an alpha or beta version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes majorVersionBump() {
|
||||
this.actions.add((selection) -> {
|
||||
String currentVersion = selection.getCurrentVersion();
|
||||
int separator = currentVersion.indexOf(".");
|
||||
String major = separator > 0 ? currentVersion.substring(0, separator) : currentVersion;
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
Pattern calVerPattern = Pattern.compile("\\d\\d\\d\\d.*");
|
||||
boolean isCalVer = calVerPattern.matcher(candidateVersion).matches();
|
||||
if (!isCalVer && !candidateVersion.startsWith(major)) {
|
||||
selection.reject("Cannot upgrade to new Major Version");
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes minorVersionBump() {
|
||||
this.actions.add(createExcludeMinorVersionBump());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Action<ComponentSelectionWithCurrent> createExcludeMinorVersionBump() {
|
||||
return (selection) -> {
|
||||
String currentVersion = selection.getCurrentVersion();
|
||||
int majorSeparator = currentVersion.indexOf(".");
|
||||
int separator = currentVersion.indexOf(".", majorSeparator + 1);
|
||||
String majorMinor = separator > 0 ? currentVersion.substring(0, separator) : currentVersion;
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
if (!candidateVersion.startsWith(majorMinor)) {
|
||||
selection.reject("Cannot upgrade to new Minor Version");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public DependencyExcludes releaseCandidatesVersions() {
|
||||
this.actions.add(excludeVersionWithRegex("(?i).*?rc.*", "a release candidate version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes milestoneVersions() {
|
||||
this.actions.add(excludeVersionWithRegex("(?i).*?m\\d+.*", "a milestone version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes snapshotVersions() {
|
||||
this.actions.add(excludeVersionWithRegex(".*?-SNAPSHOT.*", "a SNAPSHOT version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes addRule(Action<ComponentSelectionRulesWithCurrent> rule) {
|
||||
this.components.add(rule);
|
||||
return this;
|
||||
}
|
||||
|
||||
private Action<ComponentSelectionWithCurrent> excludeVersionWithRegex(String regex, String reason) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
return (selection) -> {
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
if (pattern.matcher(candidateVersion).matches()) {
|
||||
selection.reject(candidateVersion + " is not allowed because it is " + reason);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
-244
@@ -1,244 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.convention.versions;
|
||||
|
||||
import com.github.benmanes.gradle.versions.reporter.result.Dependency;
|
||||
import com.github.benmanes.gradle.versions.reporter.result.DependencyOutdated;
|
||||
import com.github.benmanes.gradle.versions.reporter.result.Result;
|
||||
import com.github.benmanes.gradle.versions.reporter.result.VersionAvailable;
|
||||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask;
|
||||
import com.github.benmanes.gradle.versions.updates.gradle.GradleUpdateResult;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionRulesWithCurrent;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ResolutionStrategyWithCurrent;
|
||||
import groovy.lang.Closure;
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.springframework.security.convention.versions.TransitiveDependencyLookupUtils.NIMBUS_JOSE_JWT_NAME;
|
||||
import static org.springframework.security.convention.versions.TransitiveDependencyLookupUtils.OIDC_SDK_NAME;
|
||||
|
||||
public class UpdateDependenciesPlugin implements Plugin<Project> {
|
||||
private GitHubApi gitHubApi;
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
UpdateDependenciesExtension updateDependenciesSettings = project.getExtensions().create("updateDependenciesSettings", UpdateDependenciesExtension.class, defaultFiles(project));
|
||||
if (project.hasProperty("updateMode")) {
|
||||
String updateMode = String.valueOf(project.findProperty("updateMode"));
|
||||
updateDependenciesSettings.setUpdateMode(UpdateDependenciesExtension.UpdateMode.valueOf(updateMode));
|
||||
}
|
||||
if (project.hasProperty("nextVersion")) {
|
||||
String nextVersion = String.valueOf(project.findProperty("nextVersion"));
|
||||
updateDependenciesSettings.getGitHub().setMilestone(nextVersion);
|
||||
}
|
||||
if (project.hasProperty("gitHubAccessToken")) {
|
||||
String gitHubAccessToken = String.valueOf(project.findProperty("gitHubAccessToken"));
|
||||
updateDependenciesSettings.getGitHub().setAccessToken(gitHubAccessToken);
|
||||
}
|
||||
project.getTasks().register("updateDependencies", DependencyUpdatesTask.class, new Action<DependencyUpdatesTask>() {
|
||||
@Override
|
||||
public void execute(DependencyUpdatesTask updateDependencies) {
|
||||
updateDependencies.setDescription("Update the dependencies");
|
||||
updateDependencies.setCheckConstraints(true);
|
||||
updateDependencies.setOutputFormatter(new Closure<Void>(null) {
|
||||
@Override
|
||||
public Void call(Object argument) {
|
||||
Result result = (Result) argument;
|
||||
if (gitHubApi == null && updateDependenciesSettings.getUpdateMode() != UpdateDependenciesExtension.UpdateMode.COMMIT) {
|
||||
gitHubApi = new GitHubApi(updateDependenciesSettings.getGitHub().getAccessToken());
|
||||
}
|
||||
updateDependencies(result, project, updateDependenciesSettings);
|
||||
updateGradleVersion(result, project, updateDependenciesSettings);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
updateDependencies.resolutionStrategy(new Action<ResolutionStrategyWithCurrent>() {
|
||||
@Override
|
||||
public void execute(ResolutionStrategyWithCurrent resolution) {
|
||||
resolution.componentSelection(new Action<ComponentSelectionRulesWithCurrent>() {
|
||||
@Override
|
||||
public void execute(ComponentSelectionRulesWithCurrent components) {
|
||||
updateDependenciesSettings.getExcludes().getActions().forEach((action) -> {
|
||||
components.all(action);
|
||||
});
|
||||
updateDependenciesSettings.getExcludes().getComponents().forEach((action) -> {
|
||||
action.execute(components);
|
||||
});
|
||||
components.all((selection) -> {
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (candidate.getGroup().startsWith("org.apache.directory.") && !candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
selection.reject("org.apache.directory.* has breaking changes in newer versions");
|
||||
}
|
||||
});
|
||||
String jaxbBetaRegex = ".*?b\\d+.*";
|
||||
components.withModule("javax.xml.bind:jaxb-api", excludeWithRegex(jaxbBetaRegex, "Reject jaxb-api beta versions"));
|
||||
components.withModule("com.sun.xml.bind:jaxb-impl", excludeWithRegex(jaxbBetaRegex, "Reject jaxb-api beta versions"));
|
||||
components.withModule("commons-collections:commons-collections", excludeWithRegex("^\\d{3,}.*", "Reject commons-collections date based releases"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDependencies(Result result, Project project, UpdateDependenciesExtension updateDependenciesSettings) {
|
||||
SortedSet<DependencyOutdated> dependencies = result.getOutdated().getDependencies();
|
||||
if (dependencies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, List<DependencyOutdated>> groups = new LinkedHashMap<>();
|
||||
dependencies.forEach(outdated -> {
|
||||
groups.computeIfAbsent(outdated.getGroup(), (key) -> new ArrayList<>()).add(outdated);
|
||||
});
|
||||
List<DependencyOutdated> nimbusds = groups.getOrDefault("com.nimbusds", new ArrayList<>());
|
||||
DependencyOutdated oidcSdc = nimbusds.stream().filter(d -> d.getName().equals(OIDC_SDK_NAME)).findFirst().orElseGet(() -> null);
|
||||
if(oidcSdc != null) {
|
||||
String oidcVersion = updatedVersion(oidcSdc);
|
||||
String jwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(oidcVersion);
|
||||
|
||||
Dependency nimbusJoseJwtDependency = result.getCurrent().getDependencies().stream().filter(d -> d.getName().equals(NIMBUS_JOSE_JWT_NAME)).findFirst().get();
|
||||
DependencyOutdated outdatedJwt = new DependencyOutdated();
|
||||
outdatedJwt.setVersion(nimbusJoseJwtDependency.getVersion());
|
||||
outdatedJwt.setGroup(oidcSdc.getGroup());
|
||||
outdatedJwt.setName(NIMBUS_JOSE_JWT_NAME);
|
||||
VersionAvailable available = new VersionAvailable();
|
||||
available.setRelease(jwtVersion);
|
||||
outdatedJwt.setAvailable(available);
|
||||
nimbusds.add(outdatedJwt);
|
||||
}
|
||||
File gradlePropertiesFile = project.getRootProject().file(Project.GRADLE_PROPERTIES);
|
||||
Mono<GitHubApi.FindCreateIssueResult> createIssueResult = createIssueResultMono(updateDependenciesSettings);
|
||||
List<File> filesWithDependencies = updateDependenciesSettings.getFiles().get();
|
||||
groups.forEach((group, outdated) -> {
|
||||
outdated.forEach((dependency) -> {
|
||||
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
|
||||
String originalDependency = ga + dependency.getVersion();
|
||||
String replacementDependency = ga + updatedVersion(dependency);
|
||||
System.out.println("Update " + originalDependency + " to " + replacementDependency);
|
||||
filesWithDependencies.forEach((fileWithDependency) -> {
|
||||
updateDependencyInlineVersion(fileWithDependency, dependency);
|
||||
updateDependencyWithVersionVariable(fileWithDependency, gradlePropertiesFile, dependency);
|
||||
});
|
||||
});
|
||||
|
||||
// commit
|
||||
DependencyOutdated firstDependency = outdated.get(0);
|
||||
String updatedVersion = updatedVersion(firstDependency);
|
||||
String title = outdated.size() == 1 ? "Update " + firstDependency.getName() + " to " + updatedVersion : "Update " + firstDependency.getGroup() + " to " + updatedVersion;
|
||||
afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResult);
|
||||
});
|
||||
}
|
||||
|
||||
private void afterGroup(UpdateDependenciesExtension updateDependenciesExtension, File rootDir, String title, Mono<GitHubApi.FindCreateIssueResult> createIssueResultMono) {
|
||||
|
||||
String commitMessage = title;
|
||||
if (updateDependenciesExtension.getUpdateMode() == UpdateDependenciesExtension.UpdateMode.GITHUB_ISSUE) {
|
||||
GitHubApi.FindCreateIssueResult createIssueResult = createIssueResultMono.block();
|
||||
Integer issueNumber = gitHubApi.createIssue(createIssueResult.getRepositoryId(), title, createIssueResult.getLabelIds(), createIssueResult.getMilestoneId(), createIssueResult.getAssigneeId()).delayElement(Duration.ofSeconds(1)).block();
|
||||
commitMessage += "\n\nCloses gh-" + issueNumber;
|
||||
}
|
||||
CommandLineUtils.runCommand(rootDir, "git", "commit", "-am", commitMessage);
|
||||
}
|
||||
|
||||
private Mono<GitHubApi.FindCreateIssueResult> createIssueResultMono(UpdateDependenciesExtension updateDependenciesExtension) {
|
||||
return Mono.defer(() -> {
|
||||
UpdateDependenciesExtension.GitHub gitHub = updateDependenciesExtension.getGitHub();
|
||||
return gitHubApi.findCreateIssueInput(gitHub.getOrganization(), gitHub.getRepository(), gitHub.getMilestone()).cache();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateGradleVersion(Result result, Project project, UpdateDependenciesExtension updateDependenciesSettings) {
|
||||
if (!result.getGradle().isEnabled()) {
|
||||
return;
|
||||
}
|
||||
GradleUpdateResult current = result.getGradle().getCurrent();
|
||||
GradleUpdateResult running = result.getGradle().getRunning();
|
||||
if (current.compareTo(running) > 0) {
|
||||
String title = "Update Gradle to " + current.getVersion();
|
||||
System.out.println(title);
|
||||
CommandLineUtils.runCommand(project.getRootDir(), "./gradlew", "wrapper", "--gradle-version", current.getVersion(), "--no-daemon");
|
||||
afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResultMono(updateDependenciesSettings));
|
||||
}
|
||||
}
|
||||
|
||||
private static Supplier<List<File>> defaultFiles(Project project) {
|
||||
return () -> {
|
||||
List<File> result = new ArrayList<>();
|
||||
result.add(project.getBuildFile());
|
||||
project.getChildProjects().values().forEach((childProject) ->
|
||||
result.add(childProject.getBuildFile())
|
||||
);
|
||||
result.add(project.getRootProject().file("buildSrc/build.gradle"));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
static Action<ComponentSelectionWithCurrent> excludeWithRegex(String regex, String reason) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
return (selection) -> {
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
if (pattern.matcher(candidateVersion).matches()) {
|
||||
selection.reject(candidateVersion + " is not allowed because it is " + reason);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static void updateDependencyInlineVersion(File buildFile, DependencyOutdated dependency){
|
||||
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
|
||||
String originalDependency = ga + dependency.getVersion();
|
||||
String replacementDependency = ga + updatedVersion(dependency);
|
||||
FileUtils.replaceFileText(buildFile, buildFileText -> buildFileText.replace(originalDependency, replacementDependency));
|
||||
}
|
||||
|
||||
static void updateDependencyWithVersionVariable(File scanFile, File gradlePropertiesFile, DependencyOutdated dependency) {
|
||||
if (!gradlePropertiesFile.exists()) {
|
||||
return;
|
||||
}
|
||||
FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> {
|
||||
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
|
||||
Pattern pattern = Pattern.compile("\"" + ga + "\\$\\{?([^'\"]+?)\\}?\"");
|
||||
String buildFileText = FileUtils.readString(scanFile);
|
||||
Matcher matcher = pattern.matcher(buildFileText);
|
||||
while (matcher.find()) {
|
||||
String versionVariable = matcher.group(1);
|
||||
gradlePropertiesText = gradlePropertiesText.replace(versionVariable + "=" + dependency.getVersion(), versionVariable + "=" + updatedVersion(dependency));
|
||||
}
|
||||
return gradlePropertiesText;
|
||||
});
|
||||
}
|
||||
|
||||
private static String updatedVersion(DependencyOutdated dependency) {
|
||||
VersionAvailable available = dependency.getAvailable();
|
||||
String release = available.getRelease();
|
||||
if (release != null) {
|
||||
return release;
|
||||
}
|
||||
return available.getMilestone();
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.convention.versions;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
|
||||
import org.gradle.api.artifacts.VersionCatalog;
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
|
||||
public class VerifyDependenciesVersionsPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
TaskProvider<VerifyDependenciesVersionsTask> verifyDependenciesVersionsTaskProvider = project.getTasks().register("verifyDependenciesVersions", VerifyDependenciesVersionsTask.class, (task) -> {
|
||||
task.setGroup("Verification");
|
||||
task.setDescription("Verify that specific dependencies are using the same version");
|
||||
VersionCatalog versionCatalog = project.getExtensions().getByType(VersionCatalogsExtension.class).named("libs");
|
||||
MinimalExternalModuleDependency oauth2OidcSdk = versionCatalog.findLibrary("com-nimbusds-oauth2-oidc-sdk").get().get();
|
||||
MinimalExternalModuleDependency nimbusJoseJwt = versionCatalog.findLibrary("com-nimbusds-nimbus-jose-jwt").get().get();
|
||||
task.setOauth2OidcSdkVersion(oauth2OidcSdk.getVersionConstraint().getDisplayName());
|
||||
task.setExpectedNimbusJoseJwtVersion(nimbusJoseJwt.getVersionConstraint().getDisplayName());
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(verifyDependenciesVersionsTaskProvider));
|
||||
}
|
||||
|
||||
public static class VerifyDependenciesVersionsTask extends DefaultTask {
|
||||
|
||||
private String oauth2OidcSdkVersion;
|
||||
|
||||
private String expectedNimbusJoseJwtVersion;
|
||||
|
||||
public void setOauth2OidcSdkVersion(String oauth2OidcSdkVersion) {
|
||||
this.oauth2OidcSdkVersion = oauth2OidcSdkVersion;
|
||||
}
|
||||
|
||||
public void setExpectedNimbusJoseJwtVersion(String expectedNimbusJoseJwtVersion) {
|
||||
this.expectedNimbusJoseJwtVersion = expectedNimbusJoseJwtVersion;
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void verify() {
|
||||
String transitiveNimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(this.oauth2OidcSdkVersion);
|
||||
if (!transitiveNimbusJoseJwtVersion.equals(this.expectedNimbusJoseJwtVersion)) {
|
||||
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, but the project contains a different version of nimbus-jose-jwt [%s]. Please align the versions.", transitiveNimbusJoseJwtVersion, this.oauth2OidcSdkVersion, this.expectedNimbusJoseJwtVersion);
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.convention.versions;
|
||||
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.artifacts.ComponentSelection;
|
||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class DependencyExcludesTests {
|
||||
|
||||
@Test
|
||||
public void createExcludeMinorVersionBumpWhenMajorVersionBumpThenReject() {
|
||||
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "2.0.0");
|
||||
verify(componentSelection).reject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createExcludeMinorVersionBumpWhenMajorCalVersionBumpThenReject() {
|
||||
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("2000.0.0", "2001.0.0");
|
||||
verify(componentSelection).reject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createExcludeMinorVersionBumpWhenMinorVersionBumpThenReject() {
|
||||
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "1.1.0");
|
||||
verify(componentSelection).reject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createExcludeMinorVersionBumpWhenMinorCalVersionBumpThenReject() {
|
||||
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("2000.0.0", "2000.1.0");
|
||||
verify(componentSelection).reject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createExcludeMinorVersionBumpWhenMinorAndPatchVersionBumpThenReject() {
|
||||
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "1.1.1");
|
||||
verify(componentSelection).reject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createExcludeMinorVersionBumpWhenPatchVersionBumpThenDoesNotReject() {
|
||||
ComponentSelection componentSelection = executeCreateExcludeMinorVersionBump("1.0.0", "1.0.1");
|
||||
verify(componentSelection, times(0)).reject(any());
|
||||
}
|
||||
|
||||
private ComponentSelection executeCreateExcludeMinorVersionBump(String currentVersion, String candidateVersion) {
|
||||
ComponentSelection componentSelection = mock(ComponentSelection.class);
|
||||
UpdateDependenciesExtension.DependencyExcludes excludes = new UpdateDependenciesExtension(() -> Collections.emptyList()).new DependencyExcludes();
|
||||
Action<ComponentSelectionWithCurrent> excludeMinorVersionBump = excludes.createExcludeMinorVersionBump();
|
||||
ComponentSelectionWithCurrent selection = currentVersionAndCandidateVersion(componentSelection, currentVersion, candidateVersion);
|
||||
excludeMinorVersionBump.execute(selection);
|
||||
return componentSelection;
|
||||
}
|
||||
|
||||
private ComponentSelectionWithCurrent currentVersionAndCandidateVersion(ComponentSelection componentSelection, String currentVersion, String candidateVersion) {
|
||||
ModuleComponentIdentifier candidate = mock(ModuleComponentIdentifier.class);
|
||||
given(componentSelection.getCandidate()).willReturn(candidate);
|
||||
ComponentSelectionWithCurrent selection = new ComponentSelectionWithCurrent(currentVersion, componentSelection);
|
||||
given(candidate.getVersion()).willReturn(candidateVersion);
|
||||
given(componentSelection.getCandidate()).willReturn(candidate);
|
||||
return selection;
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.convention.versions;
|
||||
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class TransitiveDependencyLookupUtilsTest {
|
||||
|
||||
@Test
|
||||
public void lookupJwtVersionWhen93Then961() {
|
||||
String s = TransitiveDependencyLookupUtils.lookupJwtVersion("9.3");
|
||||
assertThat(s).isEqualTo("9.6.1");
|
||||
}
|
||||
}
|
||||
+205
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,10 +22,16 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -72,6 +78,8 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
AbstractRequestMatcherRegistry.class.getClassLoader());
|
||||
}
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
protected final void setApplicationContext(ApplicationContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
@@ -314,18 +322,59 @@ 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));
|
||||
}
|
||||
}
|
||||
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 requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
return ant;
|
||||
}
|
||||
if (!hasDispatcherServlet(registrations)) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
return ant;
|
||||
}
|
||||
if (registrations.size() > 1) {
|
||||
String errorMessage = computeErrorMessage(registrations.values());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
ServletRegistration dispatcherServlet = requireOneRootDispatcherServlet(registrations);
|
||||
if (dispatcherServlet != null) {
|
||||
if (registrations.size() == 1) {
|
||||
return mvc;
|
||||
}
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, servletContext);
|
||||
}
|
||||
return requestMatchers(createMvcMatchers(method, patterns).toArray(new RequestMatcher[0]));
|
||||
dispatcherServlet = requireOnlyPathMappedDispatcherServlet(registrations);
|
||||
if (dispatcherServlet != null) {
|
||||
String mapping = dispatcherServlet.getMappings().iterator().next();
|
||||
mvc.setServletPath(mapping.substring(0, mapping.length() - 2));
|
||||
return mvc;
|
||||
}
|
||||
String errorMessage = computeErrorMessage(registrations.values());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
}
|
||||
|
||||
private Map<String, ? extends ServletRegistration> mappableServletRegistrations(ServletContext servletContext) {
|
||||
@@ -343,22 +392,66 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
if (registrations == null) {
|
||||
return false;
|
||||
}
|
||||
Class<?> dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet",
|
||||
null);
|
||||
for (ServletRegistration registration : registrations.values()) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
if (dispatcherServlet.isAssignableFrom(clazz)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
if (isDispatcherServlet(registration)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ServletRegistration requireOneRootDispatcherServlet(
|
||||
Map<String, ? extends ServletRegistration> registrations) {
|
||||
ServletRegistration rootDispatcherServlet = null;
|
||||
for (ServletRegistration registration : registrations.values()) {
|
||||
if (!isDispatcherServlet(registration)) {
|
||||
continue;
|
||||
}
|
||||
if (registration.getMappings().size() > 1) {
|
||||
return null;
|
||||
}
|
||||
if (!"/".equals(registration.getMappings().iterator().next())) {
|
||||
return null;
|
||||
}
|
||||
rootDispatcherServlet = registration;
|
||||
}
|
||||
return rootDispatcherServlet;
|
||||
}
|
||||
|
||||
private ServletRegistration requireOnlyPathMappedDispatcherServlet(
|
||||
Map<String, ? extends ServletRegistration> registrations) {
|
||||
ServletRegistration pathDispatcherServlet = null;
|
||||
for (ServletRegistration registration : registrations.values()) {
|
||||
if (!isDispatcherServlet(registration)) {
|
||||
return null;
|
||||
}
|
||||
if (registration.getMappings().size() > 1) {
|
||||
return null;
|
||||
}
|
||||
String mapping = registration.getMappings().iterator().next();
|
||||
if (!mapping.startsWith("/") || !mapping.endsWith("/*")) {
|
||||
return null;
|
||||
}
|
||||
if (pathDispatcherServlet != null) {
|
||||
return null;
|
||||
}
|
||||
pathDispatcherServlet = registration;
|
||||
}
|
||||
return pathDispatcherServlet;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private 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); "
|
||||
@@ -498,4 +591,99 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
}
|
||||
|
||||
static class DeferredRequestMatcher implements RequestMatcher {
|
||||
|
||||
final Function<HttpServletRequest, RequestMatcher> requestMatcherFactory;
|
||||
|
||||
final AtomicReference<String> description = new AtomicReference<>();
|
||||
|
||||
volatile RequestMatcher requestMatcher;
|
||||
|
||||
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;
|
||||
};
|
||||
this.description.set("Deferred " + Arrays.toString(candidates));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.description.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final AntPathRequestMatcher ant;
|
||||
|
||||
private final MvcRequestMatcher mvc;
|
||||
|
||||
private final ServletContext servletContext;
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
ServletContext servletContext) {
|
||||
this.ant = ant;
|
||||
this.mvc = mvc;
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DispatcherServletDelegating [" + "ant = " + this.ant + ", mvc = " + this.mvc + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -801,7 +801,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* replaced with "#". For example:
|
||||
*
|
||||
* <pre>
|
||||
* X-XSS-Protection: 1 ; mode=block
|
||||
* X-XSS-Protection: 1; mode=block
|
||||
* </pre>
|
||||
* @param headerValue the new header value
|
||||
* @since 5.8
|
||||
|
||||
+33
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.http;
|
||||
|
||||
import org.opensaml.core.Version;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.BeanMetadataElement;
|
||||
@@ -27,6 +28,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
|
||||
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -35,6 +37,8 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
final class Saml2LoginBeanDefinitionParserUtils {
|
||||
|
||||
private static final String OPEN_SAML_4_VERSION = "4";
|
||||
|
||||
private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
|
||||
|
||||
private static final String ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF = "authentication-request-repository-ref";
|
||||
@@ -78,15 +82,27 @@ final class Saml2LoginBeanDefinitionParserUtils {
|
||||
.rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
|
||||
.addConstructorArgValue(relyingPartyRegistrationRepository)
|
||||
.getBeanDefinition();
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver")
|
||||
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver")
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.OpenSamlAuthenticationRequestResolver")
|
||||
.addConstructorArgValue(defaultRelyingPartyRegistrationResolver)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
static BeanDefinition createAuthenticationProvider() {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider")
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider")
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder
|
||||
.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider")
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
@@ -108,4 +124,17 @@ final class Saml2LoginBeanDefinitionParserUtils {
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
static String version() {
|
||||
String version = Version.getVersion();
|
||||
if (StringUtils.hasText(version)) {
|
||||
return version;
|
||||
}
|
||||
boolean openSaml4ClassPresent = ClassUtils
|
||||
.isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
|
||||
if (openSaml4ClassPresent) {
|
||||
return OPEN_SAML_4_VERSION;
|
||||
}
|
||||
throw new IllegalStateException("cannot determine OpenSAML version");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+32
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.http;
|
||||
|
||||
import org.opensaml.core.Version;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.BeanMetadataElement;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.security.saml2.provider.service.authentication.logout
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -33,6 +35,8 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
final class Saml2LogoutBeanDefinitionParserUtils {
|
||||
|
||||
private static final String OPEN_SAML_4_VERSION = "4";
|
||||
|
||||
private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
|
||||
|
||||
private static final String ATT_LOGOUT_REQUEST_VALIDATOR_REF = "logout-request-validator-ref";
|
||||
@@ -62,8 +66,14 @@ final class Saml2LogoutBeanDefinitionParserUtils {
|
||||
if (StringUtils.hasText(logoutResponseResolver)) {
|
||||
return new RuntimeBeanReference(logoutResponseResolver);
|
||||
}
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
@@ -97,10 +107,29 @@ final class Saml2LogoutBeanDefinitionParserUtils {
|
||||
if (StringUtils.hasText(logoutRequestResolver)) {
|
||||
return new RuntimeBeanReference(logoutRequestResolver);
|
||||
}
|
||||
if (version().startsWith("4")) {
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
return BeanDefinitionBuilder.rootBeanDefinition(
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestResolver")
|
||||
.addConstructorArgValue(registrations)
|
||||
.getBeanDefinition();
|
||||
}
|
||||
|
||||
static String version() {
|
||||
String version = Version.getVersion();
|
||||
if (StringUtils.hasText(version)) {
|
||||
return version;
|
||||
}
|
||||
boolean openSaml4ClassPresent = ClassUtils
|
||||
.isPresent("org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy", null);
|
||||
if (openSaml4ClassPresent) {
|
||||
return OPEN_SAML_4_VERSION;
|
||||
}
|
||||
throw new IllegalStateException("cannot determine OpenSAML version");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,6 +55,11 @@ public class MockServletContext extends org.springframework.mock.web.MockServlet
|
||||
return this.registrations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletRegistration getServletRegistration(String servletName) {
|
||||
return this.registrations.get(servletName);
|
||||
}
|
||||
|
||||
private static class MockServletRegistration implements ServletRegistration.Dynamic {
|
||||
|
||||
private final String name;
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.MappingMatch;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletMapping;
|
||||
|
||||
public final class TestMockHttpServletMappings {
|
||||
|
||||
private TestMockHttpServletMappings() {
|
||||
|
||||
}
|
||||
|
||||
public static MockHttpServletMapping extension(HttpServletRequest request, String extension) {
|
||||
String uri = request.getRequestURI();
|
||||
String matchValue = uri.substring(0, uri.lastIndexOf(extension));
|
||||
return new MockHttpServletMapping(matchValue, "*" + extension, "extension", MappingMatch.EXTENSION);
|
||||
}
|
||||
|
||||
public static MockHttpServletMapping path(HttpServletRequest request, String path) {
|
||||
String uri = request.getRequestURI();
|
||||
String matchValue = uri.substring(path.length());
|
||||
return new MockHttpServletMapping(matchValue, path + "/*", "path", MappingMatch.PATH);
|
||||
}
|
||||
|
||||
public static MockHttpServletMapping defaultMapping() {
|
||||
return new MockHttpServletMapping("", "/", "default", MappingMatch.DEFAULT);
|
||||
}
|
||||
|
||||
}
|
||||
+134
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,10 +18,12 @@ package org.springframework.security.config.annotation.web;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.Servlet;
|
||||
import javax.servlet.http.HttpServletMapping;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -29,8 +31,11 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
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.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
@@ -43,6 +48,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractRequestMatcherRegistry}.
|
||||
@@ -195,8 +203,11 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenNoDispatcherServletThenAntPathRequestMatcherType() {
|
||||
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");
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
@@ -211,10 +222,32 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenAmbiguousServletsThenException() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/servlet/**");
|
||||
servletContext.addServlet("servletTwo", DispatcherServlet.class).addMapping("/servlet/*");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenMultipleDispatcherServletMappingsThenException() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/mvc/*");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenPathDispatcherServletAndOtherServletsThenException() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
|
||||
servletContext.addServlet("default", Servlet.class).addMapping("/");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/**"));
|
||||
}
|
||||
@@ -231,6 +264,87 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenOnlyDispatcherServletThenAllows() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenImplicitServletsThenAllows() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("defaultServlet", Servlet.class);
|
||||
servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
|
||||
servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/");
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatchersWhenPathBasedNonDispatcherServletThenAllows() {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
|
||||
servletContext.addServlet("default", DispatcherServlet.class).addMapping("/");
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/services/*");
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.defaultMapping();
|
||||
}
|
||||
};
|
||||
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
|
||||
request = new MockHttpServletRequest("GET", "/services/endpoint") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.path(this, "/services");
|
||||
}
|
||||
};
|
||||
request.setServletPath("/services");
|
||||
request.setPathInfo("/endpoint");
|
||||
assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenDispatcherServletThenMvc() {
|
||||
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") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.defaultMapping();
|
||||
}
|
||||
};
|
||||
assertThat(requestMatcher.matches(request)).isFalse();
|
||||
verify(mvc).matches(request);
|
||||
verifyNoInteractions(ant);
|
||||
request = new MockHttpServletRequest("GET", "/services/endpoint") {
|
||||
@Override
|
||||
public HttpServletMapping getHttpServletMapping() {
|
||||
return TestMockHttpServletMappings.path(this, "/services");
|
||||
}
|
||||
};
|
||||
assertThat(requestMatcher.matches(request)).isFalse();
|
||||
verify(ant).matches(request);
|
||||
verifyNoMoreInteractions(mvc);
|
||||
}
|
||||
|
||||
private void mockMvcIntrospector(boolean isPresent) {
|
||||
ApplicationContext context = this.matcherRegistry.getApplicationContext();
|
||||
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
|
||||
@@ -257,11 +371,29 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RequestMatcher> requestMatchers(RequestMatcher... requestMatchers) {
|
||||
return unwrap(super.requestMatchers(requestMatchers));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<RequestMatcher> chainRequestMatchers(List<RequestMatcher> requestMatchers) {
|
||||
return requestMatchers;
|
||||
}
|
||||
|
||||
private static 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);
|
||||
}
|
||||
else {
|
||||
requestMatchers.add(requestMatcher);
|
||||
}
|
||||
}
|
||||
return requestMatchers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-2
@@ -29,7 +29,6 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -461,7 +460,7 @@ public class Saml2LoginConfigurerTests {
|
||||
Authentication authentication = this.securityContextRepository
|
||||
.loadContext(new HttpRequestResponseHolder(this.request, this.response))
|
||||
.getAuthentication();
|
||||
Assertions.assertNotNull(authentication, "Expected a valid authentication object.");
|
||||
assertThat(authentication).as("Expected a valid authentication object.").isNotNull();
|
||||
assertThat(authentication.getAuthorities()).hasSize(1);
|
||||
assertThat(authentication.getAuthorities()).first()
|
||||
.isInstanceOf(SimpleGrantedAuthority.class)
|
||||
|
||||
+2
-2
@@ -75,7 +75,7 @@ public class HeaderSpecTests {
|
||||
this.expectedHeaders.add(HttpHeaders.EXPIRES, "0");
|
||||
this.expectedHeaders.add(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff");
|
||||
this.expectedHeaders.add(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, "DENY");
|
||||
this.expectedHeaders.add(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block");
|
||||
this.expectedHeaders.add(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -320,7 +320,7 @@ public class HeaderSpecTests {
|
||||
|
||||
@Test
|
||||
public void headersWhenXssProtectionValueEnabledModeBlockThenXssProtectionWritten() {
|
||||
this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block");
|
||||
this.expectedHeaders.set(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block");
|
||||
// @formatter:off
|
||||
this.http.headers()
|
||||
.xssProtection()
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ class ServerHeadersDslTests {
|
||||
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
|
||||
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
|
||||
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ class ServerHttpSecurityDslTests {
|
||||
.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
|
||||
.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
|
||||
.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ class ServerXssProtectionDslTests {
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block")
|
||||
.expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1; mode=block")
|
||||
}
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
|
||||
+17
-6
@@ -95,17 +95,28 @@ final class AuthorizationAnnotationUtils {
|
||||
|
||||
private static <A extends Annotation> boolean hasDuplicate(MergedAnnotations mergedAnnotations,
|
||||
Class<A> annotationType) {
|
||||
boolean alreadyFound = false;
|
||||
MergedAnnotation<Annotation> alreadyFound = null;
|
||||
for (MergedAnnotation<Annotation> mergedAnnotation : mergedAnnotations) {
|
||||
if (isSynthetic(mergedAnnotation.getSource())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mergedAnnotation.getType() == annotationType) {
|
||||
if (alreadyFound) {
|
||||
return true;
|
||||
}
|
||||
alreadyFound = true;
|
||||
if (mergedAnnotation.getType() != annotationType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (alreadyFound == null) {
|
||||
alreadyFound = mergedAnnotation;
|
||||
continue;
|
||||
}
|
||||
|
||||
// https://github.com/spring-projects/spring-framework/issues/31803
|
||||
if (!mergedAnnotation.getSource().equals(alreadyFound.getSource())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mergedAnnotation.getRoot().getType() != alreadyFound.getRoot().getType()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
+12
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -51,8 +51,7 @@ import org.springframework.util.Assert;
|
||||
public final class AuthorizationManagerAfterMethodInterceptor
|
||||
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
|
||||
|
||||
private Supplier<Authentication> authentication = getAuthentication(
|
||||
SecurityContextHolder.getContextHolderStrategy());
|
||||
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
|
||||
|
||||
private final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
@@ -156,14 +155,14 @@ public final class AuthorizationManagerAfterMethodInterceptor
|
||||
* @since 5.8
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) {
|
||||
this.authentication = getAuthentication(strategy);
|
||||
this.securityContextHolderStrategy = () -> strategy;
|
||||
}
|
||||
|
||||
private void attemptAuthorization(MethodInvocation mi, Object result) {
|
||||
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
|
||||
MethodInvocationResult object = new MethodInvocationResult(mi, result);
|
||||
AuthorizationDecision decision = this.authorizationManager.check(this.authentication, object);
|
||||
this.eventPublisher.publishAuthorizationEvent(this.authentication, object, decision);
|
||||
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object);
|
||||
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision);
|
||||
if (decision != null && !decision.isGranted()) {
|
||||
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
|
||||
+ this.authorizationManager + " and decision " + decision));
|
||||
@@ -172,15 +171,13 @@ public final class AuthorizationManagerAfterMethodInterceptor
|
||||
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
|
||||
}
|
||||
|
||||
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
|
||||
return () -> {
|
||||
Authentication authentication = strategy.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
};
|
||||
private Authentication getAuthentication() {
|
||||
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
|
||||
private static <T> void noPublish(Supplier<Authentication> authentication, T object,
|
||||
|
||||
+12
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -56,8 +56,7 @@ import org.springframework.util.Assert;
|
||||
public final class AuthorizationManagerBeforeMethodInterceptor
|
||||
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
|
||||
|
||||
private Supplier<Authentication> authentication = getAuthentication(
|
||||
SecurityContextHolder.getContextHolderStrategy());
|
||||
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
|
||||
|
||||
private final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
@@ -202,13 +201,13 @@ public final class AuthorizationManagerBeforeMethodInterceptor
|
||||
* @since 5.8
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
|
||||
this.authentication = getAuthentication(securityContextHolderStrategy);
|
||||
this.securityContextHolderStrategy = () -> securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
private void attemptAuthorization(MethodInvocation mi) {
|
||||
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
|
||||
AuthorizationDecision decision = this.authorizationManager.check(this.authentication, mi);
|
||||
this.eventPublisher.publishAuthorizationEvent(this.authentication, mi, decision);
|
||||
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi);
|
||||
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
|
||||
if (decision != null && !decision.isGranted()) {
|
||||
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
|
||||
+ this.authorizationManager + " and decision " + decision));
|
||||
@@ -217,15 +216,13 @@ public final class AuthorizationManagerBeforeMethodInterceptor
|
||||
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
|
||||
}
|
||||
|
||||
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
|
||||
return () -> {
|
||||
Authentication authentication = strategy.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
};
|
||||
private Authentication getAuthentication() {
|
||||
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
|
||||
private static <T> void noPublish(Supplier<Authentication> authentication, T object,
|
||||
|
||||
+11
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,8 +46,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
public final class PostFilterAuthorizationMethodInterceptor
|
||||
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
|
||||
|
||||
private Supplier<Authentication> authentication = getAuthentication(
|
||||
SecurityContextHolder.getContextHolderStrategy());
|
||||
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
|
||||
|
||||
private PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
|
||||
|
||||
@@ -108,7 +107,7 @@ public final class PostFilterAuthorizationMethodInterceptor
|
||||
* @since 5.8
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) {
|
||||
this.authentication = getAuthentication(strategy);
|
||||
this.securityContextHolderStrategy = () -> strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,19 +124,17 @@ public final class PostFilterAuthorizationMethodInterceptor
|
||||
return returnedObject;
|
||||
}
|
||||
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
|
||||
EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
|
||||
EvaluationContext ctx = expressionHandler.createEvaluationContext(this::getAuthentication, mi);
|
||||
return expressionHandler.filter(returnedObject, attribute.getExpression(), ctx);
|
||||
}
|
||||
|
||||
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
|
||||
return () -> {
|
||||
Authentication authentication = strategy.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
};
|
||||
private Authentication getAuthentication() {
|
||||
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,8 +47,7 @@ import org.springframework.util.StringUtils;
|
||||
public final class PreFilterAuthorizationMethodInterceptor
|
||||
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
|
||||
|
||||
private Supplier<Authentication> authentication = getAuthentication(
|
||||
SecurityContextHolder.getContextHolderStrategy());
|
||||
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
|
||||
|
||||
private PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
|
||||
|
||||
@@ -109,7 +108,7 @@ public final class PreFilterAuthorizationMethodInterceptor
|
||||
* @since 5.8
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) {
|
||||
this.authentication = getAuthentication(strategy);
|
||||
this.securityContextHolderStrategy = () -> strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +123,7 @@ public final class PreFilterAuthorizationMethodInterceptor
|
||||
return mi.proceed();
|
||||
}
|
||||
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
|
||||
EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
|
||||
EvaluationContext ctx = expressionHandler.createEvaluationContext(this::getAuthentication, mi);
|
||||
Object filterTarget = findFilterTarget(attribute.getFilterTarget(), ctx, mi);
|
||||
expressionHandler.filter(filterTarget, attribute.getExpression(), ctx);
|
||||
return mi.proceed();
|
||||
@@ -150,15 +149,13 @@ public final class PreFilterAuthorizationMethodInterceptor
|
||||
return filterTarget;
|
||||
}
|
||||
|
||||
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
|
||||
return () -> {
|
||||
Authentication authentication = strategy.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
};
|
||||
private Authentication getAuthentication() {
|
||||
Authentication authentication = this.securityContextHolderStrategy.get().getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-2
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.security.access.intercept;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
@@ -50,7 +49,7 @@ public class RunAsImplAuthenticationProviderTests {
|
||||
RunAsImplAuthenticationProvider provider = new RunAsImplAuthenticationProvider();
|
||||
provider.setKey("my_password");
|
||||
Authentication result = provider.authenticate(token);
|
||||
Assertions.assertTrue(result instanceof RunAsUserToken, "Should have returned RunAsUserToken");
|
||||
assertThat(result instanceof RunAsUserToken).as("Should have returned RunAsUserToken").isTrue();
|
||||
RunAsUserToken resultCast = (RunAsUserToken) result;
|
||||
assertThat(resultCast.getKeyHash()).isEqualTo("my_password".hashCode());
|
||||
}
|
||||
|
||||
+27
@@ -41,6 +41,13 @@ class AuthorizationAnnotationUtilsTests {
|
||||
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class));
|
||||
}
|
||||
|
||||
@Test // gh-13625
|
||||
void annotationsFromSuperSuperInterfaceShouldNotTriggerAnnotationConfigurationException() throws Exception {
|
||||
Method method = HelloImpl.class.getMethod("sayHello");
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class));
|
||||
}
|
||||
|
||||
private interface BaseRepository<T> {
|
||||
|
||||
Iterable<T> findAll();
|
||||
@@ -55,4 +62,24 @@ class AuthorizationAnnotationUtilsTests {
|
||||
|
||||
}
|
||||
|
||||
private interface Hello {
|
||||
|
||||
@PreAuthorize("hasRole('someRole')")
|
||||
String sayHello();
|
||||
|
||||
}
|
||||
|
||||
private interface SayHello extends Hello {
|
||||
|
||||
}
|
||||
|
||||
private static class HelloImpl implements SayHello {
|
||||
|
||||
@Override
|
||||
public String sayHello() {
|
||||
return "hello";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,6 +29,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
@@ -91,6 +92,25 @@ public class AuthorizationManagerAfterMethodInterceptorTests {
|
||||
verify(strategy).getContext();
|
||||
}
|
||||
|
||||
// gh-12877
|
||||
@Test
|
||||
public void afterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
|
||||
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
|
||||
Authentication authentication = new TestingAuthenticationToken("john", "password",
|
||||
AuthorityUtils.createAuthorityList("authority"));
|
||||
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
|
||||
MethodInvocation invocation = mock(MethodInvocation.class);
|
||||
AuthorizationManager<MethodInvocationResult> authorizationManager = AuthenticatedAuthorizationManager
|
||||
.authenticated();
|
||||
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
|
||||
Pointcut.TRUE, authorizationManager);
|
||||
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
|
||||
SecurityContextHolder.setContextHolderStrategy(strategy);
|
||||
advice.invoke(invocation);
|
||||
verify(strategy).getContext();
|
||||
SecurityContextHolder.setContextHolderStrategy(saved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() {
|
||||
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
|
||||
|
||||
+19
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -87,6 +87,24 @@ public class AuthorizationManagerBeforeMethodInterceptorTests {
|
||||
verify(strategy).getContext();
|
||||
}
|
||||
|
||||
// gh-12877
|
||||
@Test
|
||||
public void beforeWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
|
||||
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
|
||||
Authentication authentication = new TestingAuthenticationToken("john", "password",
|
||||
AuthorityUtils.createAuthorityList("authority"));
|
||||
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
|
||||
MethodInvocation invocation = mock(MethodInvocation.class);
|
||||
AuthorizationManager<MethodInvocation> authorizationManager = AuthenticatedAuthorizationManager.authenticated();
|
||||
AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
|
||||
Pointcut.TRUE, authorizationManager);
|
||||
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
|
||||
SecurityContextHolder.setContextHolderStrategy(strategy);
|
||||
advice.invoke(invocation);
|
||||
verify(strategy).getContext();
|
||||
SecurityContextHolder.setContextHolderStrategy(saved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() {
|
||||
AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
|
||||
|
||||
+24
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -147,6 +147,29 @@ public class PostFilterAuthorizationMethodInterceptorTests {
|
||||
verify(strategy).getContext();
|
||||
}
|
||||
|
||||
// gh-12877
|
||||
@Test
|
||||
public void postFilterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
|
||||
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
|
||||
Authentication authentication = new TestingAuthenticationToken("john", "password",
|
||||
AuthorityUtils.createAuthorityList("authority"));
|
||||
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
|
||||
String[] array = { "john", "bob" };
|
||||
MockMethodInvocation invocation = new MockMethodInvocation(new TestClass(), TestClass.class,
|
||||
"doSomethingArrayAuthentication", new Class[] { String[].class }, new Object[] { array }) {
|
||||
@Override
|
||||
public Object proceed() {
|
||||
return array;
|
||||
}
|
||||
};
|
||||
PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
|
||||
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
|
||||
SecurityContextHolder.setContextHolderStrategy(strategy);
|
||||
advice.invoke(invocation);
|
||||
verify(strategy).getContext();
|
||||
SecurityContextHolder.setContextHolderStrategy(saved);
|
||||
}
|
||||
|
||||
@PostFilter("filterObject == 'john'")
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
|
||||
+21
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -204,6 +204,26 @@ public class PreFilterAuthorizationMethodInterceptorTests {
|
||||
verify(strategy).getContext();
|
||||
}
|
||||
|
||||
// gh-12877
|
||||
@Test
|
||||
public void preFilterWhenStaticSecurityContextHolderStrategyAfterConstructorThenUses() throws Throwable {
|
||||
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
|
||||
Authentication authentication = new TestingAuthenticationToken("john", "password",
|
||||
AuthorityUtils.createAuthorityList("authority"));
|
||||
given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("john");
|
||||
list.add("bob");
|
||||
MockMethodInvocation invocation = new MockMethodInvocation(new TestClass(), TestClass.class,
|
||||
"doSomethingArrayFilterAuthentication", new Class[] { List.class }, new Object[] { list });
|
||||
PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
|
||||
SecurityContextHolderStrategy saved = SecurityContextHolder.getContextHolderStrategy();
|
||||
SecurityContextHolder.setContextHolderStrategy(strategy);
|
||||
advice.invoke(invocation);
|
||||
verify(strategy).getContext();
|
||||
SecurityContextHolder.setContextHolderStrategy(saved);
|
||||
}
|
||||
|
||||
@PreFilter("filterObject == 'john'")
|
||||
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
|
||||
|
||||
|
||||
+7
-6
@@ -20,7 +20,6 @@ import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -29,6 +28,8 @@ import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgor
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class BouncyCastleAesBytesEncryptorEquivalencyTests {
|
||||
|
||||
private byte[] testData;
|
||||
@@ -96,11 +97,11 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests {
|
||||
// and can decrypt back to the original input
|
||||
byte[] leftEncrypted = left.encrypt(this.testData);
|
||||
byte[] rightEncrypted = right.encrypt(this.testData);
|
||||
Assertions.assertArrayEquals(leftEncrypted, rightEncrypted);
|
||||
assertThat(rightEncrypted).containsExactly(leftEncrypted);
|
||||
byte[] leftDecrypted = left.decrypt(leftEncrypted);
|
||||
byte[] rightDecrypted = right.decrypt(rightEncrypted);
|
||||
Assertions.assertArrayEquals(this.testData, leftDecrypted);
|
||||
Assertions.assertArrayEquals(this.testData, rightDecrypted);
|
||||
assertThat(leftDecrypted).containsExactly(this.testData);
|
||||
assertThat(rightDecrypted).containsExactly(this.testData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +115,8 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests {
|
||||
byte[] rightEncrypted = right.encrypt(this.testData);
|
||||
byte[] leftDecrypted = left.decrypt(rightEncrypted);
|
||||
byte[] rightDecrypted = right.decrypt(leftEncrypted);
|
||||
Assertions.assertArrayEquals(this.testData, leftDecrypted);
|
||||
Assertions.assertArrayEquals(this.testData, rightDecrypted);
|
||||
assertThat(leftDecrypted).containsExactly(this.testData);
|
||||
assertThat(rightDecrypted).containsExactly(this.testData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -20,13 +20,13 @@ import java.security.SecureRandom;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bouncycastle.util.Arrays;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
public class BouncyCastleAesBytesEncryptorTests {
|
||||
@@ -64,11 +64,11 @@ public class BouncyCastleAesBytesEncryptorTests {
|
||||
private void generatesDifferentCipherTexts(BytesEncryptor bcEncryptor) {
|
||||
byte[] encrypted1 = bcEncryptor.encrypt(this.testData);
|
||||
byte[] encrypted2 = bcEncryptor.encrypt(this.testData);
|
||||
Assertions.assertFalse(Arrays.areEqual(encrypted1, encrypted2));
|
||||
assertThat(Arrays.areEqual(encrypted1, encrypted2)).isFalse();
|
||||
byte[] decrypted1 = bcEncryptor.decrypt(encrypted1);
|
||||
byte[] decrypted2 = bcEncryptor.decrypt(encrypted2);
|
||||
Assertions.assertArrayEquals(this.testData, decrypted1);
|
||||
Assertions.assertArrayEquals(this.testData, decrypted2);
|
||||
assertThat(decrypted1).containsExactly(this.testData);
|
||||
assertThat(decrypted2).containsExactly(this.testData);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* xref:features/index.adoc[Features]
|
||||
** xref:features/authentication/index.adoc[Authentication]
|
||||
*** xref:features/authentication/password-storage.adoc[Password Storage]
|
||||
** xref:features/authorization/index.adoc[Authorization]
|
||||
** xref:features/exploits/index.adoc[Protection Against Exploits]
|
||||
*** xref:features/exploits/csrf.adoc[CSRF]
|
||||
*** xref:features/exploits/headers.adoc[HTTP Headers]
|
||||
|
||||
@@ -8,4 +8,4 @@ Once authentication is performed we know the identity and can perform authorizat
|
||||
|
||||
Spring Security provides built in support for authenticating users.
|
||||
This section is dedicated to generic authentication support that applies in both Servlet and WebFlux environments.
|
||||
Refer to the sections on authentication for xref:servlet/authentication/index.adoc#servlet-authentication[Servlet] and WebFlux for details on what is supported for each stack.
|
||||
Refer to the sections on authentication for xref:servlet/authentication/index.adoc#servlet-authentication[Servlet] and xref:servlet/authentication/index.adoc[WebFlux] for details on what is supported for each stack.
|
||||
|
||||
@@ -8,7 +8,9 @@ Spring Security provides https://en.wikipedia.org/wiki/Defense_in_depth_(computi
|
||||
[[authorization-request]]
|
||||
== Request Based Authorization
|
||||
|
||||
Spring Security provides authorization based upon the request for both xref:servlet/authorization/index.adoc[Servlet] and WebFlux environments.
|
||||
Spring Security provides authorization based upon the request for both xref:servlet/authorization/authorize-http-requests.adoc[Servlet] and xref:reactive/authorization/authorize-http-requests.adoc[WebFlux] environments.
|
||||
|
||||
[[authorization-method]]
|
||||
== Method Based Authorization
|
||||
|
||||
Spring Security provides authorization based on the method invocation for both xref:servlet/authorization/method-security.adoc[Servlet] and xref:reactive/authorization/method.adoc[WebFlux] environments.
|
||||
|
||||
@@ -97,13 +97,13 @@ Spring provides two mechanisms to protect against CSRF attacks:
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Both protections require that <<Safe Methods Must be Idempotent>>
|
||||
Both protections require that <<Safe Methods Must be Read-only>>
|
||||
====
|
||||
|
||||
[[csrf-protection-idempotent]]
|
||||
=== Safe Methods Must be Idempotent
|
||||
[[csrf-protection-read-only]]
|
||||
=== Safe Methods Must be Read-only
|
||||
|
||||
In order for <<csrf-protection,either protection>> against CSRF to work, the application must ensure that https://tools.ietf.org/html/rfc7231#section-4.2.1["safe" HTTP methods are idempotent].
|
||||
In order for <<csrf-protection,either protection>> against CSRF to work, the application must ensure that https://tools.ietf.org/html/rfc7231#section-4.2.1["safe" HTTP methods are read-only].
|
||||
This means that requests with the HTTP method `GET`, `HEAD`, `OPTIONS`, and `TRACE` should not change the state of the application.
|
||||
|
||||
[[csrf-protection-stp]]
|
||||
@@ -119,7 +119,7 @@ For example, requiring the actual CSRF token in an HTTP parameter or an HTTP hea
|
||||
Requiring the actual CSRF token in a cookie does not work because cookies are automatically included in the HTTP request by the browser.
|
||||
|
||||
We can relax the expectations to only require the actual CSRF token for each HTTP request that updates state of the application.
|
||||
For that to work, our application must ensure that <<csrf-protection-idempotent,safe HTTP methods are idempotent>>.
|
||||
For that to work, our application must ensure that <<csrf-protection-read-only,safe HTTP methods are read-only>>.
|
||||
This improves usability since we want to allow linking to our website using links from external sites.
|
||||
Additionally, we do not want to include the random token in HTTP GET as this can cause the tokens to be leaked.
|
||||
|
||||
@@ -190,7 +190,7 @@ Valid values for the `SameSite` attribute are:
|
||||
|
||||
* `Strict` - when specified any request coming from the https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1[same-site] will include the cookie.
|
||||
Otherwise, the cookie will not be included in the HTTP request.
|
||||
* `Lax` - when specified cookies will be sent when coming from the https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1[same-site] or when the request comes from top-level navigations and the <<Safe Methods Must be Idempotent,method is idempotent>>.
|
||||
* `Lax` - when specified cookies will be sent when coming from the https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1[same-site] or when the request comes from top-level navigations and the <<Safe Methods Must be Read-only,method is read-only>>.
|
||||
Otherwise, the cookie will not be included in the HTTP request.
|
||||
|
||||
Let's take a look at how <<csrf-explained,our example>> could be protected using the `SameSite` attribute.
|
||||
|
||||
@@ -4,5 +4,4 @@
|
||||
|
||||
Spring Security provides integrations with numerous frameworks and APIs.
|
||||
In this section, we discuss generic integrations that are not specific to Servlet or Reactive environments.
|
||||
To see specific integrations, refer to the xref:servlet/integrations/index.adoc[Servlet] and xref:servlet/integrations/index.adoc[Reactive] Integrations sections.
|
||||
// FIXME add link to reactive integrations
|
||||
To see specific integrations, refer to the xref:servlet/integrations/index.adoc[Servlet] and xref:reactive/integrations/cors.adoc[Reactive] Integrations sections.
|
||||
|
||||
@@ -17,7 +17,7 @@ In Spring Security 6, the default is that the lookup of the `CsrfToken` will be
|
||||
[NOTE]
|
||||
====
|
||||
The `CsrfToken` is needed whenever a request is made with an HTTP verb that would change the state of the application.
|
||||
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
|
||||
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-read-only[Safe Methods Must be Read-only].
|
||||
Additionally, it is needed by any request that renders the token to the response, such as a web page with a `<form>` tag that includes a hidden `<input>` for the CSRF token.
|
||||
====
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ This section discusses Spring Security's xref:features/exploits/csrf.adoc#csrf[C
|
||||
== Using Spring Security CSRF Protection
|
||||
The steps to using Spring Security's CSRF protection are outlined below:
|
||||
|
||||
* <<webflux-csrf-idempotent,Use proper HTTP verbs>>
|
||||
* <<webflux-csrf-read-only,Use proper HTTP verbs>>
|
||||
* <<webflux-csrf-configure,Configure CSRF Protection>>
|
||||
* <<webflux-csrf-include,Include the CSRF Token>>
|
||||
|
||||
[[webflux-csrf-idempotent]]
|
||||
[[webflux-csrf-read-only]]
|
||||
=== Use proper HTTP verbs
|
||||
The first step to protecting against CSRF attacks is to ensure your website uses proper HTTP verbs.
|
||||
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
|
||||
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-read-only[Safe Methods Must be Read-only].
|
||||
|
||||
[[webflux-csrf-configure]]
|
||||
=== Configure CSRF Protection
|
||||
|
||||
@@ -62,7 +62,10 @@ This configuration enables <<rsocket-authentication-simple,simple authentication
|
||||
|
||||
For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
|
||||
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
|
||||
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
|
||||
|
||||
Spring Boot registers it automatically in `RSocketSecurityAutoConfiguration` when you include {gh-samples-url}/reactive/rsocket/hello-security/build.gradle[the correct dependencies].
|
||||
|
||||
Or, if you are not using Boot's auto-configuration, you can register it manually in the following way:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -91,6 +94,8 @@ fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor
|
||||
----
|
||||
======
|
||||
|
||||
To customize the interceptor itself, use `RSocketSecurity` to add <<rsocket-authentication,authentication>> and <<rsocket-authorization,authorization>>.
|
||||
|
||||
[[rsocket-authentication]]
|
||||
== RSocket Authentication
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ spring:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jws-algorithm: RS512
|
||||
jws-algorithms: RS512
|
||||
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
|
||||
----
|
||||
|
||||
|
||||
@@ -77,11 +77,11 @@ class SecurityConfig {
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
httpBasic { }
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
@@ -105,14 +105,14 @@ The preceding configuration automatically registers an xref:servlet/authenticati
|
||||
|
||||
To learn more about username/password authentication, consider the following use cases:
|
||||
|
||||
* I want to <<publish-authentication-manager-bean,publish an `AuthenticationManager` bean>> for custom authentication
|
||||
* I want to <<customize-global-authentication-manager,customize the global `AuthenticationManager`>>
|
||||
* I want to xref:servlet/authentication/passwords/form.adoc[learn how Form Login works]
|
||||
* I want to xref:servlet/authentication/passwords/basic.adoc[learn how HTTP Basic authentication works]
|
||||
* I want to xref:servlet/authentication/passwords/basic.adoc[learn how `DaoAuthenticationProvider` works]
|
||||
* I want to xref:servlet/authentication/passwords/dao-authentication-provider.adoc[learn how `DaoAuthenticationProvider` works]
|
||||
* I want to xref:servlet/authentication/passwords/in-memory.adoc[manage users in memory]
|
||||
* I want to xref:servlet/authentication/passwords/jdbc.adoc[manage users in a database]
|
||||
* I want to xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap-authentication[manage users in LDAP]
|
||||
* I want to <<publish-authentication-manager-bean,publish an `AuthenticationManager` bean>> for custom authentication
|
||||
* I want to <<customize-global-authentication-manager,customize the global `AuthenticationManager`>>
|
||||
|
||||
[[publish-authentication-manager-bean]]
|
||||
== Publish an `AuthenticationManager` bean
|
||||
@@ -199,7 +199,7 @@ XML::
|
||||
</user-service>
|
||||
|
||||
<bean id="passwordEncoder"
|
||||
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
|
||||
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
|
||||
</http>
|
||||
----
|
||||
|
||||
@@ -207,6 +207,8 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
@@ -215,6 +217,7 @@ class SecurityConfig {
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/login", permitAll)
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
}
|
||||
@@ -410,7 +413,7 @@ XML::
|
||||
</user-service>
|
||||
|
||||
<bean id="passwordEncoder"
|
||||
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
|
||||
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
|
||||
</http>
|
||||
----
|
||||
|
||||
@@ -418,14 +421,17 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/login", permitAll)
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
@@ -483,22 +489,22 @@ Java::
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// ...
|
||||
return http.build();
|
||||
}
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// ...
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
// Return a UserDetailsService that caches users
|
||||
// ...
|
||||
}
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
// Return a UserDetailsService that caches users
|
||||
// ...
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void configure(AuthenticationManagerBuilder builder) {
|
||||
builder.eraseCredentials(false);
|
||||
}
|
||||
@Autowired
|
||||
public void configure(AuthenticationManagerBuilder builder) {
|
||||
builder.eraseCredentials(false);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
@@ -521,8 +527,8 @@ class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
// Return a UserDetailsService that caches users
|
||||
// ...
|
||||
// Return a UserDetailsService that caches users
|
||||
// ...
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -112,7 +112,7 @@ public class SecurityWebApplicationInitializer
|
||||
|
||||
This would simply only register the springSecurityFilterChain Filter for every URL in your application.
|
||||
After that we would ensure that `WebSecurityConfig` was loaded in our existing ApplicationInitializer.
|
||||
For example, if we were using Spring MVC it would be added in the `getRootConfigClasses()`
|
||||
For example, if we were using Spring MVC it would be added in the `getServletConfigClasses()`
|
||||
|
||||
[[message-web-application-inititializer-java]]
|
||||
[source,java]
|
||||
@@ -121,14 +121,42 @@ public class MvcWebApplicationInitializer extends
|
||||
AbstractAnnotationConfigDispatcherServletInitializer {
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getRootConfigClasses() {
|
||||
return new Class[] { WebSecurityConfig.class };
|
||||
protected Class<?>[] getServletConfigClasses() {
|
||||
return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
|
||||
}
|
||||
|
||||
// ... other overrides ...
|
||||
}
|
||||
----
|
||||
|
||||
The reason for this is that Spring Security needs to be able to inspect some Spring MVC configuration in order to appropriately configure xref:servlet/authorization/authorize-http-requests.adoc#_request_matchers[underlying request matchers], so they need to be in the same application context.
|
||||
Placing Spring Security in `getRootConfigClasses` places it into a parent application context that may not be able to find Spring MVC's `HandlerMappingIntrospector`.
|
||||
|
||||
==== Configuring for Multiple Spring MVC Dispatchers
|
||||
|
||||
If desired, any Spring Security configuration that is unrelated to Spring MVC may be placed in a different configuration class like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class MvcWebApplicationInitializer extends
|
||||
AbstractAnnotationConfigDispatcherServletInitializer {
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getRootConfigClasses() {
|
||||
return new Class[] { NonWebSecurityConfig.class };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getServletConfigClasses() {
|
||||
return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
|
||||
}
|
||||
|
||||
// ... other overrides ...
|
||||
}
|
||||
----
|
||||
|
||||
This can be helpful if you have multiple instances of `AbstractAnnotationConfigDispatcherServletInitializer` and don't want to duplicate the general security configuration across both of them.
|
||||
|
||||
[[jc-httpsecurity]]
|
||||
== HttpSecurity
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ This section discusses Spring Security's xref:features/exploits/csrf.adoc#csrf[C
|
||||
== Using Spring Security CSRF Protection
|
||||
The steps to using Spring Security's CSRF protection are outlined below:
|
||||
|
||||
* <<servlet-csrf-idempotent,Use proper HTTP verbs>>
|
||||
* <<servlet-csrf-read-only,Use proper HTTP verbs>>
|
||||
* <<servlet-csrf-configure,Configure CSRF Protection>>
|
||||
* <<servlet-csrf-include,Include the CSRF Token>>
|
||||
|
||||
[[servlet-csrf-idempotent]]
|
||||
[[servlet-csrf-read-only]]
|
||||
=== Use proper HTTP verbs
|
||||
The first step to protecting against CSRF attacks is to ensure your website uses proper HTTP verbs.
|
||||
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
|
||||
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-read-only[Safe Methods Must be Read-only].
|
||||
|
||||
[[servlet-csrf-configure]]
|
||||
=== Configure CSRF Protection
|
||||
|
||||
@@ -198,6 +198,63 @@ fun index(): String {
|
||||
======
|
||||
<1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`.
|
||||
|
||||
The following code shows how to set an `Authentication` as a request attribute:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
String resourceUri = ...
|
||||
|
||||
Authentication anonymousAuthentication = new AnonymousAuthenticationToken(
|
||||
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||
String body = webClient
|
||||
.get()
|
||||
.uri(resourceUri)
|
||||
.attributes(authentication(anonymousAuthentication)) <1>
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
|
||||
...
|
||||
|
||||
return "index";
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/")
|
||||
fun index(): String {
|
||||
val resourceUri: String = ...
|
||||
|
||||
val anonymousAuthentication: Authentication = AnonymousAuthenticationToken(
|
||||
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"))
|
||||
val body: String = webClient
|
||||
.get()
|
||||
.uri(resourceUri)
|
||||
.attributes(authentication(anonymousAuthentication)) <1>
|
||||
.retrieve()
|
||||
.bodyToMono()
|
||||
.block()
|
||||
|
||||
...
|
||||
|
||||
return "index"
|
||||
}
|
||||
----
|
||||
======
|
||||
<1> `authentication()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`.
|
||||
|
||||
[WARNING]
|
||||
It is recommended to be cautious with this feature since all HTTP requests will receive an access token bound to the provided principal.
|
||||
|
||||
|
||||
=== Defaulting the Authorized Client
|
||||
|
||||
|
||||
@@ -513,7 +513,7 @@ spring:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jws-algorithm: RS512
|
||||
jws-algorithms: RS512
|
||||
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
|
||||
----
|
||||
|
||||
|
||||
@@ -55,7 +55,42 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
|
||||
== Minimal Dependencies
|
||||
|
||||
SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
|
||||
It builds off of the OpenSAML library.
|
||||
It builds off of the OpenSAML library, and, for that reason, you must also include the Shibboleth Maven repository in your build configuration.
|
||||
Check https://shibboleth.atlassian.net/wiki/spaces/DEV/pages/1123844333/Use+of+Maven+Central#Publishing-to-Maven-Central[this link] for more details about why a separate repository is needed.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary"]
|
||||
----
|
||||
<repositories>
|
||||
<!-- ... -->
|
||||
<repository>
|
||||
<id>shibboleth-releases</id>
|
||||
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-saml2-service-provider</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary"]
|
||||
----
|
||||
repositories {
|
||||
// ...
|
||||
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
|
||||
}
|
||||
dependencies {
|
||||
// ...
|
||||
implementation 'org.springframework.security:spring-security-saml2-service-provider'
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[servlet-saml2login-minimalconfiguration]]
|
||||
== Minimal Configuration
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=2.7.12
|
||||
version=5.8.8-SNAPSHOT
|
||||
version=5.8.9
|
||||
samplesBranch=5.8.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
com-squareup-okhttp3 = "3.14.9"
|
||||
io-r2dbc = "0.9.1.RELEASE"
|
||||
io-rsocket = "1.1.4"
|
||||
io-spring-javaformat = "0.0.39"
|
||||
io-spring-javaformat = "0.0.40"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-aspectj = "1.9.20.1"
|
||||
@@ -15,10 +15,10 @@ org-mockito = "4.8.1"
|
||||
org-opensaml4 = "4.1.0"
|
||||
org-opensaml3 = "3.4.6"
|
||||
org-slf4j = "1.7.36"
|
||||
org-springframework = "5.3.30"
|
||||
org-springframework = "5.3.31"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.12"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.13"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.13.5"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
@@ -30,8 +30,8 @@ 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.38"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.37"
|
||||
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.40"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.39"
|
||||
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
|
||||
io-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" }
|
||||
@@ -90,7 +90,7 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1"
|
||||
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.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2021.2.18"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:2.4.1"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
@@ -102,7 +102,6 @@ org-yaml-snakeyaml = "org.yaml:snakeyaml:1.30"
|
||||
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-apollographql-apollo-apollo-runtime = "com.apollographql.apollo:apollo-runtime:2.4.5"
|
||||
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"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
|
||||
+55
-56
@@ -22,7 +22,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -30,6 +29,8 @@ import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultMapOAuth2AccessTokenResponseConverter}.
|
||||
*
|
||||
@@ -56,24 +57,24 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
|
||||
map.put("custom_parameter_2", "custom-value-2");
|
||||
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
|
||||
OAuth2AccessToken accessToken = converted.getAccessToken();
|
||||
Assertions.assertNotNull(accessToken);
|
||||
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
|
||||
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
|
||||
assertThat(accessToken).isNotNull();
|
||||
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
|
||||
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
Set<String> scopes = accessToken.getScopes();
|
||||
Assertions.assertNotNull(scopes);
|
||||
Assertions.assertEquals(2, scopes.size());
|
||||
Assertions.assertTrue(scopes.contains("read"));
|
||||
Assertions.assertTrue(scopes.contains("write"));
|
||||
Assertions.assertEquals(3600,
|
||||
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
|
||||
assertThat(scopes).isNotNull();
|
||||
assertThat(scopes.size()).isEqualTo(2);
|
||||
assertThat(scopes.contains("read")).isTrue();
|
||||
assertThat(scopes.contains("write")).isTrue();
|
||||
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds())
|
||||
.isEqualTo(3600);
|
||||
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
|
||||
Assertions.assertNotNull(refreshToken);
|
||||
Assertions.assertEquals("refresh-token-1234", refreshToken.getTokenValue());
|
||||
assertThat(refreshToken).isNotNull();
|
||||
assertThat(refreshToken.getTokenValue()).isEqualTo("refresh-token-1234");
|
||||
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
|
||||
Assertions.assertNotNull(additionalParameters);
|
||||
Assertions.assertEquals(2, additionalParameters.size());
|
||||
Assertions.assertEquals("custom-value-1", additionalParameters.get("custom_parameter_1"));
|
||||
Assertions.assertEquals("custom-value-2", additionalParameters.get("custom_parameter_2"));
|
||||
assertThat(additionalParameters).isNotNull();
|
||||
assertThat(additionalParameters.size()).isEqualTo(2);
|
||||
assertThat(additionalParameters.get("custom_parameter_1")).isEqualTo("custom-value-1");
|
||||
assertThat(additionalParameters.get("custom_parameter_2")).isEqualTo("custom-value-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -83,19 +84,18 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
|
||||
map.put("token_type", "bearer");
|
||||
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
|
||||
OAuth2AccessToken accessToken = converted.getAccessToken();
|
||||
Assertions.assertNotNull(accessToken);
|
||||
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
|
||||
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
|
||||
assertThat(accessToken).isNotNull();
|
||||
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
|
||||
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
Set<String> scopes = accessToken.getScopes();
|
||||
Assertions.assertNotNull(scopes);
|
||||
Assertions.assertEquals(0, scopes.size());
|
||||
Assertions.assertEquals(1,
|
||||
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
|
||||
assertThat(scopes).isNotNull();
|
||||
assertThat(scopes.size()).isEqualTo(0);
|
||||
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()).isEqualTo(1);
|
||||
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
|
||||
Assertions.assertNull(refreshToken);
|
||||
assertThat(refreshToken).isNull();
|
||||
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
|
||||
Assertions.assertNotNull(additionalParameters);
|
||||
Assertions.assertEquals(0, additionalParameters.size());
|
||||
assertThat(additionalParameters).isNotNull();
|
||||
assertThat(additionalParameters.size()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,19 +106,18 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
|
||||
map.put("expires_in", "2100-01-01-abc");
|
||||
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
|
||||
OAuth2AccessToken accessToken = converted.getAccessToken();
|
||||
Assertions.assertNotNull(accessToken);
|
||||
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
|
||||
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
|
||||
assertThat(accessToken).isNotNull();
|
||||
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
|
||||
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
Set<String> scopes = accessToken.getScopes();
|
||||
Assertions.assertNotNull(scopes);
|
||||
Assertions.assertEquals(0, scopes.size());
|
||||
Assertions.assertEquals(1,
|
||||
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
|
||||
assertThat(scopes).isNotNull();
|
||||
assertThat(scopes.size()).isEqualTo(0);
|
||||
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()).isEqualTo(1);
|
||||
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
|
||||
Assertions.assertNull(refreshToken);
|
||||
assertThat(refreshToken).isNull();
|
||||
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
|
||||
Assertions.assertNotNull(additionalParameters);
|
||||
Assertions.assertEquals(0, additionalParameters.size());
|
||||
assertThat(additionalParameters).isNotNull();
|
||||
assertThat(additionalParameters.size()).isEqualTo(0);
|
||||
}
|
||||
|
||||
// gh-9685
|
||||
@@ -130,11 +129,11 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
|
||||
map.put("expires_in", 3600);
|
||||
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
|
||||
OAuth2AccessToken accessToken = converted.getAccessToken();
|
||||
Assertions.assertNotNull(accessToken);
|
||||
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
|
||||
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
|
||||
Assertions.assertEquals(3600,
|
||||
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
|
||||
assertThat(accessToken).isNotNull();
|
||||
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
|
||||
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds())
|
||||
.isEqualTo(3600);
|
||||
}
|
||||
|
||||
// gh-9685
|
||||
@@ -153,24 +152,24 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests {
|
||||
map.put("custom_parameter_2", "custom-value-2");
|
||||
OAuth2AccessTokenResponse converted = this.messageConverter.convert(map);
|
||||
OAuth2AccessToken accessToken = converted.getAccessToken();
|
||||
Assertions.assertNotNull(accessToken);
|
||||
Assertions.assertEquals("access-token-1234", accessToken.getTokenValue());
|
||||
Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType());
|
||||
assertThat(accessToken).isNotNull();
|
||||
assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234");
|
||||
assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
Set<String> scopes = accessToken.getScopes();
|
||||
Assertions.assertNotNull(scopes);
|
||||
Assertions.assertEquals(2, scopes.size());
|
||||
Assertions.assertTrue(scopes.contains("read"));
|
||||
Assertions.assertTrue(scopes.contains("write"));
|
||||
Assertions.assertEquals(3600,
|
||||
Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds());
|
||||
assertThat(scopes).isNotNull();
|
||||
assertThat(scopes.size()).isEqualTo(2);
|
||||
assertThat(scopes.contains("read")).isTrue();
|
||||
assertThat(scopes.contains("write")).isTrue();
|
||||
assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds())
|
||||
.isEqualTo(3600);
|
||||
OAuth2RefreshToken refreshToken = converted.getRefreshToken();
|
||||
Assertions.assertNotNull(refreshToken);
|
||||
Assertions.assertEquals("refresh-token-1234", refreshToken.getTokenValue());
|
||||
assertThat(refreshToken).isNotNull();
|
||||
assertThat(refreshToken.getTokenValue()).isEqualTo("refresh-token-1234");
|
||||
Map<String, Object> additionalParameters = converted.getAdditionalParameters();
|
||||
Assertions.assertNotNull(additionalParameters);
|
||||
Assertions.assertEquals(2, additionalParameters.size());
|
||||
Assertions.assertEquals(nestedObject, additionalParameters.get("custom_parameter_1"));
|
||||
Assertions.assertEquals("custom-value-2", additionalParameters.get("custom_parameter_2"));
|
||||
assertThat(additionalParameters).isNotNull();
|
||||
assertThat(additionalParameters.size()).isEqualTo(2);
|
||||
assertThat(additionalParameters.get("custom_parameter_1")).isEqualTo(nestedObject);
|
||||
assertThat(additionalParameters.get("custom_parameter_2")).isEqualTo("custom-value-2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-21
@@ -22,13 +22,14 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultOAuth2AccessTokenResponseMapConverter}.
|
||||
*
|
||||
@@ -61,14 +62,14 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests {
|
||||
.build();
|
||||
// @formatter:on
|
||||
Map<String, Object> result = this.messageConverter.convert(build);
|
||||
Assertions.assertEquals(7, result.size());
|
||||
Assertions.assertEquals("access-token-value-1234", result.get("access_token"));
|
||||
Assertions.assertEquals("refresh-token-value-1234", result.get("refresh_token"));
|
||||
Assertions.assertEquals("read write", result.get("scope"));
|
||||
Assertions.assertEquals("Bearer", result.get("token_type"));
|
||||
Assertions.assertNotNull(result.get("expires_in"));
|
||||
Assertions.assertEquals("custom-value-1", result.get("custom_parameter_1"));
|
||||
Assertions.assertEquals("custom-value-2", result.get("custom_parameter_2"));
|
||||
assertThat(result.size()).isEqualTo(7);
|
||||
assertThat(result.get("access_token")).isEqualTo("access-token-value-1234");
|
||||
assertThat(result.get("refresh_token")).isEqualTo("refresh-token-value-1234");
|
||||
assertThat(result.get("scope")).isEqualTo("read write");
|
||||
assertThat(result.get("token_type")).isEqualTo("Bearer");
|
||||
assertThat(result.get("expires_in")).isNotNull();
|
||||
assertThat(result.get("custom_parameter_1")).isEqualTo("custom-value-1");
|
||||
assertThat(result.get("custom_parameter_2")).isEqualTo("custom-value-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -79,10 +80,10 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests {
|
||||
.build();
|
||||
// @formatter:on
|
||||
Map<String, Object> result = this.messageConverter.convert(build);
|
||||
Assertions.assertEquals(3, result.size());
|
||||
Assertions.assertEquals("access-token-value-1234", result.get("access_token"));
|
||||
Assertions.assertEquals("Bearer", result.get("token_type"));
|
||||
Assertions.assertNotNull(result.get("expires_in"));
|
||||
assertThat(result.size()).isEqualTo(3);
|
||||
assertThat(result.get("access_token")).isEqualTo("access-token-value-1234");
|
||||
assertThat(result.get("token_type")).isEqualTo("Bearer");
|
||||
assertThat(result.get("expires_in")).isNotNull();
|
||||
}
|
||||
|
||||
// gh-9685
|
||||
@@ -107,14 +108,14 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests {
|
||||
.build();
|
||||
// @formatter:on
|
||||
Map<String, Object> result = this.messageConverter.convert(build);
|
||||
Assertions.assertEquals(7, result.size());
|
||||
Assertions.assertEquals("access-token-value-1234", result.get("access_token"));
|
||||
Assertions.assertEquals("refresh-token-value-1234", result.get("refresh_token"));
|
||||
Assertions.assertEquals("read write", result.get("scope"));
|
||||
Assertions.assertEquals("Bearer", result.get("token_type"));
|
||||
Assertions.assertNotNull(result.get("expires_in"));
|
||||
Assertions.assertEquals(nestedObject, result.get("custom_parameter_1"));
|
||||
Assertions.assertEquals("custom-value-2", result.get("custom_parameter_2"));
|
||||
assertThat(result.size()).isEqualTo(7);
|
||||
assertThat(result.get("access_token")).isEqualTo("access-token-value-1234");
|
||||
assertThat(result.get("refresh_token")).isEqualTo("refresh-token-value-1234");
|
||||
assertThat(result.get("scope")).isEqualTo("read write");
|
||||
assertThat(result.get("token_type")).isEqualTo("Bearer");
|
||||
assertThat(result.get("expires_in")).isNotNull();
|
||||
assertThat(result.get("custom_parameter_1")).isEqualTo(nestedObject);
|
||||
assertThat(result.get("custom_parameter_2")).isEqualTo("custom-value-2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -144,6 +144,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
}
|
||||
catch (Exception ex) {
|
||||
this.logger.trace("Failed to parse token", ex);
|
||||
if (ex instanceof ParseException) {
|
||||
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed token"), ex);
|
||||
}
|
||||
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,23 +43,34 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
|
||||
*/
|
||||
private final AtomicReference<Mono<JWKSet>> cachedJWKSet = new AtomicReference<>(Mono.empty());
|
||||
|
||||
/**
|
||||
* The cached JWK set URL.
|
||||
*/
|
||||
private final AtomicReference<String> cachedJwkSetUrl = new AtomicReference<>();
|
||||
|
||||
private WebClient webClient = WebClient.create();
|
||||
|
||||
private final String jwkSetURL;
|
||||
private final Mono<String> jwkSetUrlProvider;
|
||||
|
||||
ReactiveRemoteJWKSource(String jwkSetURL) {
|
||||
Assert.hasText(jwkSetURL, "jwkSetURL cannot be empty");
|
||||
this.jwkSetURL = jwkSetURL;
|
||||
this.jwkSetUrlProvider = Mono.just(jwkSetURL);
|
||||
}
|
||||
|
||||
ReactiveRemoteJWKSource(Mono<String> jwkSetUrlProvider) {
|
||||
Assert.notNull(jwkSetUrlProvider, "jwkSetUrlProvider cannot be null");
|
||||
this.jwkSetUrlProvider = Mono.fromCallable(this.cachedJwkSetUrl::get)
|
||||
.switchIfEmpty(Mono.defer(() -> jwkSetUrlProvider.doOnNext(this.cachedJwkSetUrl::set)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<List<JWK>> get(JWKSelector jwkSelector) {
|
||||
// @formatter:off
|
||||
return this.cachedJWKSet.get()
|
||||
.switchIfEmpty(Mono.defer(() -> getJWKSet()))
|
||||
.switchIfEmpty(Mono.defer(this::getJWKSet))
|
||||
.flatMap((jwkSet) -> get(jwkSelector, jwkSet))
|
||||
.switchIfEmpty(Mono.defer(() -> getJWKSet()
|
||||
.map((jwkSet) -> jwkSelector.select(jwkSet)))
|
||||
.map(jwkSelector::select))
|
||||
);
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -95,13 +106,15 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
|
||||
*/
|
||||
private Mono<JWKSet> getJWKSet() {
|
||||
// @formatter:off
|
||||
return this.webClient.get()
|
||||
.uri(this.jwkSetURL)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
return this.jwkSetUrlProvider
|
||||
.flatMap((jwkSetURL) -> this.webClient.get()
|
||||
.uri(jwkSetURL)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
)
|
||||
.map(this::parse)
|
||||
.doOnNext((jwkSet) -> this.cachedJWKSet
|
||||
.set(Mono.just(jwkSet))
|
||||
.set(Mono.just(jwkSet))
|
||||
)
|
||||
.cache();
|
||||
// @formatter:on
|
||||
|
||||
+8
@@ -97,6 +97,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*/
|
||||
public class NimbusJwtDecoderTests {
|
||||
|
||||
private static final String MALFORMED_TOKEN = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9";
|
||||
|
||||
private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
|
||||
|
||||
private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}";
|
||||
@@ -194,6 +196,12 @@ public class NimbusJwtDecoderTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenTokenMalformedThenReturnsMalformedTokenMessage() {
|
||||
assertThatExceptionOfType(BadJwtException.class).isThrownBy(() -> this.jwtDecoder.decode(MALFORMED_TOKEN))
|
||||
.withMessage("An error occurred while attempting to decode the Jwt: Malformed token");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenJwtFailsValidationThenReturnsCorrespondingErrorMessage() {
|
||||
OAuth2Error failure = new OAuth2Error("mock-error", "mock-description", "mock-uri");
|
||||
|
||||
+25
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKMatcher;
|
||||
@@ -31,10 +32,16 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willReturn;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
@@ -52,6 +59,9 @@ public class ReactiveRemoteJWKSourceTests {
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
@Mock
|
||||
private Supplier<String> mockStringSupplier;
|
||||
|
||||
// @formatter:off
|
||||
private String keys = "{\n"
|
||||
+ " \"keys\": [\n"
|
||||
@@ -156,4 +166,18 @@ public class ReactiveRemoteJWKSourceTests {
|
||||
assertThat(this.source.get(this.selector).block()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getShouldRecoverAndReturnKeysAfterErrorCase() {
|
||||
given(this.matcher.matches(any())).willReturn(true);
|
||||
this.source = new ReactiveRemoteJWKSource(Mono.fromSupplier(this.mockStringSupplier));
|
||||
willThrow(WebClientResponseException.ServiceUnavailable.class).given(this.mockStringSupplier).get();
|
||||
// first case: id provider has error state
|
||||
assertThatExceptionOfType(WebClientResponseException.ServiceUnavailable.class)
|
||||
.isThrownBy(() -> this.source.get(this.selector).block());
|
||||
// second case: id provider is healthy again
|
||||
willReturn(this.server.url("/").toString()).given(this.mockStringSupplier).get();
|
||||
List<JWK> actual = this.source.get(this.selector).block();
|
||||
assertThat(actual).isNotEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -167,10 +167,12 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
.samlRequest(deflatedAndEncoded)
|
||||
.relayState(relayState);
|
||||
if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) {
|
||||
Map<String, String> parameters = OpenSamlSigningUtils.sign(registration)
|
||||
.param(Saml2ParameterNames.SAML_REQUEST, deflatedAndEncoded)
|
||||
.param(Saml2ParameterNames.RELAY_STATE, relayState)
|
||||
.parameters();
|
||||
OpenSamlSigningUtils.QueryParametersPartial parametersPartial = OpenSamlSigningUtils.sign(registration)
|
||||
.param(Saml2ParameterNames.SAML_REQUEST, deflatedAndEncoded);
|
||||
if (relayState != null) {
|
||||
parametersPartial = parametersPartial.param(Saml2ParameterNames.RELAY_STATE, relayState);
|
||||
}
|
||||
Map<String, String> parameters = parametersPartial.parameters();
|
||||
builder.sigAlg(parameters.get(Saml2ParameterNames.SIG_ALG))
|
||||
.signature(parameters.get(Saml2ParameterNames.SIGNATURE));
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
@@ -190,14 +189,14 @@ public class OpenSamlAuthenticationRequestFactoryTests {
|
||||
@Test
|
||||
public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() {
|
||||
AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST);
|
||||
Assertions.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding());
|
||||
assertThat(SAMLConstants.SAML2_POST_BINDING_URI).isEqualTo(authn.getProtocolBinding());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAuthenticationRequestWhenSetUriThenReturnsCorrectBinding() {
|
||||
this.factory.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
|
||||
AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST);
|
||||
Assertions.assertEquals(SAMLConstants.SAML2_REDIRECT_BINDING_URI, authn.getProtocolBinding());
|
||||
assertThat(SAMLConstants.SAML2_REDIRECT_BINDING_URI).isEqualTo(authn.getProtocolBinding());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,7 +20,6 @@ import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
@@ -190,7 +189,7 @@ public class OpenSaml4AuthenticationRequestFactoryTests {
|
||||
@Test
|
||||
public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() {
|
||||
AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST);
|
||||
Assertions.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding());
|
||||
assertThat(SAMLConstants.SAML2_POST_BINDING_URI).isEqualTo(authn.getProtocolBinding());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+69
-5
@@ -16,12 +16,15 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2ParameterNames;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
|
||||
@@ -32,6 +35,12 @@ import org.springframework.security.saml2.provider.service.registration.TestRely
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSamlAuthenticationRequestResolver}
|
||||
@@ -40,7 +49,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
|
||||
private RelyingPartyRegistration.Builder relyingPartyRegistrationBuilder;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.relyingPartyRegistrationBuilder = TestRelyingPartyRegistrations.relyingPartyRegistration();
|
||||
}
|
||||
@@ -102,7 +111,9 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.assertingPartyDetails((party) -> party.verificationX509Credentials((c) -> c.add(credential)))
|
||||
.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> resolver.resolve(request, null));
|
||||
assertThatExceptionOfType(Saml2Exception.class)
|
||||
.isThrownBy(() -> resolver.resolve(request, (r, authnRequest) -> {
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -161,7 +172,8 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
(party) -> party.signingAlgorithms((algs) -> algs.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1)))
|
||||
.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, null);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
});
|
||||
assertThat(result.getSamlRequest()).isNotEmpty();
|
||||
assertThat(result.getRelayState()).isNotNull();
|
||||
assertThat(result.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
|
||||
@@ -169,6 +181,58 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveAuthenticationRequestWhenSignedAndRelayStateIsNullThenSignsWithoutRelayState() {
|
||||
try (MockedStatic<OpenSamlSigningUtils> openSamlSigningUtilsMockedStatic = mockStatic(
|
||||
OpenSamlSigningUtils.class, Answers.CALLS_REAL_METHODS)) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setPathInfo("/saml2/authenticate/registration-id");
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder
|
||||
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(true))
|
||||
.build();
|
||||
OpenSamlSigningUtils.QueryParametersPartial queryParametersPartialSpy = spy(
|
||||
new OpenSamlSigningUtils.QueryParametersPartial(registration));
|
||||
openSamlSigningUtilsMockedStatic.when(() -> OpenSamlSigningUtils.sign(any()))
|
||||
.thenReturn(queryParametersPartialSpy);
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
resolver.setRelayStateResolver((source) -> null);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
});
|
||||
assertThat(result.getSamlRequest()).isNotEmpty();
|
||||
assertThat(result.getRelayState()).isNull();
|
||||
assertThat(result.getSigAlg()).isNotNull();
|
||||
assertThat(result.getSignature()).isNotNull();
|
||||
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||
verify(queryParametersPartialSpy, never()).param(eq(Saml2ParameterNames.RELAY_STATE), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveAuthenticationRequestWhenSignedAndRelayStateIsEmptyThenSignsWithEmptyRelayState() {
|
||||
try (MockedStatic<OpenSamlSigningUtils> openSamlSigningUtilsMockedStatic = mockStatic(
|
||||
OpenSamlSigningUtils.class, Answers.CALLS_REAL_METHODS)) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setPathInfo("/saml2/authenticate/registration-id");
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder
|
||||
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(true))
|
||||
.build();
|
||||
OpenSamlSigningUtils.QueryParametersPartial queryParametersPartialSpy = spy(
|
||||
new OpenSamlSigningUtils.QueryParametersPartial(registration));
|
||||
openSamlSigningUtilsMockedStatic.when(() -> OpenSamlSigningUtils.sign(any()))
|
||||
.thenReturn(queryParametersPartialSpy);
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
resolver.setRelayStateResolver((source) -> "");
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
});
|
||||
assertThat(result.getSamlRequest()).isNotEmpty();
|
||||
assertThat(result.getRelayState()).isEmpty();
|
||||
assertThat(result.getSigAlg()).isNotNull();
|
||||
assertThat(result.getSignature()).isNotNull();
|
||||
assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||
verify(queryParametersPartialSpy).param(eq(Saml2ParameterNames.RELAY_STATE), eq(""));
|
||||
}
|
||||
}
|
||||
|
||||
private OpenSamlAuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistration registration) {
|
||||
return new OpenSamlAuthenticationRequestResolver((request, id) -> registration);
|
||||
}
|
||||
|
||||
+4
-4
@@ -18,7 +18,6 @@ package org.springframework.security.saml2.provider.service.web.authentication;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -46,6 +45,7 @@ import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
@@ -94,7 +94,7 @@ public class Saml2WebSsoAuthenticationFilterTests {
|
||||
|
||||
@Test
|
||||
public void requiresAuthenticationWhenHappyPathThenReturnsTrue() {
|
||||
Assertions.assertTrue(this.filter.requiresAuthentication(this.request, this.response));
|
||||
assertThat(this.filter.requiresAuthentication(this.request, this.response)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,7 +102,7 @@ public class Saml2WebSsoAuthenticationFilterTests {
|
||||
this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/some/other/path/{registrationId}");
|
||||
this.request.setPathInfo("/some/other/path/idp-registration-id");
|
||||
this.request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "xml-data-goes-here");
|
||||
Assertions.assertTrue(this.filter.requiresAuthentication(this.request, this.response));
|
||||
assertThat(this.filter.requiresAuthentication(this.request, this.response)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -143,7 +143,7 @@ public class Saml2WebSsoAuthenticationFilterTests {
|
||||
this.filter.setAuthenticationDetailsSource(authenticationDetailsSource);
|
||||
this.request.setPathInfo("/some/other/path/idp-registration-id");
|
||||
this.filter.attemptAuthentication(this.request, this.response);
|
||||
Assertions.assertEquals(details, token.getDetails());
|
||||
assertThat(token.getDetails()).isEqualTo(details);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,6 +36,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
@@ -244,7 +245,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||
if (session != null) {
|
||||
AuthenticationException ex = (AuthenticationException) session
|
||||
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
|
||||
errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials";
|
||||
errorMsg = (ex != null && StringUtils.hasLength(ex.getMessage())) ? ex.getMessage()
|
||||
: "Invalid credentials";
|
||||
}
|
||||
}
|
||||
String contextPath = request.getContextPath();
|
||||
|
||||
+2
-2
@@ -122,7 +122,7 @@ public final class XXssProtectionHeaderWriter implements HeaderWriter {
|
||||
* specify mode as blocked. The content will be replaced with "#". For example:
|
||||
*
|
||||
* <pre>
|
||||
* X-XSS-Protection: 1 ; mode=block
|
||||
* X-XSS-Protection: 1; mode=block
|
||||
* </pre>
|
||||
* @param headerValue the new header value
|
||||
* @throws IllegalArgumentException when headerValue is null
|
||||
@@ -134,7 +134,7 @@ public final class XXssProtectionHeaderWriter implements HeaderWriter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block"
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1; mode=block"
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 5.8
|
||||
|
||||
+3
-3
@@ -122,7 +122,7 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW
|
||||
* specify mode as blocked. The content will be replaced with "#". For example:
|
||||
*
|
||||
* <pre>
|
||||
* X-XSS-Protection: 1 ; mode=block
|
||||
* X-XSS-Protection: 1; mode=block
|
||||
* </pre>
|
||||
* @param headerValue the new headerValue
|
||||
* @throws IllegalArgumentException if headerValue is null
|
||||
@@ -135,14 +135,14 @@ public class XXssProtectionServerHttpHeadersWriter implements ServerHttpHeadersW
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1 ; mode=block"
|
||||
* The value of the x-xss-protection header. One of: "0", "1", "1; mode=block"
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 5.8
|
||||
*/
|
||||
public enum HeaderValue {
|
||||
|
||||
DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1 ; mode=block");
|
||||
DISABLED("0"), ENABLED("1"), ENABLED_MODE_BLOCK("1; mode=block");
|
||||
|
||||
private final String value;
|
||||
|
||||
|
||||
+14
@@ -182,6 +182,20 @@ public class DefaultLoginPageGeneratingFilterTests {
|
||||
.contains("<a href=\"/saml/sso/google\">Google < > " ' &</a>");
|
||||
} // Fake OpenID filter (since it's not in this module
|
||||
|
||||
// gh-13768
|
||||
@Test
|
||||
public void generatesWhenExceptionWithEmptyMessageThenInvalidCredentials() throws Exception {
|
||||
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(
|
||||
new UsernamePasswordAuthenticationFilter());
|
||||
filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/login");
|
||||
request.setQueryString("error");
|
||||
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new BadCredentialsException(null));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
filter.doFilter(request, response, this.chain);
|
||||
assertThat(response.getContentAsString()).contains("Invalid credentials");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MockProcessingFilter extends AbstractAuthenticationProcessingFilter {
|
||||
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ public class XXssProtectionServerHttpHeadersWriterTests {
|
||||
this.writer.writeHttpHeaders(this.exchange);
|
||||
assertThat(this.headers).hasSize(1);
|
||||
assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION))
|
||||
.containsOnly("1 ; mode=block");
|
||||
.containsOnly("1; mode=block");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,7 +99,7 @@ public class XXssProtectionServerHttpHeadersWriterTests {
|
||||
this.writer.writeHttpHeaders(this.exchange);
|
||||
assertThat(this.headers).hasSize(1);
|
||||
assertThat(this.headers.get(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION))
|
||||
.containsOnly("1 ; mode=block");
|
||||
.containsOnly("1; mode=block");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user