Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e75b210015 | |||
| 6cf8c53aaa | |||
| 2d52fb8e4b | |||
| e9ee2cefb9 | |||
| 15dc917c23 | |||
| 46368f0a75 | |||
| 0448e2804c | |||
| 7571ab9b7d | |||
| 8566bbcdac | |||
| 609d5a71c6 | |||
| c73876e596 | |||
| 8a07d17c69 | |||
| 37d8846652 | |||
| 54117d7d27 | |||
| 74b8c6715b | |||
| 8c9275067e | |||
| 4b8bfee7c5 | |||
| b727f24c95 | |||
| d55c8aac4f | |||
| 28be37238d | |||
| c4e9fb885d | |||
| dad1fba1bf | |||
| 442faccb5f | |||
| 68fd755abc | |||
| e25117856e | |||
| 16dcfd1cfe | |||
| c69df9fba0 | |||
| 5ffebaf12b | |||
| 39cee36065 | |||
| d5603a944d | |||
| a513fc0f38 | |||
| eb58655fa9 | |||
| c15589ede1 | |||
| a106188add | |||
| 6bda1d2bf3 | |||
| a708007536 | |||
| fd65dc6756 | |||
| 5eefe9dcff | |||
| c75ee25a6d | |||
| 8d664bc4c2 | |||
| 889fa55c9c | |||
| 98321b769a | |||
| a74008cc79 | |||
| 3d7e22a4e9 | |||
| abd51f7b63 | |||
| 4154ed543a | |||
| 2e92dad761 | |||
| 84cca81edf | |||
| ebabcaa51a | |||
| 094bf1b527 | |||
| e7b14b3ca2 | |||
| 66665344c5 | |||
| 6c7703789a | |||
| a6918f9e0c | |||
| fabf7f649c | |||
| b2240f376e | |||
| 7b88ab289d | |||
| ad57c0567f | |||
| 26566af431 | |||
| 86578ea9de | |||
| 5257e36ffc | |||
| 383e0c2cf0 | |||
| 0421e25cba | |||
| 1c885cf3a3 | |||
| 97ba596ca3 | |||
| 1c3ce1e401 | |||
| 79887fa213 | |||
| 35cf52d3bd | |||
| 6bf11181ef | |||
| 5be58b2622 | |||
| b851a8bf6d | |||
| faadadf71a | |||
| 207e41f6f6 | |||
| ce46f06d81 | |||
| a93cc3f0a0 | |||
| 2fcf48a11b | |||
| 0398d86f39 | |||
| c9112dfaab | |||
| 4156766fe0 | |||
| f125785328 | |||
| 4643c0d999 | |||
| d2d82b5e94 | |||
| d4d03ea146 | |||
| 6ba88e6545 | |||
| a3855ea092 | |||
| f57b433fb1 | |||
| 30566eba9e | |||
| c373f6afd9 | |||
| d245a54951 | |||
| 5f599d3d77 | |||
| f49a933490 | |||
| 8961264ec5 | |||
| aaa6983fda | |||
| a1b282ff03 | |||
| 2db4430dcd | |||
| 0baf650f38 | |||
| 000b4bc495 | |||
| 82c86b822f | |||
| 4f3faa78f7 | |||
| ca1961d35e | |||
| 821db0a1ea | |||
| 45b81b194b | |||
| 0d4c619648 | |||
| 4a8961bc80 | |||
| aad3e61f4d | |||
| eaa9692c0c | |||
| 51d5fb9c03 | |||
| 33e72b35f9 | |||
| c306df9b46 | |||
| d42405de42 | |||
| 7db357d36a | |||
| 5beabbe357 | |||
| f5bc6ce665 | |||
| 108b03da3a | |||
| 4cbb057b97 | |||
| 8758a00e90 | |||
| c0f7cecc6d | |||
| 22ffa833ca | |||
| 090c5f96ce | |||
| f41b77a4db | |||
| 6f43104eb3 | |||
| 2028507bf8 | |||
| ae46032ced | |||
| ffdb397830 | |||
| 88a8ef647b | |||
| 5e1db6a771 | |||
| 225dc593a8 | |||
| fda0e9a2b6 | |||
| 3cfaf0d11d | |||
| 892bbcfe0f | |||
| 5406fed5dc | |||
| fbfa13bd47 | |||
| 992bda32f9 | |||
| 0c6b427132 | |||
| 0f7d17bae6 | |||
| 44d5cb0a63 | |||
| c11248b589 | |||
| 972075677f | |||
| 314ae69b70 | |||
| 11614edcfe |
@@ -55,6 +55,7 @@ jobs:
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
cache: 'gradle'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Set up gradle user name
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ See also the https://github.com/spring-projects/spring-framework/wiki/Gradle-bui
|
||||
|
||||
== Getting Support
|
||||
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
|
||||
https://spring.io/services[Commercial support] is available too.
|
||||
https://spring.io/support[Commercial support] is available too.
|
||||
|
||||
== Contributing
|
||||
https://help.github.com/articles/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/main/CONTRIBUTING.adoc[contributor guidelines] for details.
|
||||
|
||||
+7
-1
@@ -3,7 +3,7 @@ import io.spring.gradle.IncludeRepoTask
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.10'
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.11'
|
||||
classpath "io.freefair.gradle:aspectj-plugin:6.5.1"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
@@ -119,6 +119,12 @@ updateDependenciesSettings {
|
||||
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
|
||||
|
||||
@@ -93,13 +93,13 @@ dependencies {
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
implementation 'com.github.spullara.mustache.java:compiler:0.9.4'
|
||||
implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
|
||||
implementation 'io.spring.nohttp:nohttp-gradle:0.0.10'
|
||||
implementation 'io.spring.nohttp:nohttp-gradle:0.0.11'
|
||||
implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0'
|
||||
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
|
||||
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.29.0'
|
||||
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
|
||||
testImplementation platform('org.junit:junit-bom:5.9.1')
|
||||
testImplementation platform('org.junit:junit-bom:5.9.2')
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
@@ -36,7 +36,7 @@ class CheckstylePlugin implements Plugin<Project> {
|
||||
if (checkstyleDir.exists() && checkstyleDir.directory) {
|
||||
project.getPluginManager().apply('checkstyle')
|
||||
project.dependencies.add('checkstyle', 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.15')
|
||||
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.10')
|
||||
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.11')
|
||||
|
||||
project.checkstyle {
|
||||
configDirectory = checkstyleDir
|
||||
|
||||
@@ -62,6 +62,33 @@ public class SchemaDeployPlugin implements Plugin<Project> {
|
||||
execute "rm -f $tempPath*.zip"
|
||||
execute "rm -rf $extractPath*"
|
||||
execute "mv $tempPath/* $extractPath"
|
||||
|
||||
/*
|
||||
* Copy spring-security-oauth schemas so that legacy projects using the "http" scheme can
|
||||
* resolve the following schema locations:
|
||||
*
|
||||
* - http://www.springframework.org/schema/security/spring-security-oauth-1.0.xsd
|
||||
* - http://www.springframework.org/schema/security/spring-security-oauth2-1.0.xsd
|
||||
* - http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd
|
||||
*
|
||||
* Note: The directory:
|
||||
* https://www.springframework.org/schema/security/
|
||||
*
|
||||
* is a symbolic link pointing to:
|
||||
* https://docs.spring.io/autorepo/schema/spring-security/current/security/
|
||||
*
|
||||
* which in turn points to the latest:
|
||||
* https://docs.spring.io/autorepo/schema/spring-security/$version/security/
|
||||
*
|
||||
* with the help of the autoln command which is run regularly via a cron job.
|
||||
*
|
||||
* See https://github.com/spring-io/autoln for more info.
|
||||
*/
|
||||
if (name == "spring-security") {
|
||||
def springSecurityOauthPath = "/var/www/domains/spring.io/docs/htdocs/autorepo/schema/spring-security-oauth/current/security"
|
||||
execute "cp $springSecurityOauthPath/* ${extractPath}security"
|
||||
}
|
||||
|
||||
execute "chmod -R g+w $extractPath"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2020-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.gradle.github.user;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class GitHubUserApi {
|
||||
private String baseUrl = "https://api.github.com";
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public GitHubUserApi(String gitHubAccessToken) {
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.addInterceptor(new AuthorizationInterceptor(gitHubAccessToken))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a GitHub user by the personal access token.
|
||||
*
|
||||
* @return The GitHub user
|
||||
*/
|
||||
public User getUser() {
|
||||
String url = this.baseUrl + "/user";
|
||||
Request request = new Request.Builder().url(url).get().build();
|
||||
try (Response response = this.httpClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
throw new RuntimeException(
|
||||
String.format("Unable to retrieve GitHub user." +
|
||||
" Please check the personal access token and try again." +
|
||||
" Got response %s", response));
|
||||
}
|
||||
return this.gson.fromJson(response.body().charStream(), User.class);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Unable to retrieve GitHub user.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthorizationInterceptor implements Interceptor {
|
||||
private final String token;
|
||||
|
||||
public AuthorizationInterceptor(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request().newBuilder()
|
||||
.addHeader("Authorization", "Bearer " + this.token)
|
||||
.build();
|
||||
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.springframework.gradle.github.user;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class User {
|
||||
private Long id;
|
||||
private String login;
|
||||
private String name;
|
||||
private String url;
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return this.login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", login='" + login + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", url='" + url + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@ import java.util.Base64;
|
||||
* Implements necessary calls to the Sagan API See https://spring.io/restdocs/index.html
|
||||
*/
|
||||
public class SaganApi {
|
||||
private String baseUrl = "https://spring.io/api";
|
||||
private String baseUrl = "https://api.spring.io";
|
||||
|
||||
private OkHttpClient client;
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public SaganApi(String gitHubToken) {
|
||||
public SaganApi(String username, String gitHubToken) {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new BasicInterceptor("not-used", gitHubToken))
|
||||
.addInterceptor(new BasicInterceptor(username, gitHubToken))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+16
-2
@@ -20,6 +20,9 @@ import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.gradle.github.user.GitHubUserApi;
|
||||
import org.springframework.gradle.github.user.User;
|
||||
|
||||
public class SaganCreateReleaseTask extends DefaultTask {
|
||||
|
||||
@Input
|
||||
@@ -35,11 +38,22 @@ public class SaganCreateReleaseTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void saganCreateRelease() {
|
||||
SaganApi sagan = new SaganApi(this.gitHubAccessToken);
|
||||
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
|
||||
User user = github.getUser();
|
||||
|
||||
// Antora reference docs URLs for snapshots do not contain -SNAPSHOT
|
||||
String referenceDocUrl = this.referenceDocUrl;
|
||||
if (this.version.endsWith("-SNAPSHOT")) {
|
||||
referenceDocUrl = this.referenceDocUrl
|
||||
.replace("{version}", this.version)
|
||||
.replace("-SNAPSHOT", "");
|
||||
}
|
||||
|
||||
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
|
||||
Release release = new Release();
|
||||
release.setVersion(this.version);
|
||||
release.setApiDocUrl(this.apiDocUrl);
|
||||
release.setReferenceDocUrl(this.referenceDocUrl);
|
||||
release.setReferenceDocUrl(referenceDocUrl);
|
||||
sagan.createReleaseForProject(release, this.projectName);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.gradle.github.user.GitHubUserApi;
|
||||
import org.springframework.gradle.github.user.User;
|
||||
|
||||
public class SaganDeleteReleaseTask extends DefaultTask {
|
||||
|
||||
@Input
|
||||
@@ -31,7 +34,10 @@ public class SaganDeleteReleaseTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void saganCreateRelease() {
|
||||
SaganApi sagan = new SaganApi(this.gitHubAccessToken);
|
||||
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
|
||||
User user = github.getUser();
|
||||
|
||||
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
|
||||
sagan.deleteReleaseForProject(this.version, this.projectName);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public class SaganApiTests {
|
||||
public void setup() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
this.sagan = new SaganApi("mock-oauth-token");
|
||||
this.sagan = new SaganApi("user", "mock-oauth-token");
|
||||
this.baseUrl = this.server.url("/api").toString();
|
||||
this.sagan.setBaseUrl(this.baseUrl);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class SaganApiTests {
|
||||
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
|
||||
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases");
|
||||
assertThat(request.getMethod()).isEqualToIgnoringCase("post");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg==");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
|
||||
assertThat(request.getBody().readString(Charset.defaultCharset())).isEqualToIgnoringWhitespace("{\n" +
|
||||
" \"version\":\"5.6.0-SNAPSHOT\",\n" +
|
||||
" \"current\":false,\n" +
|
||||
@@ -79,7 +79,7 @@ public class SaganApiTests {
|
||||
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
|
||||
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases/5.6.0-SNAPSHOT");
|
||||
assertThat(request.getMethod()).isEqualToIgnoringCase("delete");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg==");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
|
||||
assertThat(request.getBody().readString(Charset.defaultCharset())).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2020-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.gradle.github.user;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class GitHubUserApiTests {
|
||||
private GitHubUserApi gitHubUserApi;
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
this.baseUrl = this.server.url("/api").toString();
|
||||
this.gitHubUserApi = new GitHubUserApi("mock-oauth-token");
|
||||
this.gitHubUserApi.setBaseUrl(this.baseUrl);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() throws Exception {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserWhenValidParametersThenSuccess() {
|
||||
// @formatter:off
|
||||
String responseJson = "{\n" +
|
||||
" \"avatar_url\": \"https://avatars.githubusercontent.com/u/583231?v=4\",\n" +
|
||||
" \"bio\": null,\n" +
|
||||
" \"blog\": \"https://github.blog\",\n" +
|
||||
" \"company\": \"@github\",\n" +
|
||||
" \"created_at\": \"2011-01-25T18:44:36Z\",\n" +
|
||||
" \"email\": null,\n" +
|
||||
" \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n" +
|
||||
" \"followers\": 8481,\n" +
|
||||
" \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n" +
|
||||
" \"following\": 9,\n" +
|
||||
" \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n" +
|
||||
" \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n" +
|
||||
" \"gravatar_id\": \"\",\n" +
|
||||
" \"hireable\": null,\n" +
|
||||
" \"html_url\": \"https://github.com/octocat\",\n" +
|
||||
" \"id\": 583231,\n" +
|
||||
" \"location\": \"San Francisco\",\n" +
|
||||
" \"login\": \"octocat\",\n" +
|
||||
" \"name\": \"The Octocat\",\n" +
|
||||
" \"node_id\": \"MDQ6VXNlcjU4MzIzMQ==\",\n" +
|
||||
" \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n" +
|
||||
" \"public_gists\": 8,\n" +
|
||||
" \"public_repos\": 8,\n" +
|
||||
" \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n" +
|
||||
" \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n" +
|
||||
" \"site_admin\": false,\n" +
|
||||
" \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n" +
|
||||
" \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n" +
|
||||
" \"twitter_username\": null,\n" +
|
||||
" \"type\": \"User\",\n" +
|
||||
" \"updated_at\": \"2023-02-25T12:14:58Z\",\n" +
|
||||
" \"url\": \"https://api.github.com/users/octocat\"\n" +
|
||||
"}";
|
||||
// @formatter:on
|
||||
this.server.enqueue(new MockResponse().setBody(responseJson));
|
||||
|
||||
User user = this.gitHubUserApi.getUser();
|
||||
assertThat(user.getId()).isEqualTo(583231);
|
||||
assertThat(user.getLogin()).isEqualTo("octocat");
|
||||
assertThat(user.getName()).isEqualTo("The Octocat");
|
||||
assertThat(user.getUrl()).isEqualTo("https://api.github.com/users/octocat");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserWhenErrorResponseThenException() {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(400));
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(RuntimeException.class)
|
||||
.isThrownBy(() -> this.gitHubUserApi.getUser());
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
+7
-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.
|
||||
@@ -3270,7 +3270,12 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
}
|
||||
|
||||
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
|
||||
int order = this.filterOrders.getOrder(registeredFilter) + offset;
|
||||
Integer registeredFilterOrder = this.filterOrders.getOrder(registeredFilter);
|
||||
if (registeredFilterOrder == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"The Filter class " + registeredFilter.getName() + " does not have a registered order");
|
||||
}
|
||||
int order = registeredFilterOrder + offset;
|
||||
this.filters.add(new OrderedFilter(filter, order));
|
||||
this.filterOrders.put(filter.getClass(), order);
|
||||
return this;
|
||||
|
||||
+12
-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.
|
||||
@@ -35,6 +35,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -326,6 +328,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
*/
|
||||
private LogoutFilter createLogoutFilter(H http) {
|
||||
this.contextLogoutHandler.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
|
||||
this.logoutHandlers.add(this.contextLogoutHandler);
|
||||
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
|
||||
LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
|
||||
@@ -337,6 +340,14 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return result;
|
||||
}
|
||||
|
||||
private SecurityContextRepository getSecurityContextRepository(H http) {
|
||||
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
|
||||
if (securityContextRepository == null) {
|
||||
securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
}
|
||||
return securityContextRepository;
|
||||
}
|
||||
|
||||
private RequestMatcher getLogoutRequestMatcher(H http) {
|
||||
if (this.logoutRequestMatcher != null) {
|
||||
return this.logoutRequestMatcher;
|
||||
|
||||
+1
-1
@@ -303,7 +303,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
/**
|
||||
* Controls the maximum number of sessions for a user. The default is to allow any
|
||||
* number of users.
|
||||
* number of sessions.
|
||||
* @param maximumSessions the maximum number of sessions for a user
|
||||
* @return the {@link SessionManagementConfigurer} for further customizations
|
||||
*/
|
||||
|
||||
+10
-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.
|
||||
@@ -56,6 +56,8 @@ final class WebSocketMessageBrokerSecurityConfiguration
|
||||
|
||||
private static final String SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME = "stompWebSocketHandlerMapping";
|
||||
|
||||
private static final String CSRF_CHANNEL_INTERCEPTOR_BEAN_NAME = "csrfChannelInterceptor";
|
||||
|
||||
private MessageMatcherDelegatingAuthorizationManager b;
|
||||
|
||||
private static final AuthorizationManager<Message<?>> ANY_MESSAGE_AUTHENTICATED = MessageMatcherDelegatingAuthorizationManager
|
||||
@@ -66,7 +68,7 @@ final class WebSocketMessageBrokerSecurityConfiguration
|
||||
|
||||
private final SecurityContextChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor();
|
||||
|
||||
private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
|
||||
private ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
|
||||
|
||||
private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor(
|
||||
ANY_MESSAGE_AUTHENTICATED);
|
||||
@@ -86,6 +88,12 @@ final class WebSocketMessageBrokerSecurityConfiguration
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
ChannelInterceptor csrfChannelInterceptor = getBeanOrNull(CSRF_CHANNEL_INTERCEPTOR_BEAN_NAME,
|
||||
ChannelInterceptor.class);
|
||||
if (csrfChannelInterceptor != null) {
|
||||
this.csrfChannelInterceptor = csrfChannelInterceptor;
|
||||
}
|
||||
|
||||
this.authorizationChannelInterceptor
|
||||
.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context));
|
||||
this.authorizationChannelInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
|
||||
|
||||
+37
-18
@@ -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.
|
||||
@@ -208,30 +208,49 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
ParserContext parserContext) {
|
||||
String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID);
|
||||
String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION);
|
||||
RelyingPartyRegistration.Builder builder;
|
||||
if (StringUtils.hasText(metadataLocation)) {
|
||||
builder = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId);
|
||||
}
|
||||
else {
|
||||
builder = RelyingPartyRegistration.withRegistrationId(registrationId)
|
||||
.assertingPartyDetails((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt,
|
||||
assertingParties, apBuilder, parserContext));
|
||||
}
|
||||
addRemainingProperties(relyingPartyRegistrationElt, builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void addRemainingProperties(Element relyingPartyRegistrationElt,
|
||||
RelyingPartyRegistration.Builder builder) {
|
||||
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
|
||||
String singleLogoutServiceLocation = relyingPartyRegistrationElt
|
||||
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
|
||||
String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt
|
||||
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
|
||||
Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt);
|
||||
if (StringUtils.hasText(metadataLocation)) {
|
||||
return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
|
||||
.singleLogoutServiceBinding(singleLogoutServiceBinding);
|
||||
}
|
||||
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
|
||||
String assertionConsumerServiceLocation = relyingPartyRegistrationElt
|
||||
.getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION);
|
||||
Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding(
|
||||
relyingPartyRegistrationElt);
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId)
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.assertionConsumerServiceBinding(assertionConsumerServiceBinding)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
|
||||
.singleLogoutServiceBinding(singleLogoutServiceBinding)
|
||||
.assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties,
|
||||
builder, parserContext));
|
||||
if (StringUtils.hasText(entityId)) {
|
||||
builder.entityId(entityId);
|
||||
}
|
||||
if (StringUtils.hasText(singleLogoutServiceLocation)) {
|
||||
builder.singleLogoutServiceLocation(singleLogoutServiceLocation);
|
||||
}
|
||||
if (StringUtils.hasText(singleLogoutServiceResponseLocation)) {
|
||||
builder.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation);
|
||||
}
|
||||
if (singleLogoutServiceBinding != null) {
|
||||
builder.singleLogoutServiceBinding(singleLogoutServiceBinding);
|
||||
}
|
||||
if (StringUtils.hasText(assertionConsumerServiceLocation)) {
|
||||
builder.assertionConsumerServiceLocation(assertionConsumerServiceLocation);
|
||||
}
|
||||
if (assertionConsumerServiceBinding != null) {
|
||||
builder.assertionConsumerServiceBinding(assertionConsumerServiceBinding);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildAssertingParty(Element relyingPartyElt, Map<String, Map<String, Object>> assertingParties,
|
||||
@@ -309,7 +328,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
if (StringUtils.hasText(assertionConsumerServiceBinding)) {
|
||||
return Saml2MessageBinding.valueOf(assertionConsumerServiceBinding);
|
||||
}
|
||||
return Saml2MessageBinding.REDIRECT;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Saml2MessageBinding getSingleLogoutServiceBinding(Element relyingPartyRegistrationElt) {
|
||||
@@ -317,7 +336,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
|
||||
if (StringUtils.hasText(singleLogoutServiceBinding)) {
|
||||
return Saml2MessageBinding.valueOf(singleLogoutServiceBinding);
|
||||
}
|
||||
return Saml2MessageBinding.POST;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) {
|
||||
|
||||
+48
-2
@@ -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.
|
||||
@@ -30,6 +30,7 @@ import org.assertj.core.api.ListAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.UnsatisfiedDependencyException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
@@ -45,12 +46,29 @@ import org.springframework.security.web.context.request.async.WebAsyncManagerInt
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class HttpSecurityAddFilterTest {
|
||||
public class HttpSecurityAddFilterTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Test
|
||||
public void addFilterAfterFilterNotRegisteredYetThenThrowIllegalArgument() {
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(
|
||||
() -> this.spring.register(MyOtherFilterAfterMyFilterNotRegisteredYetConfig.class).autowire())
|
||||
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterBeforeFilterNotRegisteredYetThenThrowIllegalArgument() {
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(
|
||||
() -> this.spring.register(MyOtherFilterBeforeMyFilterNotRegisteredYetConfig.class).autowire())
|
||||
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterAfterWhenSameFilterDifferentPlacesThenOrderCorrect() {
|
||||
this.spring.register(MyFilterMultipleAfterConfig.class).autowire();
|
||||
@@ -209,6 +227,34 @@ public class HttpSecurityAddFilterTest {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterAfterMyFilterNotRegisteredYetConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterAfter(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterBeforeMyFilterNotRegisteredYetConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterRelativeToMyFilterBeforeConfig {
|
||||
|
||||
+85
-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.
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -23,7 +26,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
@@ -33,9 +39,12 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
@@ -45,6 +54,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
@@ -321,6 +331,80 @@ public class LogoutConfigurerTests {
|
||||
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception {
|
||||
CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class);
|
||||
this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire();
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder logoutRequest = post("/logout")
|
||||
.with(csrf())
|
||||
.with(user("user"))
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
|
||||
this.mvc.perform(logoutRequest)
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"));
|
||||
// @formatter:on
|
||||
int invocationCount = 2; // 1 from user() post processor and 1 from
|
||||
// SecurityContextLogoutHandler
|
||||
verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(),
|
||||
any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception {
|
||||
this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire();
|
||||
MockHttpSession session = mock(MockHttpSession.class);
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder logoutRequest = post("/logout")
|
||||
.with(csrf())
|
||||
.with(user("user"))
|
||||
.session(session)
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
|
||||
this.mvc.perform(logoutRequest)
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"))
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class InvalidateHttpSessionFalseConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout((logout) -> logout.invalidateHttpSession(false))
|
||||
.securityContext((context) -> context.requireExplicitSave(true));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CustomSecurityContextRepositoryConfig {
|
||||
|
||||
static SecurityContextRepository repository;
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout(Customizer.withDefaults())
|
||||
.securityContext((context) -> context
|
||||
.requireExplicitSave(true)
|
||||
.securityContextRepository(repository)
|
||||
);
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class NullLogoutSuccessHandlerConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
||||
* @author Alavudin Kuttikkattil
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class ReactiveOAuth2ClientImportSelectorTest {
|
||||
public class ReactiveOAuth2ClientImportSelectorTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
+5
-3
@@ -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.
|
||||
@@ -61,6 +61,7 @@ import org.springframework.security.messaging.context.SecurityContextChannelInte
|
||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -79,6 +80,7 @@ import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSo
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
|
||||
|
||||
public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
|
||||
@@ -284,7 +286,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
|
||||
private void assertHandshake(HttpServletRequest request) {
|
||||
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
|
||||
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
|
||||
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
|
||||
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
|
||||
}
|
||||
@@ -306,7 +308,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
|
||||
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
|
||||
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
|
||||
request.setAttribute(CsrfToken.class.getName(), this.token);
|
||||
request.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token));
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.socket;
|
||||
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
final class TestDeferredCsrfToken implements DeferredCsrfToken {
|
||||
|
||||
private final CsrfToken csrfToken;
|
||||
|
||||
TestDeferredCsrfToken(CsrfToken csrfToken) {
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CsrfToken get() {
|
||||
return this.csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
+5
-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.
|
||||
@@ -70,6 +70,7 @@ import org.springframework.security.messaging.context.SecurityContextChannelInte
|
||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -92,6 +93,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
|
||||
|
||||
public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
|
||||
@@ -367,7 +369,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
|
||||
private void assertHandshake(HttpServletRequest request) {
|
||||
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
|
||||
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
|
||||
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
|
||||
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
|
||||
}
|
||||
@@ -389,7 +391,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
|
||||
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
|
||||
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
|
||||
request.setAttribute(CsrfToken.class.getName(), this.token);
|
||||
request.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token));
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
+57
-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.
|
||||
@@ -62,6 +62,27 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
"</b:beans>\n";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String METADATA_LOCATION_OVERRIDE_PROPERTIES_XML_CONFIG = "<b:beans xmlns:b=\"http://www.springframework.org/schema/beans\"\n" +
|
||||
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
|
||||
" xmlns=\"http://www.springframework.org/schema/security\"\n" +
|
||||
" xsi:schemaLocation=\"\n" +
|
||||
"\t\t\thttp://www.springframework.org/schema/security\n" +
|
||||
"\t\t\thttps://www.springframework.org/schema/security/spring-security.xsd\n" +
|
||||
"\t\t\thttp://www.springframework.org/schema/beans\n" +
|
||||
"\t\t\thttps://www.springframework.org/schema/beans/spring-beans.xsd\">\n" +
|
||||
" \n" +
|
||||
" <relying-party-registrations>\n" +
|
||||
" <relying-party-registration registration-id=\"one\"\n" +
|
||||
" entity-id=\"https://rp.example.org\"\n" +
|
||||
" metadata-location=\"${metadata-location}\"\n" +
|
||||
" assertion-consumer-service-location=\"https://rp.example.org/location\"\n" +
|
||||
" assertion-consumer-service-binding=\"REDIRECT\"/>" +
|
||||
" </relying-party-registrations>\n" +
|
||||
"\n" +
|
||||
"</b:beans>\n";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String METADATA_RESPONSE = "<?xml version=\"1.0\"?>\n" +
|
||||
"<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" entityID=\"https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php\" ID=\"_e793a707d3e1a9ee6cbec7454fdad2c7cd793dd3703179a527b9620a6e9682af\"><ds:Signature>\n" +
|
||||
@@ -143,6 +164,41 @@ public class RelyingPartyRegistrationsBeanDefinitionParserTests {
|
||||
.containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWhenMetadataLocationConfiguredAndRegistrationHasPropertiesThenDoNotOverrideSpecifiedProperties()
|
||||
throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String serverUrl = this.server.url("/").toString();
|
||||
this.server.enqueue(xmlResponse(METADATA_RESPONSE));
|
||||
String metadataConfig = METADATA_LOCATION_OVERRIDE_PROPERTIES_XML_CONFIG.replace("${metadata-location}",
|
||||
serverUrl);
|
||||
this.spring.context(metadataConfig).autowire();
|
||||
assertThat(this.relyingPartyRegistrationRepository)
|
||||
.isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class);
|
||||
RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository
|
||||
.findByRegistrationId("one");
|
||||
RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = relyingPartyRegistration
|
||||
.getAssertingPartyDetails();
|
||||
assertThat(relyingPartyRegistration).isNotNull();
|
||||
assertThat(relyingPartyRegistration.getRegistrationId()).isEqualTo("one");
|
||||
assertThat(relyingPartyRegistration.getEntityId()).isEqualTo("https://rp.example.org");
|
||||
assertThat(relyingPartyRegistration.getAssertionConsumerServiceLocation())
|
||||
.isEqualTo("https://rp.example.org/location");
|
||||
assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding())
|
||||
.isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||
assertThat(assertingPartyDetails.getEntityId())
|
||||
.isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php");
|
||||
assertThat(assertingPartyDetails.getWantAuthnRequestsSigned()).isFalse();
|
||||
assertThat(assertingPartyDetails.getVerificationX509Credentials()).hasSize(1);
|
||||
assertThat(assertingPartyDetails.getEncryptionX509Credentials()).hasSize(1);
|
||||
assertThat(assertingPartyDetails.getSingleSignOnServiceLocation())
|
||||
.isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php");
|
||||
assertThat(assertingPartyDetails.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
|
||||
assertThat(assertingPartyDetails.getSigningAlgorithms())
|
||||
.containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWhenSingleRelyingPartyRegistrationThenAvailableInRepository() {
|
||||
this.spring.configLocations(xml("SingleRegistration")).autowire();
|
||||
|
||||
+31
-6
@@ -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.
|
||||
@@ -61,6 +61,7 @@ import org.springframework.security.test.context.annotation.SecurityTestExecutio
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@@ -77,6 +78,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
/**
|
||||
@@ -381,12 +383,14 @@ public class WebSocketMessageBrokerConfigTests {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
String csrfAttributeName = CsrfToken.class.getName();
|
||||
String customAttributeName = this.getClass().getName();
|
||||
MvcResult result = mvc.perform(get("/app").requestAttr(csrfAttributeName, this.token)
|
||||
.sessionAttr(customAttributeName, "attributeValue")).andReturn();
|
||||
MvcResult result = mvc.perform(
|
||||
get("/app").requestAttr(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token))
|
||||
.sessionAttr(customAttributeName, "attributeValue"))
|
||||
.andReturn();
|
||||
CsrfToken handshakeToken = (CsrfToken) this.testHandshakeHandler.attributes.get(csrfAttributeName);
|
||||
String handshakeValue = (String) this.testHandshakeHandler.attributes.get(customAttributeName);
|
||||
String sessionValue = (String) result.getRequest().getSession().getAttribute(customAttributeName);
|
||||
assertThat(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThatCsrfToken(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThat(handshakeValue).isEqualTo(sessionValue)
|
||||
.withFailMessage("Explicitly listed session variables are not overridden");
|
||||
}
|
||||
@@ -398,12 +402,13 @@ public class WebSocketMessageBrokerConfigTests {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
String csrfAttributeName = CsrfToken.class.getName();
|
||||
String customAttributeName = this.getClass().getName();
|
||||
MvcResult result = mvc.perform(get("/app/289/tpyx6mde/websocket").requestAttr(csrfAttributeName, this.token)
|
||||
MvcResult result = mvc.perform(get("/app/289/tpyx6mde/websocket")
|
||||
.requestAttr(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token))
|
||||
.sessionAttr(customAttributeName, "attributeValue")).andReturn();
|
||||
CsrfToken handshakeToken = (CsrfToken) this.testHandshakeHandler.attributes.get(csrfAttributeName);
|
||||
String handshakeValue = (String) this.testHandshakeHandler.attributes.get(customAttributeName);
|
||||
String sessionValue = (String) result.getRequest().getSession().getAttribute(customAttributeName);
|
||||
assertThat(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThatCsrfToken(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThat(handshakeValue).isEqualTo(sessionValue)
|
||||
.withFailMessage("Explicitly listed session variables are not overridden");
|
||||
}
|
||||
@@ -526,6 +531,26 @@ public class WebSocketMessageBrokerConfigTests {
|
||||
return SecurityContextHolder.getContextHolderStrategy();
|
||||
}
|
||||
|
||||
private static final class TestDeferredCsrfToken implements DeferredCsrfToken {
|
||||
|
||||
private final CsrfToken csrfToken;
|
||||
|
||||
TestDeferredCsrfToken(CsrfToken csrfToken) {
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CsrfToken get() {
|
||||
return this.csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MessageController {
|
||||
|
||||
|
||||
+12
-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.
|
||||
@@ -62,12 +62,15 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
|
||||
/**
|
||||
* Creates an instance of {@link AuthorityAuthorizationManager} with the provided
|
||||
* authority.
|
||||
* @param role the authority to check for prefixed with "ROLE_"
|
||||
* @param role the authority to check for prefixed with "ROLE_". Role should not start
|
||||
* with "ROLE_" since it is automatically prepended already.
|
||||
* @param <T> the type of object being authorized
|
||||
* @return the new instance
|
||||
*/
|
||||
public static <T> AuthorityAuthorizationManager<T> hasRole(String role) {
|
||||
Assert.notNull(role, "role cannot be null");
|
||||
Assert.isTrue(!role.startsWith(ROLE_PREFIX), () -> role + " should not start with " + ROLE_PREFIX + " since "
|
||||
+ ROLE_PREFIX + " is automatically prepended when using hasRole. Consider using hasAuthority instead.");
|
||||
return hasAuthority(ROLE_PREFIX + role);
|
||||
}
|
||||
|
||||
@@ -86,7 +89,8 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
|
||||
/**
|
||||
* Creates an instance of {@link AuthorityAuthorizationManager} with the provided
|
||||
* authorities.
|
||||
* @param roles the authorities to check for prefixed with "ROLE_"
|
||||
* @param roles the authorities to check for prefixed with "ROLE_". Each role should
|
||||
* not start with "ROLE_" since it is automatically prepended already.
|
||||
* @param <T> the type of object being authorized
|
||||
* @return the new instance
|
||||
*/
|
||||
@@ -125,7 +129,11 @@ public final class AuthorityAuthorizationManager<T> implements AuthorizationMana
|
||||
private static String[] toNamedRolesArray(String rolePrefix, String[] roles) {
|
||||
String[] result = new String[roles.length];
|
||||
for (int i = 0; i < roles.length; i++) {
|
||||
result[i] = rolePrefix + roles[i];
|
||||
String role = roles[i];
|
||||
Assert.isTrue(!role.startsWith(rolePrefix), () -> role + " should not start with " + rolePrefix + " since "
|
||||
+ rolePrefix
|
||||
+ " is automatically prepended when using hasAnyRole. Consider using hasAnyAuthority instead.");
|
||||
result[i] = rolePrefix + role;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import org.springframework.security.core.Authentication;
|
||||
* A reactive authorization manager which can determine if an {@link Authentication} has
|
||||
* access to a specific object.
|
||||
*
|
||||
* @param <T> the type of object that the authorization check is being done one.
|
||||
* @param <T> the type of object that the authorization check is being done on.
|
||||
* @author Rob Winch
|
||||
* @since 5.0
|
||||
*/
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import org.springframework.security.access.intercept.aspectj.MethodInvocationAda
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class PostInvocationAdviceProviderTest {
|
||||
public class PostInvocationAdviceProviderTests {
|
||||
|
||||
@Mock
|
||||
private PostInvocationAuthorizationAdvice authorizationAdvice;
|
||||
+20
-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.
|
||||
@@ -44,6 +44,15 @@ public class AuthorityAuthorizationManagerTests {
|
||||
.withMessage("role cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasRoleWhenContainRoleWithRolePrefixThenException() {
|
||||
String ROLE_PREFIX = "ROLE_";
|
||||
String ROLE_USER = ROLE_PREFIX + "USER";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasRole(ROLE_USER))
|
||||
.withMessage(ROLE_USER + " should not start with " + ROLE_PREFIX + " since " + ROLE_PREFIX
|
||||
+ " is automatically prepended when using hasRole. Consider using hasAuthority instead.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAuthorityWhenNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAuthority(null))
|
||||
@@ -76,6 +85,16 @@ public class AuthorityAuthorizationManagerTests {
|
||||
.withMessage("rolePrefix cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAnyRoleWhenContainRoleWithRolePrefixThenException() {
|
||||
String ROLE_PREFIX = "ROLE_";
|
||||
String ROLE_USER = ROLE_PREFIX + "USER";
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(new String[] { ROLE_USER }))
|
||||
.withMessage(ROLE_USER + " should not start with " + ROLE_PREFIX + " since " + ROLE_PREFIX
|
||||
+ " is automatically prepended when using hasAnyRole. Consider using hasAnyAuthority instead.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasAnyAuthorityWhenNullThenException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null))
|
||||
|
||||
@@ -11,5 +11,6 @@ dependencies {
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.mockito:mockito-inline"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
}
|
||||
|
||||
+28
-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.
|
||||
@@ -16,8 +16,13 @@
|
||||
|
||||
package org.springframework.security.crypto.factory;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
@@ -34,6 +39,8 @@ import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
||||
*/
|
||||
public final class PasswordEncoderFactories {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(PasswordEncoderFactories.class);
|
||||
|
||||
private PasswordEncoderFactories() {
|
||||
}
|
||||
|
||||
@@ -78,7 +85,8 @@ public final class PasswordEncoderFactories {
|
||||
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
|
||||
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
|
||||
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
|
||||
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
putIfAlgorithmSupported("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder::defaultsForSpringSecurity_v5_8,
|
||||
encoders);
|
||||
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
|
||||
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
|
||||
@@ -90,4 +98,22 @@ public final class PasswordEncoderFactories {
|
||||
return new DelegatingPasswordEncoder(encodingId, encoders);
|
||||
}
|
||||
|
||||
private static void putIfAlgorithmSupported(String encodingId, Supplier<PasswordEncoder> encoderSupplier,
|
||||
Map<String, PasswordEncoder> encoders) {
|
||||
try {
|
||||
PasswordEncoder passwordEncoder = encoderSupplier.get();
|
||||
encoders.put(encodingId, passwordEncoder);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (ex.getCause() instanceof NoSuchAlgorithmException) {
|
||||
logger.warn(String.format(
|
||||
"Cannot create PasswordEncoder with encodingId [%s] because the algorithm is not available",
|
||||
encodingId), ex);
|
||||
}
|
||||
else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-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.
|
||||
@@ -16,11 +16,17 @@
|
||||
|
||||
package org.springframework.security.crypto.factory;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
@@ -123,4 +129,14 @@ public class PasswordEncoderFactoriesTests {
|
||||
assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWhenAlgorithmNotAvailableThenSkip() {
|
||||
try (MockedStatic<Pbkdf2PasswordEncoder> pbkdf2PasswordEncoderMock = mockStatic(Pbkdf2PasswordEncoder.class)) {
|
||||
pbkdf2PasswordEncoderMock.when(Pbkdf2PasswordEncoder::defaultsForSpringSecurity_v5_8)
|
||||
.thenThrow(new IllegalArgumentException(new NoSuchAlgorithmException()));
|
||||
|
||||
assertThatNoException().isThrownBy(PasswordEncoderFactories::createDelegatingPasswordEncoder);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-9
@@ -8,16 +8,16 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.26")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.31")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.3")
|
||||
api platform("org.junit:junit-bom:5.9.1")
|
||||
api platform("org.junit:junit-bom:5.9.2")
|
||||
api platform("org.mockito:mockito-bom:4.8.1")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.6")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.11")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.4.20221013")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.5")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.2.11"
|
||||
api "ch.qos.logback:logback-classic:1.2.12"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.24.4"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.43.1"
|
||||
@@ -26,7 +26,7 @@ dependencies {
|
||||
api "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.13.3"
|
||||
api "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
|
||||
api "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
|
||||
api "jakarta.inject:jakarta.inject-api:1.0.5"
|
||||
api "jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7"
|
||||
@@ -50,11 +50,11 @@ dependencies {
|
||||
api "org.assertj:assertj-core:3.23.1"
|
||||
api "org.bouncycastle:bcpkix-jdk15on:1.70"
|
||||
api "org.bouncycastle:bcprov-jdk15on:1.70"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.50.v20221201"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.50.v20221201"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.51.v20230217"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.51.v20230217"
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.14.Final"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
api "org.hsqldb:hsqldb:2.7.1"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
api "org.openid4java:openid4java-nodeps:0.9.6"
|
||||
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
/package-lock.json
|
||||
/node_modules/
|
||||
/*-antora-playbook.yml
|
||||
|
||||
+8
-2
@@ -6,7 +6,13 @@ nav:
|
||||
ext:
|
||||
collector:
|
||||
run:
|
||||
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntora
|
||||
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntoraYml
|
||||
local: true
|
||||
scan:
|
||||
dir: ./build/generateAntora
|
||||
dir: ./build/generated-antora-resources
|
||||
asciidoc:
|
||||
attributes:
|
||||
icondir: icons
|
||||
gh-old-samples-url: 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
|
||||
gh-samples-url: "https://github.com/spring-projects/spring-security-samples/tree/{gh-tag}"
|
||||
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# The purpose of this Antora playbook is to generate a preview of the docs in the current branch.
|
||||
antora:
|
||||
extensions:
|
||||
- '@antora/collector-extension'
|
||||
site:
|
||||
title: Spring Security
|
||||
url: https://docs.spring.io/spring-security/reference
|
||||
content:
|
||||
sources:
|
||||
- url: ./..
|
||||
branches: HEAD
|
||||
start_path: docs
|
||||
worktrees: true
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-pagination: ''
|
||||
hide-uri-scheme: '@'
|
||||
urls:
|
||||
latest_version_segment: ''
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
|
||||
snapshot: true
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 89 KiB |
@@ -4,12 +4,11 @@
|
||||
The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0.
|
||||
Use 5.8 and the steps below to minimize changes when
|
||||
ifdef::spring-security-version[]
|
||||
xref:6.0.0@migration/index.adoc[updating to 6.0]
|
||||
xref:6.0.2@migration/index.adoc[updating to 6.0].
|
||||
endif::[]
|
||||
ifndef::spring-security-version[]
|
||||
updating to 6.0
|
||||
updating to 6.0.
|
||||
endif::[]
|
||||
.
|
||||
|
||||
== Update to Spring Security 5.8
|
||||
|
||||
|
||||
@@ -400,6 +400,58 @@ http {
|
||||
|
||||
To opt-out of the 6.0 defaults and instead continue to pass `AuthenticationServiceException` on to ``ServerAuthenticationEntryPoint``s, you can follow the same steps as above, except set `rethrowAuthenticationServiceException` to false.
|
||||
|
||||
[[add-configuration-annotation]]
|
||||
== Add `@Configuration` annotation
|
||||
|
||||
In 6.0, `@Configuration` is removed from `@EnableWebFluxSecurity` and `@EnableReactiveMethodSecurity`.
|
||||
|
||||
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
|
||||
For example, `@EnableReactiveMethodSecurity` changes from:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
== Address OAuth2 Client Deprecations
|
||||
|
||||
=== `ServerOAuth2AuthorizedClientExchangeFilterFunction`
|
||||
@@ -424,3 +476,47 @@ The method `setAllowMultipleAuthorizationRequests(...)` has no direct replacemen
|
||||
=== `UnAuthenticatedServerOAuth2AuthorizedClientRepository`
|
||||
|
||||
The class `UnAuthenticatedServerOAuth2AuthorizedClientRepository` has no direct replacement. Usage of the class can be replaced with `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`.
|
||||
|
||||
== Add `@Configuration` to `@Enable*` annotations
|
||||
|
||||
In 6.0, all Spring Security's `@Enable*` annotations had their `@Configuration` removed.
|
||||
While convenient, it was not consistent with the rest of the Spring projects and most notably Spring Framework's `@Enable*` annotations.
|
||||
Additionally, the introduction of support for `@Configuration(proxyBeanMethods=false)` in Spring Framework provides another reason to remove `@Configuration` meta-annotation from Spring Security's `@Enable*` annotations and allow users to opt into their preferred configuration mode.
|
||||
|
||||
The following annotations had their `@Configuration` removed:
|
||||
|
||||
- `@EnableGlobalAuthentication`
|
||||
- `@EnableGlobalMethodSecurity`
|
||||
- `@EnableMethodSecurity`
|
||||
- `@EnableReactiveMethodSecurity`
|
||||
- `@EnableWebSecurity`
|
||||
- `@EnableWebFluxSecurity`
|
||||
|
||||
For example, if you are using `@EnableWebFluxSecurity`, you will need to change:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
And the same applies to every other annotation listed above.
|
||||
|
||||
@@ -104,6 +104,111 @@ should change to:
|
||||
----
|
||||
====
|
||||
|
||||
=== Use a Custom `@Bean` instead of subclassing `DefaultMethodSecurityExpressionHandler`
|
||||
|
||||
As a performance optimization, a new method was introduced to `MethodSecurityExpressionHandler` that takes a `Supplier<Authentication>` instead of an `Authentication`.
|
||||
|
||||
This allows Spring Security to defer the lookup of the `Authentication`, and is taken advantage of automatically when you use `@EnableMethodSecurity` instead of `@EnableGlobalMethodSecurity`.
|
||||
|
||||
However, let's say that your code extends `DefaultMethodSecurityExpressionHandler` and overrides `createSecurityExpressionRoot(Authentication, MethodInvocation)` to return a custom `SecurityExpressionRoot` instance.
|
||||
This will no longer work because the arrangement that `@EnableMethodSecurity` sets up calls `createEvaluationContext(Supplier<Authentication>, MethodInvocation)` instead.
|
||||
|
||||
Happily, such a level of customization is often unnecessary.
|
||||
Instead, you can create a custom bean with the authorization methods that you need.
|
||||
|
||||
For example, let's say you are wanting a custom evaluation of `@PostAuthorize("hasAuthority('ADMIN')")`.
|
||||
You can create a custom `@Bean` like this one:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
class MyAuthorizer {
|
||||
boolean isAdmin(MethodSecurityExpressionOperations root) {
|
||||
boolean decision = root.hasAuthority("ADMIN");
|
||||
// custom work ...
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class MyAuthorizer {
|
||||
fun isAdmin(val root: MethodSecurityExpressionOperations): boolean {
|
||||
val decision = root.hasAuthority("ADMIN");
|
||||
// custom work ...
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
and then refer to it in the annotation like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("@authz.isAdmin(#root)")
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("@authz.isAdmin(#root)")
|
||||
----
|
||||
====
|
||||
|
||||
==== I'd still prefer to subclass `DefaultMethodSecurityExpressionHandler`
|
||||
|
||||
If you must continue subclassing `DefaultMethodSecurityExpressionHandler`, you can still do so.
|
||||
Instead, override the `createEvaluationContext(Supplier<Authentication>, MethodInvocation)` method like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
|
||||
@Override
|
||||
public EvaluationContext createEvaluationContext(
|
||||
Supplier<Authentication> authentication, MethodInvocation mi) {
|
||||
StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
|
||||
MySecurityExpressionRoot root = new MySecurityExpressionRoot(authentication, invocation);
|
||||
root.setPermissionEvaluator(getPermissionEvaluator());
|
||||
root.setTrustResolver(new AuthenticationTrustResolverImpl());
|
||||
root.setRoleHierarchy(getRoleHierarchy());
|
||||
context.setRootObject(root);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class MyExpressionHandler: DefaultMethodSecurityExpressionHandler {
|
||||
override fun createEvaluationContext(val authentication: Supplier<Authentication>,
|
||||
val mi: MethodInvocation): EvaluationContext {
|
||||
val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext;
|
||||
val root = new MySecurityExpressionRoot(authentication, invocation);
|
||||
root.setPermissionEvaluator(getPermissionEvaluator());
|
||||
root.setTrustResolver(new AuthenticationTrustResolverImpl());
|
||||
root.setRoleHierarchy(getRoleHierarchy());
|
||||
context.setRootObject(root);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
==== Opt-out Steps
|
||||
|
||||
If you need to opt-out of these changes, you can use `@EnableGlobalMethodSecurity` instead of `@EnableMethodSecurity`
|
||||
|
||||
[[servlet-replace-permissionevaluator-bean-with-methodsecurityexpression-handler]]
|
||||
=== Publish a `MethodSecurityExpressionHandler` instead of a `PermissionEvaluator`
|
||||
|
||||
@@ -247,12 +352,22 @@ public final class PreAuthorizeAuthorizationManagerAdapter implements Authorizat
|
||||
|
||||
Once you have implemented `AuthorizationManager`, please follow the details in the reference manual for xref:servlet/authorization/method-security.adoc#jc-method-security-custom-authorization-manager[adding a custom `AuthorizationManager`].
|
||||
|
||||
==== I use a custom `AfterInvocationManager`
|
||||
==== I use `AfterInvocationManager` or `AfterInvocationProvider`
|
||||
|
||||
{security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`] replaces both {security-api-url}org/springframework/security/access/AccessDecisionManager.html[`AccessDecisionManager`] and {security-api-url}org/springframework/security/access/intercept/AfterInvocationManager.html[`AfterInvocationManager`].
|
||||
The difference is that `AuthorizationManager<MethodInvocation>` replaces `AccessDecisionManager` and `AuthorizationManager<MethodInvocationResult>` replaces `AfterInvocationManager`.
|
||||
{security-api-url}org/springframework/security/access/intercept/AfterInvocationManager.html;[`AfterInvocationManager`] and {security-api-url}org/springframework/security/access/intercept/AfterInvocationProvider.html[`AfterInvocationProvider`] make an authorization decision about an invocation's result.
|
||||
For example, in the case of method invocation, these make an authorization decision about a method's return value.
|
||||
|
||||
Given that, <<_i_use_a_custom_accessdecisionvoter,the same rules apply for adaptation>>, where the goal this time is to implement `AuthorizationManager<MethodInvocationResult>` instead of `AuthorizationManager<MethodInvocation>` and use `AuthorizationManagerAfterMethodInterceptor` instead of `AuthorizationManagerBeforeMethodInterceptor`.
|
||||
In Spring Security 3.0, authorization decision-making was standardized into the xref:servlet/authorization/method-security.adoc[`@PostAuthorize` and `@PostFilter` annotations].
|
||||
`@PostAuthorize` is for deciding whether the return value as a whole was permitted to be returned.
|
||||
`@PostFilter` is for filtering individual entries from a returned collection, array, or stream.
|
||||
|
||||
These two annotations should serve most needs, and you are encouraged to migrate to one or both of them since `AfterInvocationProvider` and `AfterInvocationManager` are now deprecated.
|
||||
|
||||
If you've implemented your own `AfterInvocationManager` or `AfterInvocationProvider`, you should first ask yourself what it is trying to do.
|
||||
If it is trying to authorize the return type, <<_i_use_a_custom_accessdecisionvoter,consider implementing `AuthorizationManager<MethodInvocationResult>` and using `AfterMethodAuthorizationManagerInterceptor`>>. Or publishing a custom bean and using `@PostAuthorize("@myBean.authorize(#root)")`.
|
||||
|
||||
If it is trying to filter, then consider publishing a custom bean and using `@PostFilter("@mybean.authorize(#root)")`.
|
||||
Or, if needed, you can implement your own `MethodInterceptor`, taking a look at `PostFilterAuthorizationMethodInterceptor` and `PrePostMethodSecurityConfiguration` for an example.
|
||||
|
||||
==== I use `RunAsManager`
|
||||
|
||||
@@ -1042,7 +1157,7 @@ public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplic
|
||||
|
||||
@Override
|
||||
protected EnumSet<DispatcherType> getSecurityDispatcherTypes() {
|
||||
return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR, DispatcherType.FORWARD,
|
||||
return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR, DispatcherType.ASYNC,
|
||||
DispatcherType.FORWARD, DispatcherType.INCLUDE);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,58 @@
|
||||
|
||||
The following steps relate to changes around how to configure `HttpSecurity`, `WebSecurity`, and `AuthenticationManager`.
|
||||
|
||||
[[add-configuration-annotation]]
|
||||
== Add `@Configuration` annotation
|
||||
|
||||
In 6.0, `@Configuration` is removed from `@EnableWebSecurity`, `@EnableMethodSecurity`, `@EnableGlobalMethodSecurity`, and `@EnableGlobalAuthentication`.
|
||||
|
||||
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
|
||||
For example, `@EnableMethodSecurity` changes from:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[use-new-requestmatchers]]
|
||||
== Use the new `requestMatchers` methods
|
||||
|
||||
|
||||
@@ -7,8 +7,20 @@ The following steps relate to changes around how to configure CSRF.
|
||||
In Spring Security 5, the default behavior is that the `CsrfToken` will be loaded on every request.
|
||||
This means that in a typical setup, the `HttpSession` must be read for every request even if it is unnecessary.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Some examples of where it should be unnecessary to read the session include endpoints marked `permitAll()` such as static assets, static HTML pages, single-page applications hosted under the same domain/server, etc.
|
||||
====
|
||||
|
||||
In Spring Security 6, the default is that the lookup of the `CsrfToken` will be deferred until it is needed.
|
||||
|
||||
[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].
|
||||
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.
|
||||
====
|
||||
|
||||
To opt into the new Spring Security 6 default, the following configuration can be used.
|
||||
|
||||
[[servlet-opt-in-defer-loading-csrf-token]]
|
||||
@@ -18,7 +30,7 @@ To opt into the new Spring Security 6 default, the following configuration can b
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
public SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName("_csrf");
|
||||
@@ -61,7 +73,156 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
====
|
||||
|
||||
If this breaks your application, then you can explicitly opt into the 5.8 defaults using the following configuration:
|
||||
[NOTE]
|
||||
====
|
||||
When the `CsrfToken` is deferred (the default in Spring Security 6), some applications may break due to the fact that they were designed with non-deferred CSRF tokens.
|
||||
See <<servlet-defer-loading-csrf-token-opt-out,Opt-out Steps>> below for more information.
|
||||
====
|
||||
|
||||
[[servlet-defer-loading-csrf-token-opt-out]]
|
||||
=== Opt-out Steps
|
||||
|
||||
If configuring the `CsrfToken` to be deferred gives you trouble, take a look at these scenarios for optimal opt out behavior:
|
||||
|
||||
==== I am using a Single-Page Application with `CookieCsrfTokenRepository`
|
||||
|
||||
If you are using a single-page app (SPA) to connect to a backend protected by Spring Security along with `CookieCsrfTokenRepository.withHttpOnlyFalse()`, you may find that the CSRF token is no longer returned to your application as a cookie on the first request to the server.
|
||||
|
||||
In this case, you have several options for restoring the behavior your client-side application expects.
|
||||
One option is to add a `Filter` that eagerly renders the `CsrfToken` to the response regardless of which request is made first, like so:
|
||||
|
||||
.Add a `Filter` to return a cookie on the response
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName("_csrf");
|
||||
http
|
||||
// ...
|
||||
.csrf((csrf) -> csrf
|
||||
.csrfTokenRepository(tokenRepository)
|
||||
.csrfTokenRequestHandler(requestHandler)
|
||||
)
|
||||
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private static final class CsrfCookieFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
// Render the token value to a cookie by causing the deferred token to be loaded
|
||||
csrfToken.getToken();
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
val tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
|
||||
val requestHandler = CsrfTokenRequestAttributeHandler()
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName("_csrf")
|
||||
http {
|
||||
csrf {
|
||||
csrfTokenRepository = tokenRepository
|
||||
csrfTokenRequestHandler = requestHandler
|
||||
}
|
||||
addFilterAfter<BasicAuthenticationFilter>(CsrfCookieFilter())
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
class CsrfCookieFilter : OncePerRequestFilter() {
|
||||
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
|
||||
val csrfToken = request.getAttribute(CsrfToken::class.java.name) as CsrfToken
|
||||
// Render the token value to a cookie by causing the deferred token to be loaded
|
||||
csrfToken.token
|
||||
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The option above does not require changes to the single-page application, but does cause the `CsrfToken` to be loaded on every request.
|
||||
If you do not wish to add a `Filter` to eagerly load tokens on every request, additional options are listed below.
|
||||
|
||||
==== I am using a Single-Page Application with `HttpSessionCsrfTokenRepository`
|
||||
|
||||
If you are using sessions, your application will benefit from deferred tokens.
|
||||
Instead of opting out, another option is to add a new `@RestController` with a `/csrf` endpoint, like so:
|
||||
|
||||
.Add a `/csrf` endpoint
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
public class CsrfController {
|
||||
|
||||
@GetMapping("/csrf")
|
||||
public CsrfToken csrf(CsrfToken csrfToken) {
|
||||
return csrfToken;
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
class CsrfController {
|
||||
|
||||
@GetMapping("/csrf")
|
||||
fun csrf(csrfToken: CsrfToken): CsrfToken {
|
||||
return csrfToken
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
You may consider adding `.requestMatchers("/csrf").permitAll()` if the endpoint above is required prior to authenticating with the server.
|
||||
====
|
||||
|
||||
The `/csrf` endpoint would need to be consumed by the client-side application in order to bootstrap the application for subsequent requests.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Instructions for calling the `/csrf` endpoint on application launch are specific to your client-side framework and therefore outside the scope of this document.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
While this requires changes to your single-page application, the benefit is that the CSRF token is only loaded once and the token can continue to be deferred.
|
||||
This approach works particularly well with applications that use `HttpSessionCsrfTokenRepository` and do benefit from deferred tokens by allowing the `HttpSession` not to be read on every request.
|
||||
====
|
||||
|
||||
If you simply wish to opt out of deferred tokens altogether, that option is listed next.
|
||||
|
||||
==== I need to opt out of deferred tokens for another reason
|
||||
|
||||
If deferred tokens break your application for another reason, then you can explicitly opt into the 5.8 defaults using the following configuration:
|
||||
|
||||
.Explicit Configure `CsrfToken` with 5.8 Defaults
|
||||
====
|
||||
@@ -69,7 +230,7 @@ If this breaks your application, then you can explicitly opt into the 5.8 defaul
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName(null);
|
||||
@@ -86,7 +247,7 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
val requestHandler = CsrfTokenRequestAttributeHandler()
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName(null)
|
||||
@@ -115,6 +276,12 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
By setting the `csrfRequestAttributeName` to `null`, the `CsrfToken` must first be loaded to determine what attribute name to use.
|
||||
This causes the `CsrfToken` to be loaded on every request.
|
||||
====
|
||||
|
||||
== Protect against CSRF BREACH
|
||||
|
||||
If the steps for <<Defer Loading CsrfToken>> work for you, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` using the following configuration:
|
||||
@@ -240,6 +407,83 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
====
|
||||
|
||||
This is the RECOMMENDED way to configure Spring Security to work with a client-side application that uses cookie values, because it continues to allow the response to return a randomized value for the CSRF token in case the application returns HTML or other responses that could be vulnerable to BREACH without your knowledge.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
BREACH protection works to protect the token when it is included in a response body that can be GZIP compressed, which generally does not include headers and cookies.
|
||||
====
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Any token value returned by the server can be used successfully by the client-side application because the underlying (raw) CSRF token does not change.
|
||||
It is not required for an AngularJS (or similar) application to refresh the CSRF token before/after every request.
|
||||
====
|
||||
|
||||
If you simply wish to opt out of CSRF BREACH protection altogether, that option is listed next.
|
||||
|
||||
==== I need to opt out of CSRF BREACH protection for another reason
|
||||
|
||||
If CSRF BREACH protection does not work for you for another reason, you can opt out using the configuration from the <<servlet-opt-in-defer-loading-csrf-token>> section.
|
||||
|
||||
== CSRF BREACH with WebSocket support
|
||||
|
||||
If the steps for <<Protect against CSRF BREACH>> work for normal HTTP requests and you are using xref:servlet/integrations/websocket.adoc[WebSocket Security] support, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` with xref:servlet/integrations/websocket.adoc#websocket-sameorigin-csrf[Stomp headers].
|
||||
|
||||
.WebSocket Security BREACH Protection
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
ChannelInterceptor csrfChannelInterceptor() {
|
||||
return new XorCsrfChannelInterceptor();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun csrfChannelInterceptor(): ChannelInterceptor {
|
||||
return XorCsrfChannelInterceptor()
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="csrfChannelInterceptor"
|
||||
class="org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor"/>
|
||||
----
|
||||
====
|
||||
|
||||
If configuring CSRF BREACH protection for WebSocket Security gives you trouble, you can configure the 5.8 default using the following configuration:
|
||||
|
||||
.Configure WebSocket Security with 5.8 default
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
ChannelInterceptor csrfChannelInterceptor() {
|
||||
return new CsrfChannelInterceptor();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun csrfChannelInterceptor(): ChannelInterceptor {
|
||||
return CsrfChannelInterceptor()
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="csrfChannelInterceptor"
|
||||
class="org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor"/>
|
||||
----
|
||||
====
|
||||
|
||||
@@ -11,6 +11,11 @@ In Spring Security 6, the default behavior is that the xref:servlet/authenticati
|
||||
Users now must explicitly save the `SecurityContext` with the `SecurityContextRepository` if they want the `SecurityContext` to persist between requests.
|
||||
This removes ambiguity and improves performance by only requiring writing to the `SecurityContextRepository` (i.e. `HttpSession`) when it is necessary.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Saving the context is also needed when clearing it out, for example during logout. Refer to this section to xref:servlet/authentication/session-management.adoc#properly-clearing-authentication[know more about that].
|
||||
====
|
||||
|
||||
To opt into the new Spring Security 6 default, the following configuration can be used.
|
||||
|
||||
include::partial$servlet/architecture/security-context-explicit.adoc[]
|
||||
|
||||
@@ -7,6 +7,8 @@ For example:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
||||
|
||||
@Test
|
||||
public void messageWhenNotAuthenticated() throws Exception {
|
||||
this.rest
|
||||
@@ -67,6 +69,7 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception {
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser
|
||||
|
||||
//...
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ For example:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
this.rest
|
||||
// provide a valid CSRF token
|
||||
.mutateWith(csrf())
|
||||
@@ -18,6 +20,8 @@ this.rest
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
|
||||
|
||||
this.rest
|
||||
// provide a valid CSRF token
|
||||
.mutateWith(csrf())
|
||||
|
||||
@@ -6,6 +6,9 @@ The basic setup looks like this:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
|
||||
public class HelloWebfluxMethodApplicationTests {
|
||||
@@ -31,6 +34,9 @@ public class HelloWebfluxMethodApplicationTests {
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class])
|
||||
class HelloWebfluxMethodApplicationTests {
|
||||
|
||||
@@ -265,6 +265,60 @@ The code below demonstrates how to customize the `RequestCache` implementation t
|
||||
|
||||
include::partial$servlet/architecture/request-cache-continue.adoc[]
|
||||
|
||||
[[requestcache-prevent-saved-request]]
|
||||
==== Prevent the Request From Being Saved
|
||||
|
||||
There are a number of reasons you may want to not store the user's unauthenticated request in the session.
|
||||
You may want to offload that storage onto the user's browser or store it in a database.
|
||||
Or you may want to shut off this feature since you always want to redirect the user to the home page instead of the page they tried to visit before login.
|
||||
|
||||
To do that, you can use {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[the `NullRequestCache` implementation].
|
||||
|
||||
.Prevent the Request From Being Saved
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
RequestCache nullRequestCache = new NullRequestCache();
|
||||
http
|
||||
// ...
|
||||
.requestCache((cache) -> cache
|
||||
.requestCache(nullRequestCache)
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
val nullRequestCache = NullRequestCache()
|
||||
http {
|
||||
requestCache {
|
||||
requestCache = nullRequestCache
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http auto-config="true">
|
||||
<!-- ... -->
|
||||
<request-cache ref="nullRequestCache"/>
|
||||
</http>
|
||||
|
||||
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
[[requestcacheawarefilter]]
|
||||
=== RequestCacheAwareFilter
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ The default is that accessing the URL `/logout` will log the user out by:
|
||||
- Invalidating the HTTP Session
|
||||
- Cleaning up any RememberMe authentication that was configured
|
||||
- Clearing the `SecurityContextHolder`
|
||||
- Clearing the `SecurityContextRepository`
|
||||
- Redirect to `/login?logout`
|
||||
|
||||
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
|
||||
@@ -139,6 +140,7 @@ If not configured a status code 200 will be returned by default.
|
||||
[[jc-logout-references]]
|
||||
== Further Logout-Related References
|
||||
|
||||
- xref:servlet/authentication/session-management.adoc#properly-clearing-authentication[Properly Clearing Authentication When Explicit Save Is Enabled]
|
||||
- <<ns-logout, Logout Handling>>
|
||||
- xref:servlet/test/mockmvc/logout.adoc#test-logout[ Testing Logout]
|
||||
- xref:servlet/integrations/servlet-api.adoc#servletapi-logout[ HttpServletRequest.logout()]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -413,3 +413,46 @@ open class SecurityConfig {
|
||||
<3> Allow access to URLs that start with `/user/` to users with the `USER` role, using `AntPathRequestMatcher`
|
||||
<4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
|
||||
<5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher`
|
||||
|
||||
== Expressions
|
||||
|
||||
It is recommended that you use type-safe authorization managers instead of SpEL.
|
||||
However, `WebExpressionAuthorizationManager` is available to help migrate legacy SpEL.
|
||||
|
||||
To use `WebExpressionAuthorizationManager`, you can construct one with the expression you are trying to migrate, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
.requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
.requestMatchers("/test/**").access(WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
|
||||
----
|
||||
====
|
||||
|
||||
If you are referring to a bean in your expression like so: `@webSecurity.check(authentication, request)`, it's recommended that you instead call the bean directly, which will look something like the following:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
.requestMatchers("/test/**").access((authentication, context) ->
|
||||
new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
.requestMatchers("/test/**").access((authentication, context): AuthorizationManager<RequestAuthorizationContext> ->
|
||||
AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
|
||||
----
|
||||
====
|
||||
|
||||
For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement `AuthorizationManager` and refer to them by calling `.access(AuthorizationManager)`.
|
||||
|
||||
If you are not able to do that, you can configure a `DefaultHttpSecurityExpressionHandler` with a bean resolver and supply that to `WebExpressionAuthorizationManager#setExpressionhandler`.
|
||||
|
||||
@@ -211,7 +211,7 @@ You could refer to the method using:
|
||||
----
|
||||
http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
|
||||
.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@webSecurity.checkUserId(authentication,#userId)"))
|
||||
...
|
||||
);
|
||||
----
|
||||
|
||||
@@ -548,7 +548,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public Advisor customAuthorize(AuthorizationManager<MethodInvocationResult> rules) {
|
||||
AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
|
||||
AnnotationMatchingPointcut pattern = new AnnotationMatchingPointcut(MySecurityAnnotation.class);
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
|
||||
return interceptor;
|
||||
@@ -564,7 +564,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun customAuthorize(rules : AuthorizationManager<MethodInvocationResult>) : Advisor {
|
||||
val pattern = AnnotationMethodMatcher(MySecurityAnnotation::class.java);
|
||||
val pattern = AnnotationMatchingPointcut(MySecurityAnnotation::class.java);
|
||||
val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
|
||||
return interceptor;
|
||||
|
||||
@@ -20,7 +20,7 @@ If you are using Spring Security, the `Principal` on the `HttpServletRequest` is
|
||||
|
||||
More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application.
|
||||
|
||||
[[websocket-configuration]]
|
||||
[[websocket-authorization]]
|
||||
== WebSocket Authorization
|
||||
|
||||
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
|
||||
|
||||
@@ -1,17 +1,97 @@
|
||||
[[servlet-saml2login-authenticate-responses]]
|
||||
= Authenticating ``<saml2:Response>``s
|
||||
|
||||
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default.
|
||||
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it.
|
||||
|
||||
You can configure this in a number of ways including:
|
||||
|
||||
1. Setting a clock skew to timestamp validation
|
||||
2. Mapping the response to a list of `GrantedAuthority` instances
|
||||
3. Customizing the strategy for validating assertions
|
||||
4. Customizing the strategy for decrypting response and assertion elements
|
||||
1. Changing the way the `RelyingPartyRegistration` is Looked Up
|
||||
2. Setting a clock skew to timestamp validation
|
||||
3. Mapping the response to a list of `GrantedAuthority` instances
|
||||
4. Customizing the strategy for validating assertions
|
||||
5. Customizing the strategy for decrypting response and assertion elements
|
||||
|
||||
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
|
||||
|
||||
[[relyingpartyregistrationresolver-apply]]
|
||||
== Changing `RelyingPartyRegistration` Lookup
|
||||
|
||||
`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`].
|
||||
|
||||
To apply a `RelyingPartyRegistrationResolver` when processing `<saml2:Response>` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) {
|
||||
return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations));
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter {
|
||||
return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations));
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default.
|
||||
If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso"))
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
// ...
|
||||
.saml2Login {
|
||||
filterProcessingUrl = "/saml2/login/sso"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
and:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
|
||||
== Setting a Clock Skew
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
[[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
|
||||
image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
|
||||
This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
|
||||
This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
|
||||
@@ -640,6 +641,16 @@ which in a deployed application would translate to
|
||||
|
||||
`+https://rp.example.com/adfs+`
|
||||
|
||||
The prevailing URI patterns are as follows:
|
||||
|
||||
* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
|
||||
* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`] based on the configurations for that `RelyingPartyRegistration`
|
||||
* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state
|
||||
* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration`
|
||||
|
||||
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
|
||||
If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
|
||||
|
||||
[[servlet-saml2login-rpr-credentials]]
|
||||
=== Credentials
|
||||
|
||||
@@ -712,56 +723,6 @@ resource.inputStream.use {
|
||||
[TIP]
|
||||
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
|
||||
|
||||
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
||||
=== Resolving the Relying Party from the Request
|
||||
|
||||
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
||||
|
||||
There are a number of reasons you may want to customize. Among them:
|
||||
|
||||
* You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
|
||||
* You may identify tenants in a way other than by the URI path
|
||||
|
||||
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
|
||||
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
|
||||
|
||||
You can provide a simpler resolver that, for example, always returns the same relying party:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
|
||||
private final RelyingPartyRegistrationResolver delegate;
|
||||
|
||||
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
|
||||
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
return this.delegate.resolve(request, "single");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
|
||||
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
|
||||
return this.delegate.resolve(request, "single")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Then, you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce ``<saml2:AuthnRequest>``s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ``<saml2:Response>``s], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
|
||||
|
||||
[NOTE]
|
||||
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
|
||||
|
||||
[[servlet-saml2login-rpr-duplicated]]
|
||||
=== Duplicated Relying Party Configurations
|
||||
|
||||
@@ -856,3 +817,184 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
||||
=== Resolving the `RelyingPartyRegistration` from the Request
|
||||
|
||||
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
||||
|
||||
There are a number of reasons you may want to customize that. Among them:
|
||||
|
||||
* You may already <<relyingpartyregistrationresolver-single, know which `RelyingPartyRegistration` you need>>
|
||||
* You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>>
|
||||
|
||||
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
|
||||
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
|
||||
|
||||
[NOTE]
|
||||
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
|
||||
|
||||
[[relyingpartyregistrationresolver-single]]
|
||||
==== Resolving to a Single Consistent `RelyingPartyRegistration`
|
||||
|
||||
You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
|
||||
private final RelyingPartyRegistrationResolver delegate;
|
||||
|
||||
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
|
||||
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
return this.delegate.resolve(request, "single");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
|
||||
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
|
||||
return this.delegate.resolve(request, "single")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`<saml2:SPSSODescriptor>` metadata production].
|
||||
|
||||
[[relyingpartyregistrationresolver-entityid]]
|
||||
==== Resolving Based on the `<saml2:Response#Issuer>`
|
||||
|
||||
When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <<servlet-saml2login-rpr-duplicated, relying party information duplicated across each instance>>.
|
||||
|
||||
This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable.
|
||||
|
||||
You can instead resolve the `registrationId` via the `Issuer`.
|
||||
A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
private final InMemoryRelyingPartyRegistrationRepository registrations;
|
||||
|
||||
// ... constructor
|
||||
|
||||
@Override
|
||||
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
if (registrationId != null) {
|
||||
return this.registrations.findByRegistrationId(registrationId);
|
||||
}
|
||||
String entityId = resolveEntityIdFromSamlResponse(request);
|
||||
for (RelyingPartyRegistration registration : this.registrations) {
|
||||
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
|
||||
RelyingPartyRegistrationResolver {
|
||||
@Override
|
||||
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
|
||||
if (registrationId != null) {
|
||||
return this.registrations.findByRegistrationId(registrationId)
|
||||
}
|
||||
String entityId = resolveEntityIdFromSamlResponse(request)
|
||||
for (val registration : this.registrations) {
|
||||
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
|
||||
return registration
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`<saml2:Response>` authentication].
|
||||
|
||||
[[federating-saml2-login]]
|
||||
=== Federating Login
|
||||
|
||||
One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties.
|
||||
In this case, the identity provider's metadata endpoint returns multiple `<md:IDPSSODescriptor>` elements.
|
||||
|
||||
These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
||||
.stream().map((builder) -> builder
|
||||
.registrationId(UUID.randomUUID().toString())
|
||||
.entityId("https://example.org/saml2/sp")
|
||||
.build()
|
||||
)
|
||||
.collect(Collectors.toList()));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
||||
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
|
||||
.registrationId(UUID.randomUUID().toString())
|
||||
.entityId("https://example.org/saml2/sp")
|
||||
.build()
|
||||
}
|
||||
.collect(Collectors.toList()));
|
||||
----
|
||||
====
|
||||
|
||||
Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable.
|
||||
There are several ways to address this; let's focus on a way that suits the specific use case of federation.
|
||||
|
||||
In many federation cases, all the asserting parties share service provider configuration.
|
||||
Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`.
|
||||
|
||||
There are two main URIs you will want to change along those lines:
|
||||
|
||||
* <<relyingpartyregistrationresolver-entityid,Resolve by `<saml2:Response#Issuer>`>>
|
||||
* <<relyingpartyregistrationresolver-single,Resolve with a default `RelyingPartyRegistration`>>
|
||||
|
||||
[NOTE]
|
||||
Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal.
|
||||
|
||||
You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|
||||
|
||||
[[using-spring-security-saml-extension-uris]]
|
||||
=== Using Spring Security SAML Extension URIs
|
||||
|
||||
In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.
|
||||
|
||||
For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
[[servlet-saml2login-metadata]]
|
||||
= Producing `<saml2:SPSSODescriptor>` Metadata
|
||||
= Saml 2.0 Metadata
|
||||
|
||||
Spring Security can <<parsing-asserting-party-metadata,parse asserting party metadata>> to produce an `AssertingPartyDetails` instance as well as <<publishing-relying-party-metadata,publish relying party metadata>> from a `RelyingPartyRegistration` instance.
|
||||
|
||||
[[parsing-asserting-party-metadata]]
|
||||
== Parsing `<saml2:IDPSSODescriptor>` metadata
|
||||
|
||||
You can parse an asserting party's metadata xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[using `RelyingPartyRegistrations`].
|
||||
|
||||
When using the OpenSAML vendor support, the resulting `AssertingPartyDetails` will be of type `OpenSamlAssertingPartyDetails`.
|
||||
This means you'll be able to do get the underlying OpenSAML XMLObject by doing the following:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
OpenSamlAssertingPartyDetails details = (OpenSamlAssertingPartyDetails)
|
||||
registration.getAssertingPartyDetails();
|
||||
EntityDescriptor openSamlEntityDescriptor = details.getEntityDescriptor();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val details: OpenSamlAssertingPartyDetails =
|
||||
registration.getAssertingPartyDetails() as OpenSamlAssertingPartyDetails;
|
||||
val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor();
|
||||
----
|
||||
====
|
||||
|
||||
[[publishing-relying-party-metadata]]
|
||||
== Producing `<saml2:SPSSODescriptor>` Metadata
|
||||
|
||||
You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
|
||||
|
||||
@@ -72,3 +103,45 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
||||
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
|
||||
----
|
||||
====
|
||||
|
||||
== Changing the Way a `RelyingPartyRegistration` Is Looked Up
|
||||
|
||||
To apply a custom `RelyingPartyRegistrationResolver` to the metadata endpoint, you can provide it directly in the filter constructor like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
RelyingPartyRegistrationResolver myRegistrationResolver = ...;
|
||||
Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver());
|
||||
|
||||
// ...
|
||||
|
||||
http.addFilterBefore(metadata, BasicAuthenticationFilter.class);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
----
|
||||
val myRegistrationResolver: RelyingPartyRegistrationResolver = ...;
|
||||
val metadata = new Saml2MetadataFilter(myRegistrationResolver, OpenSamlMetadataResolver());
|
||||
|
||||
// ...
|
||||
|
||||
http.addFilterBefore(metadata, BasicAuthenticationFilter::class.java);
|
||||
----
|
||||
====
|
||||
|
||||
In the event that you are applying a `RelyingPartyRegistrationResolver` to remove the `registrationId` from the URI, you must also change the URI in the filter like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
metadata.setRequestMatcher("/saml2/metadata")
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
----
|
||||
metadata.setRequestMatcher("/saml2/metadata")
|
||||
----
|
||||
====
|
||||
|
||||
@@ -61,6 +61,7 @@ public class WithMockUserTests {
|
||||
@ContextConfiguration
|
||||
class WithMockUserTests {
|
||||
----
|
||||
====
|
||||
|
||||
This is a basic example of how to setup Spring Security Test. The highlights are:
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): String? {
|
||||
----
|
||||
====
|
||||
|
||||
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2User` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
|
||||
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2Login` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
|
||||
@@ -1,78 +1,25 @@
|
||||
plugins {
|
||||
id 'org.antora' version '1.0.0-alpha.3'
|
||||
id 'org.antora' version '1.0.0'
|
||||
id 'io.spring.antora.generate-antora-yml' version '0.0.1'
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
antora {
|
||||
version = '3.2.0-alpha.2'
|
||||
playbook = file('local-antora-playbook.yml')
|
||||
options = ['--clean', '--stacktrace']
|
||||
environment = [
|
||||
'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691',
|
||||
'ALGOLIA_APP_ID': '244V8V9FGG',
|
||||
'ALGOLIA_INDEX_NAME': 'security-docs'
|
||||
]
|
||||
dependencies = [
|
||||
'@antora/collector-extension': '1.0.0-alpha.3'
|
||||
]
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-security'
|
||||
branch = 'docs-build'
|
||||
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
|
||||
checkLocalBranch = true
|
||||
}
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
}
|
||||
|
||||
tasks.register('generateAntora') {
|
||||
group = 'Documentation'
|
||||
description = 'Generates the antora.yml for dynamic properties'
|
||||
doLast {
|
||||
def docsTag = snapshotBuild ? 'current' : project.version
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
|
||||
def ghOldSamplesUrl = 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
|
||||
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
|
||||
def ymlVersions = resolvedVersions(project.configurations.testRuntimeClasspath).call()
|
||||
.collect(v -> " ${v.getKey()}: ${v.getValue()}")
|
||||
.join('\n')
|
||||
def outputFile = layout.buildDirectory.file('generateAntora/antora.yml').get().asFile
|
||||
mkdir(outputFile.getParentFile())
|
||||
def mainVersion = project.version
|
||||
def prerelease = null
|
||||
def versionComponents = mainVersion.split(/(?=-)/)
|
||||
if (versionComponents.length > 1) {
|
||||
if (versionComponents[1] == '-SNAPSHOT') {
|
||||
mainVersion = versionComponents[0]
|
||||
prerelease = "'-SNAPSHOT'"
|
||||
} else {
|
||||
prerelease = 'true'
|
||||
}
|
||||
}
|
||||
def antoraYmlText = file('antora.yml').text
|
||||
layout.buildDirectory.file('.antora.yml').get().asFile.text = antoraYmlText
|
||||
antoraYmlText = antoraYmlText.lines().collect { l ->
|
||||
if (l.startsWith('version: ')) {
|
||||
return prerelease == null ? "version: '${mainVersion}'" : "version: '${mainVersion}'\nprerelease: ${prerelease}"
|
||||
}
|
||||
if (l.startsWith('title: ')) return "title: ${project.parent.description}"
|
||||
return l == 'ext:' || l.getAt(0) == ' ' ? null : l
|
||||
}.findAll(Objects::nonNull).join('\n')
|
||||
outputFile.text = """$antoraYmlText
|
||||
asciidoc:
|
||||
attributes:
|
||||
icondir: icons
|
||||
gh-old-samples-url: $ghOldSamplesUrl
|
||||
gh-samples-url: $ghSamplesUrl
|
||||
gh-url: $ghUrl
|
||||
security-api-url: $securityApiUrl
|
||||
security-reference-url: $securityReferenceUrl
|
||||
spring-framework-api-url: $springFrameworkApiUrl
|
||||
spring-framework-reference-url: $springFrameworkReferenceUrl
|
||||
spring-security-version: ${project.version}
|
||||
${ymlVersions}
|
||||
"""
|
||||
}
|
||||
tasks.named("generateAntoraYml") {
|
||||
asciidocAttributes = project.provider( { generateAttributes() } )
|
||||
asciidocAttributes.putAll(providers.provider( { resolvedVersions(project.configurations.testRuntimeClasspath) }))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -82,12 +29,33 @@ dependencies {
|
||||
testImplementation 'org.springframework:spring-core'
|
||||
}
|
||||
|
||||
def generateAttributes() {
|
||||
def docsTag = snapshotBuild ? 'current' : project.version
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
|
||||
def ghOldSamplesUrl = 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
|
||||
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
|
||||
|
||||
return ['gh-old-samples-url': ghOldSamplesUrl.toString(),
|
||||
'gh-samples-url': ghSamplesUrl.toString(),
|
||||
'gh-url': ghUrl.toString(),
|
||||
'security-api-url': securityApiUrl.toString(),
|
||||
'security-reference-url': securityReferenceUrl.toString(),
|
||||
'spring-framework-api-url': springFrameworkApiUrl.toString(),
|
||||
'spring-framework-reference-url': springFrameworkReferenceUrl.toString(),
|
||||
'spring-security-version': project.version]
|
||||
+ resolvedVersions(project.configurations.testRuntimeClasspath)
|
||||
}
|
||||
|
||||
def resolvedVersions(Configuration configuration) {
|
||||
return {
|
||||
configuration.resolvedConfiguration
|
||||
return configuration.resolvedConfiguration
|
||||
.resolvedArtifacts
|
||||
.collectEntries { [(it.name + '-version'): it.moduleVersion.id.version] }
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
^http://www.w3.org/2000/09/xmldsig.*
|
||||
^http://www.w3.org/2001/10/xml-exc-c14n
|
||||
^http://www.w3.org/2001/04/xmldsig-more
|
||||
^http://www.w3.org/2001/04/xmlenc
|
||||
^http://www.w3.org/2001/04/xmlenc
|
||||
^http://www.springframework.org/schema/security/.*
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
aspectjVersion=1.9.9.1
|
||||
springJavaformatVersion=0.0.35
|
||||
aspectjVersion=1.9.19
|
||||
springJavaformatVersion=0.0.38
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=5.3.24
|
||||
springFrameworkVersion=5.3.27
|
||||
openSamlVersion=3.4.6
|
||||
version=5.8.1-SNAPSHOT
|
||||
version=5.8.3
|
||||
kotlinVersion=1.7.22
|
||||
samplesBranch=5.8.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.messaging.web.csrf;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
|
||||
/**
|
||||
* {@link ChannelInterceptor} that validates a CSRF token masked by the
|
||||
* {@link org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler} in
|
||||
* the header of any {@link SimpMessageType#CONNECT} message.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 5.8
|
||||
*/
|
||||
public final class XorCsrfChannelInterceptor implements ChannelInterceptor {
|
||||
|
||||
private final MessageMatcher<Object> matcher = new SimpMessageTypeMatcher(SimpMessageType.CONNECT);
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
if (!this.matcher.matches(message)) {
|
||||
return message;
|
||||
}
|
||||
Map<String, Object> sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
|
||||
CsrfToken expectedToken = (sessionAttributes != null)
|
||||
? (CsrfToken) sessionAttributes.get(CsrfToken.class.getName()) : null;
|
||||
if (expectedToken == null) {
|
||||
throw new MissingCsrfTokenException(null);
|
||||
}
|
||||
String actualToken = SimpMessageHeaderAccessor.wrap(message)
|
||||
.getFirstNativeHeader(expectedToken.getHeaderName());
|
||||
String actualTokenValue = XorCsrfTokenUtils.getTokenValue(actualToken, expectedToken.getToken());
|
||||
boolean csrfCheckPassed = equalsConstantTime(expectedToken.getToken(), actualTokenValue);
|
||||
if (!csrfCheckPassed) {
|
||||
throw new InvalidCsrfTokenException(expectedToken, actualToken);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant time comparison to prevent against timing attacks.
|
||||
* @param expected
|
||||
* @param actual
|
||||
* @return
|
||||
*/
|
||||
private static boolean equalsConstantTime(String expected, String actual) {
|
||||
if (expected == actual) {
|
||||
return true;
|
||||
}
|
||||
if (expected == null || actual == null) {
|
||||
return false;
|
||||
}
|
||||
// Encode after ensure that the string is not null
|
||||
byte[] expectedBytes = Utf8.encode(expected);
|
||||
byte[] actualBytes = Utf8.encode(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.messaging.web.csrf;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
|
||||
/**
|
||||
* Copied from
|
||||
* {@link org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler}.
|
||||
*
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-projects/spring-security/issues/12378">gh-12378</a>
|
||||
*/
|
||||
final class XorCsrfTokenUtils {
|
||||
|
||||
private XorCsrfTokenUtils() {
|
||||
}
|
||||
|
||||
static String getTokenValue(String actualToken, String token) {
|
||||
byte[] actualBytes;
|
||||
try {
|
||||
actualBytes = Base64.getUrlDecoder().decode(actualToken);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
return xoredCsrf;
|
||||
}
|
||||
|
||||
}
|
||||
+18
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.
|
||||
@@ -24,15 +24,18 @@ import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
/**
|
||||
* Copies a CsrfToken from the HttpServletRequest's attributes to the WebSocket
|
||||
* attributes. This is used as the expected CsrfToken when validating connection requests
|
||||
* to ensure only the same origin connects.
|
||||
* Loads a CsrfToken from the HttpServletRequest and HttpServletResponse to populate the
|
||||
* WebSocket attributes. This is used as the expected CsrfToken when validating connection
|
||||
* requests to ensure only the same origin connects.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Steve Riesenberg
|
||||
* @since 4.0
|
||||
*/
|
||||
public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor {
|
||||
@@ -41,11 +44,19 @@ public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
|
||||
Map<String, Object> attributes) {
|
||||
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
CsrfToken token = (CsrfToken) httpRequest.getAttribute(CsrfToken.class.getName());
|
||||
if (token == null) {
|
||||
DeferredCsrfToken deferredCsrfToken = (DeferredCsrfToken) httpRequest
|
||||
.getAttribute(DeferredCsrfToken.class.getName());
|
||||
if (deferredCsrfToken == null) {
|
||||
return true;
|
||||
}
|
||||
attributes.put(CsrfToken.class.getName(), token);
|
||||
CsrfToken csrfToken = deferredCsrfToken.get();
|
||||
// Ensure the values of the CsrfToken are copied into a new token so the old token
|
||||
// is available for garbage collection.
|
||||
// This is required because the original token could hold a reference to the
|
||||
// HttpServletRequest/Response of the handshake request.
|
||||
CsrfToken resolvedCsrfToken = new DefaultCsrfToken(csrfToken.getHeaderName(), csrfToken.getParameterName(),
|
||||
csrfToken.getToken());
|
||||
attributes.put(CsrfToken.class.getName(), resolvedCsrfToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.messaging.web.csrf;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link XorCsrfChannelInterceptor}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class XorCsrfChannelInterceptorTests {
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = "wpe7zB62-NCpcA==";
|
||||
|
||||
private static final String INVALID_XOR_CSRF_TOKEN_VALUE = "KneoaygbRZtfHQ==";
|
||||
|
||||
private CsrfToken token;
|
||||
|
||||
private SimpMessageHeaderAccessor messageHeaders;
|
||||
|
||||
private MessageChannel channel;
|
||||
|
||||
private XorCsrfChannelInterceptor interceptor;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.token = new DefaultCsrfToken("header", "param", "token");
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
|
||||
this.messageHeaders.setSessionAttributes(new HashMap<>());
|
||||
this.channel = mock(MessageChannel.class);
|
||||
this.interceptor = new XorCsrfChannelInterceptor();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithValidTokenThenSuccess() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithInvalidTokenThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), INVALID_XOR_CSRF_TOKEN_VALUE);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithNoTokenThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithMissingTokenThenThrowsMissingCsrfTokenException() {
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(MissingCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithNullSessionAttributesThenThrowsMissingCsrfTokenException() {
|
||||
this.messageHeaders.setSessionAttributes(null);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(MissingCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenAckThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenDisconnectThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenHeartbeatThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenMessageThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenOtherThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.OTHER);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenUnsubscribeThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.UNSUBSCRIBE);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
private Message<String> message() {
|
||||
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
|
||||
}
|
||||
|
||||
}
|
||||
+32
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.
|
||||
@@ -31,6 +31,7 @@ import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -72,10 +73,38 @@ public class CsrfTokenHandshakeInterceptorTests {
|
||||
@Test
|
||||
public void beforeHandshake() throws Exception {
|
||||
CsrfToken token = new DefaultCsrfToken("header", "param", "token");
|
||||
this.httpRequest.setAttribute(CsrfToken.class.getName(), token);
|
||||
this.httpRequest.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(token));
|
||||
this.interceptor.beforeHandshake(this.request, this.response, this.wsHandler, this.attributes);
|
||||
assertThat(this.attributes.keySet()).containsOnly(CsrfToken.class.getName());
|
||||
assertThat(this.attributes.values()).containsOnly(token);
|
||||
CsrfToken csrfToken = (CsrfToken) this.attributes.get(CsrfToken.class.getName());
|
||||
assertThat(csrfToken.getHeaderName()).isEqualTo(token.getHeaderName());
|
||||
assertThat(csrfToken.getParameterName()).isEqualTo(token.getParameterName());
|
||||
assertThat(csrfToken.getToken()).isEqualTo(token.getToken());
|
||||
// Ensure the values of the CsrfToken are copied into a new token so the old token
|
||||
// is available for garbage collection.
|
||||
// This is required because the original token could hold a reference to the
|
||||
// HttpServletRequest/Response of the handshake request.
|
||||
assertThat(csrfToken).isNotSameAs(token);
|
||||
}
|
||||
|
||||
private static final class TestDeferredCsrfToken implements DeferredCsrfToken {
|
||||
|
||||
private final CsrfToken csrfToken;
|
||||
|
||||
private TestDeferredCsrfToken(CsrfToken csrfToken) {
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CsrfToken get() {
|
||||
return this.csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-7
@@ -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.
|
||||
@@ -55,6 +55,8 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor;
|
||||
import com.nimbusds.jwt.proc.JWTProcessor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@@ -388,15 +390,19 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
});
|
||||
ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri);
|
||||
source.setWebClient(this.webClient);
|
||||
Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = getExpectedJwsAlgorithms(jwsKeySelector);
|
||||
Mono<ConfigurableJWTProcessor<JWKSecurityContext>> jwtProcessorMono = this.jwtProcessorCustomizer
|
||||
Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = this.jwtProcessorCustomizer
|
||||
.apply(source, jwtProcessor)
|
||||
.map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
|
||||
.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
|
||||
return (jwt) -> {
|
||||
JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
|
||||
return jwtProcessorMono.flatMap((processor) -> source.get(selector)
|
||||
.onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex))
|
||||
.map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList))));
|
||||
return jwtProcessorMono.flatMap((tuple) -> {
|
||||
JWTProcessor<JWKSecurityContext> processor = tuple.getT1();
|
||||
Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = tuple.getT2();
|
||||
JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
|
||||
return source.get(selector)
|
||||
.onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex))
|
||||
.map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList)));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+12
-17
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* A {@link JwtDecoder} that lazily initializes another {@link JwtDecoder}
|
||||
*
|
||||
@@ -26,12 +28,17 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
public final class SupplierJwtDecoder implements JwtDecoder {
|
||||
|
||||
private final Supplier<JwtDecoder> jwtDecoderSupplier;
|
||||
|
||||
private volatile JwtDecoder delegate;
|
||||
private final Supplier<JwtDecoder> delegate;
|
||||
|
||||
public SupplierJwtDecoder(Supplier<JwtDecoder> jwtDecoderSupplier) {
|
||||
this.jwtDecoderSupplier = jwtDecoderSupplier;
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
try {
|
||||
return jwtDecoderSupplier.get();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw wrapException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,19 +46,7 @@ public final class SupplierJwtDecoder implements JwtDecoder {
|
||||
*/
|
||||
@Override
|
||||
public Jwt decode(String token) throws JwtException {
|
||||
if (this.delegate == null) {
|
||||
synchronized (this.jwtDecoderSupplier) {
|
||||
if (this.delegate == null) {
|
||||
try {
|
||||
this.delegate = this.jwtDecoderSupplier.get();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw wrapException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.delegate.decode(token);
|
||||
return this.delegate.get().decode(token);
|
||||
}
|
||||
|
||||
private JwtDecoderInitializationException wrapException(Exception ex) {
|
||||
|
||||
+2
@@ -60,6 +60,7 @@ import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.cache.support.SimpleValueWrapper;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -700,6 +701,7 @@ public class NimbusJwtDecoderTests {
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
Cache cache = mock(Cache.class);
|
||||
given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET);
|
||||
given(cache.get(eq(JWK_SET_URI))).willReturn(new SimpleValueWrapper(JWK_SET));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK));
|
||||
|
||||
|
||||
+17
-1
@@ -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.
|
||||
@@ -39,6 +39,8 @@ import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
|
||||
import com.nimbusds.jose.proc.JWKSecurityContext;
|
||||
@@ -365,6 +367,20 @@ public class NimbusReactiveJwtDecoderTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withJwkSetUriWhenJwtProcessorCustomizerSetsJWSKeySelectorThenUseCustomizedJWSKeySelector()
|
||||
throws InvalidKeySpecException {
|
||||
WebClient webClient = mockJwkSetResponse(new JWKSet(new RSAKey.Builder(key()).build()).toString());
|
||||
// @formatter:off
|
||||
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
|
||||
.jwsAlgorithm(SignatureAlgorithm.ES256).webClient(webClient)
|
||||
.jwtProcessorCustomizer((p) -> p
|
||||
.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.RS512, new JWKSecurityContextJWKSet())))
|
||||
.build();
|
||||
assertThat(decoder.decode(this.rsa512).block()).extracting(Jwt::getSubject).isEqualTo("test-subject");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withPublicKeyWhenNullThenThrowsException() {
|
||||
// @formatter:off
|
||||
|
||||
+2
-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.
|
||||
@@ -60,7 +60,7 @@ public final class Saml2LogoutRequest implements Serializable {
|
||||
|
||||
private final String relyingPartyRegistrationId;
|
||||
|
||||
private Function<Map<String, String>, String> encoder;
|
||||
private transient Function<Map<String, String>, String> encoder;
|
||||
|
||||
private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map<String, String> parameters, String id,
|
||||
String relyingPartyRegistrationId) {
|
||||
|
||||
+7
-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.
|
||||
@@ -71,7 +71,12 @@ class OpenSamlMetadataAssertingPartyDetailsConverter {
|
||||
if (xmlObject instanceof EntitiesDescriptor) {
|
||||
EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject;
|
||||
for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
|
||||
builders.add(convert(descriptor));
|
||||
if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
|
||||
builders.add(convert(descriptor));
|
||||
}
|
||||
}
|
||||
if (builders.isEmpty()) {
|
||||
throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements");
|
||||
}
|
||||
return builders;
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.saml2.provider.service.registration;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
class OpenSamlMetadataRelyingPartyRegistrationConverter {
|
||||
|
||||
private final OpenSamlMetadataAssertingPartyDetailsConverter converter = new OpenSamlMetadataAssertingPartyDetailsConverter();
|
||||
|
||||
Collection<RelyingPartyRegistration.Builder> convert(InputStream source) {
|
||||
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
|
||||
for (RelyingPartyRegistration.AssertingPartyDetails.Builder builder : this.converter.convert(source)) {
|
||||
builders.add(new RelyingPartyRegistration.Builder(builder));
|
||||
}
|
||||
return builders;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-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.
|
||||
@@ -89,8 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
|
||||
@Override
|
||||
public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
|
||||
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
||||
return RelyingPartyRegistration
|
||||
.withAssertingPartyDetails(this.converter.convert(inputMessage.getBody()).iterator().next().build());
|
||||
return new RelyingPartyRegistration.Builder(this.converter.convert(inputMessage.getBody()).iterator().next());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+25
-10
@@ -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.
|
||||
@@ -30,6 +30,7 @@ import java.util.function.Function;
|
||||
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -986,6 +987,14 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
|
||||
|
||||
private Builder() {
|
||||
|
||||
}
|
||||
|
||||
private Builder(AssertingPartyDetails.Builder assertingPartyDetailsBuilder) {
|
||||
this.assertingPartyDetailsBuilder = assertingPartyDetailsBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the asserting party's <a href=
|
||||
* "https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor">EntityID</a>.
|
||||
@@ -1048,7 +1057,7 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String registrationId;
|
||||
private Converter<ProviderDetails, String> registrationId = ProviderDetails::getEntityId;
|
||||
|
||||
private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
|
||||
|
||||
@@ -1068,12 +1077,17 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
private String nameIdFormat = null;
|
||||
|
||||
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
|
||||
private ProviderDetails.Builder providerDetails;
|
||||
|
||||
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new LinkedHashSet<>();
|
||||
|
||||
private Builder(String registrationId) {
|
||||
this.registrationId = registrationId;
|
||||
this.registrationId = (party) -> registrationId;
|
||||
this.providerDetails = new ProviderDetails.Builder();
|
||||
}
|
||||
|
||||
Builder(AssertingPartyDetails.Builder builder) {
|
||||
this.providerDetails = new ProviderDetails.Builder(builder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1082,7 +1096,7 @@ public final class RelyingPartyRegistration {
|
||||
* @return this object
|
||||
*/
|
||||
public Builder registrationId(String id) {
|
||||
this.registrationId = id;
|
||||
this.registrationId = (party) -> id;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1405,11 +1419,12 @@ public final class RelyingPartyRegistration {
|
||||
this.singleLogoutServiceBindings.add(Saml2MessageBinding.POST);
|
||||
}
|
||||
|
||||
return new RelyingPartyRegistration(this.registrationId, this.entityId,
|
||||
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
|
||||
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||
this.singleLogoutServiceBindings, this.providerDetails.build(), this.nameIdFormat, this.credentials,
|
||||
this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
ProviderDetails party = this.providerDetails.build();
|
||||
String registrationId = this.registrationId.convert(party);
|
||||
return new RelyingPartyRegistration(registrationId, this.entityId, this.assertionConsumerServiceLocation,
|
||||
this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation,
|
||||
this.singleLogoutServiceResponseLocation, this.singleLogoutServiceBindings, party,
|
||||
this.nameIdFormat, this.credentials, this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-9
@@ -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,13 +18,11 @@ package org.springframework.security.saml2.provider.service.registration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails;
|
||||
|
||||
/**
|
||||
* A utility class for constructing instances of {@link RelyingPartyRegistration}
|
||||
@@ -36,7 +34,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
|
||||
*/
|
||||
public final class RelyingPartyRegistrations {
|
||||
|
||||
private static final OpenSamlMetadataAssertingPartyDetailsConverter assertingPartyMetadataConverter = new OpenSamlMetadataAssertingPartyDetailsConverter();
|
||||
private static final OpenSamlMetadataRelyingPartyRegistrationConverter relyingPartyRegistrationConverter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
|
||||
|
||||
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
@@ -215,11 +213,7 @@ public final class RelyingPartyRegistrations {
|
||||
* @since 5.7
|
||||
*/
|
||||
public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadata(InputStream source) {
|
||||
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
|
||||
for (AssertingPartyDetails.Builder builder : assertingPartyMetadataConverter.convert(source)) {
|
||||
builders.add(RelyingPartyRegistration.withAssertingPartyDetails(builder.build()));
|
||||
}
|
||||
return builders;
|
||||
return relyingPartyRegistrationConverter.convert(source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-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.
|
||||
@@ -31,10 +31,12 @@ import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.NameIDPolicy;
|
||||
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller;
|
||||
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
@@ -71,6 +73,8 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
|
||||
private final NameIDBuilder nameIdBuilder;
|
||||
|
||||
private final NameIDPolicyBuilder nameIdPolicyBuilder;
|
||||
|
||||
private RequestMatcher requestMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}");
|
||||
|
||||
private Converter<HttpServletRequest, String> relayStateResolver = (request) -> UUID.randomUUID().toString();
|
||||
@@ -95,6 +99,9 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML");
|
||||
this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.nameIdBuilder, "nameIdBuilder must be configured in OpenSAML");
|
||||
this.nameIdPolicyBuilder = (NameIDPolicyBuilder) registry.getBuilderFactory()
|
||||
.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.nameIdPolicyBuilder, "nameIdPolicyBuilder must be configured in OpenSAML");
|
||||
}
|
||||
|
||||
void setRelayStateResolver(Converter<HttpServletRequest, String> relayStateResolver) {
|
||||
@@ -130,6 +137,11 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
authnRequest.setIssuer(iss);
|
||||
authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
|
||||
authnRequest.setAssertionConsumerServiceURL(registration.getAssertionConsumerServiceLocation());
|
||||
if (registration.getNameIdFormat() != null) {
|
||||
NameIDPolicy nameIdPolicy = this.nameIdPolicyBuilder.buildObject();
|
||||
nameIdPolicy.setFormat(registration.getNameIdFormat());
|
||||
authnRequest.setNameIDPolicy(nameIdPolicy);
|
||||
}
|
||||
authnRequestConsumer.accept(registration, authnRequest);
|
||||
if (authnRequest.getID() == null) {
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
@@ -674,7 +675,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
attributeMap.addAll(attribute.getName(), attributeValues);
|
||||
}
|
||||
}
|
||||
return attributeMap;
|
||||
return new LinkedHashMap<>(attributeMap); // gh-11785
|
||||
}
|
||||
|
||||
private static List<String> getSessionIndexes(Assertion assertion) {
|
||||
|
||||
+19
@@ -32,6 +32,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
@@ -68,6 +69,7 @@ import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
@@ -349,6 +351,23 @@ public class OpenSaml4AuthenticationProviderTests {
|
||||
assertThat(principal.getSessionIndexes()).contains("session-index");
|
||||
}
|
||||
|
||||
// gh-11785
|
||||
@Test
|
||||
public void deserializeWhenAssertionContainsAttributesThenWorks() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ClassLoader loader = getClass().getClassLoader();
|
||||
mapper.registerModules(SecurityJackson2Modules.getModules(loader));
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
List<AttributeStatement> attributes = TestOpenSamlObjects.attributeStatements();
|
||||
assertion.getAttributeStatements().addAll(attributes);
|
||||
response.getAssertions().add(signed(assertion));
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
Authentication authentication = this.provider.authenticate(token);
|
||||
String result = mapper.writeValueAsString(authentication);
|
||||
mapper.readValue(result, Authentication.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAssertionContainsCustomAttributesThenItSucceeds() {
|
||||
Response response = response();
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class Saml2AuthenticationExceptionMixinTest {
|
||||
class Saml2AuthenticationExceptionMixinTests {
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.saml2.provider.service.registration;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
|
||||
|
||||
private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
|
||||
|
||||
private String metadata;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
ClassPathResource resource = new ClassPathResource("test-metadata.xml");
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
|
||||
this.metadata = reader.lines().collect(Collectors.joining());
|
||||
}
|
||||
}
|
||||
|
||||
// gh-12667
|
||||
@Test
|
||||
public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception {
|
||||
try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) {
|
||||
this.converter.convert(source)
|
||||
.forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails())
|
||||
.isInstanceOf(OpenSamlAssertingPartyDetails.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+14
@@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -243,4 +244,17 @@ public class RelyingPartyRegistrationsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionFromMetadataLocationCanHandleFederationMetadata() {
|
||||
Collection<RelyingPartyRegistration.Builder> federationMetadataWithSkippedSPEntries = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("classpath:test-federated-metadata.xml");
|
||||
assertThat(federationMetadataWithSkippedSPEntries.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionFromMetadataLocationWithoutIdpThenSaml2Exception() {
|
||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("classpath:test-metadata-without-idp.xml"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -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.
|
||||
@@ -38,7 +38,7 @@ public final class TestRelyingPartyRegistrations {
|
||||
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
||||
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId).nameIdFormat("format")
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
|
||||
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
|
||||
|
||||
+5
-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.
|
||||
@@ -52,6 +52,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -75,6 +76,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -110,6 +112,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -132,6 +135,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
|
||||
+23
-1
@@ -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.
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -77,6 +82,23 @@ public class HttpSessionLogoutRequestRepositoryTests {
|
||||
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isEqualTo(two);
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializeAndDeserializeSaml2LogoutRequest() throws IOException, ClassNotFoundException {
|
||||
Saml2LogoutRequest requestToSerialize = createLogoutRequest().relayState("state-serialized").build();
|
||||
byte[] data;
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
|
||||
objectOutputStream.writeObject(requestToSerialize);
|
||||
data = outputStream.toByteArray();
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
|
||||
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream)) {
|
||||
Saml2LogoutRequest deserializedRequest = (Saml2LogoutRequest) objectInputStream.readObject();
|
||||
assertThat(requestToSerialize.getRelayState()).isEqualTo(deserializedRequest.getRelayState());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadLogoutRequestWhenSavedAndStateParameterNullThenReturnNull() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="federation_root"
|
||||
cacheDuration="P0Y0M0DT0H15M0.000S" validUntil="2099-03-04T20:18:29.383Z">
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
|
||||
entityID="https://localhost/simplesaml/saml2/idp/metadata.php">
|
||||
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="https://localhost/simplesaml/saml2/idp/SingleLogoutService.php"/>
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
|
||||
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="https://localhost/simplesaml/saml2/idp/SSOService.php"/>
|
||||
</md:IDPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
<md:EntityDescriptor entityID="https://service.provider.org">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:Extensions>
|
||||
<idpdisco:DiscoveryResponse xmlns:idpdisco="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://nomp.se/saml/login?disco=true" index="0"/>
|
||||
</md:Extensions>
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SSO" index="0" isDefault="true"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sp.provider.org/saml/SSO" index="1"/>
|
||||
<md:AttributeConsumingService index="0">
|
||||
<md:ServiceName xml:lang="en">The SP</md:ServiceName>
|
||||
<md:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="eduPersonPrincipalName" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42"/>
|
||||
<md:RequestedAttribute FriendlyName="surName" Name="urn:oid:2.5.4.4"/>
|
||||
</md:AttributeConsumingService>
|
||||
</md:SPSSODescriptor>
|
||||
<md:Organization>
|
||||
<md:OrganizationName xml:lang="en">Service Provider</md:OrganizationName>
|
||||
<md:OrganizationDisplayName xml:lang="en">Service Provider</md:OrganizationDisplayName>
|
||||
</md:Organization>
|
||||
</md:EntityDescriptor>
|
||||
</md:EntitiesDescriptor>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="federation_root"
|
||||
cacheDuration="P0Y0M0DT0H15M0.000S" validUntil="2099-03-04T20:18:29.383Z">
|
||||
<md:EntityDescriptor entityID="https://service.provider.org">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:Extensions>
|
||||
<idpdisco:DiscoveryResponse xmlns:idpdisco="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://nomp.se/saml/login?disco=true" index="0"/>
|
||||
</md:Extensions>
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDhzCCAm+gAwIBAgIEQ4NWOjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJTRTESMBAGA1UE CBMJU3RvY2tob2xtMRMwEQYDVQQHEwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjEN MAsGA1UECxMETk9NUDEXMBUGA1UEAxMOU3RlZmFuIE5vcmJlcmcwHhcNMTgwNzAxMTEzODUwWhcN MzgwNjI2MTEzODUwWjB0MQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRMwEQYDVQQH EwpTdW5kYnliZXJnMRQwEgYDVQQKEwtTZWxlc3NpYSBBQjENMAsGA1UECxMETk9NUDEXMBUGA1UE AxMOU3RlZmFuIE5vcmJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxGtC9ZwND QipHu5MslBANi/+k9CQPK4uHrfmVl8porr8pUWDlpVIGnfbJSc/glZQXCy/xbi79RfF/sFsTrmlb acMSSwwA0TYjJPBsx/MUBKdYQaei91b2IhP2yLSCWug+/A4fF3l/kUcqtX3SPhXpAESjbapyrKzp n1KWjDl7anV/kelOYdFGDATQWhUnslMml1hSeOgaaKQIbFzUH5yOw4RQ52zQkYP8wXF3h8BSP3LD tlSjP1Owme+UDjD+517zCaYHqV0RexDMU7h30m5a6YQeDdhJU02Ene86WhFfssqC+4HpL5g8KcbF T8vYY7Phe/7NqxUYXCaQlxTYHWWdAgMBAAGjITAfMB0GA1UdDgQWBBTv2MiZukGzYLRO/UsRUjvW AreSATANBgkqhkiG9w0BAQsFAAOCAQEACPkF8vkFWNEJDYsuNINKo3qUD9351gjHXo8ZNBbPzi23 xvMWHObYtkZb8+CGxEzI41hhZDnUSIu3CrpwVkf26hnKC6TyrdPsURN1CkdBwcUzjFdo3ZkZo4Uu RJtDBcn/DdZ86mMkEArojWzgleZCe37+7hEm5K/sRuxdT9wfqzprw9tOp/b7Y8423yGwW3+E+aef pKxbZyLCkabo1CT54PoCuypfNcQsSRDF0rmA0mQwfcmgVVkiNPkvQFO6VuNJsQjesxMN3QXSJf7v yqB3Y0IzGVC669FHsEF178Re0WJn4GwIR2UronR38dVdGEEMesyMPgwbww7U77qUkQLdug==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sp.provider.org/saml/SingleLogout"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sp.provider.org/saml/SSO" index="0" isDefault="true"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sp.provider.org/saml/SSO" index="1"/>
|
||||
<md:AttributeConsumingService index="0">
|
||||
<md:ServiceName xml:lang="en">The SP</md:ServiceName>
|
||||
<md:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="eduPersonPrincipalName" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" isRequired="true"/>
|
||||
<md:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42"/>
|
||||
<md:RequestedAttribute FriendlyName="surName" Name="urn:oid:2.5.4.4"/>
|
||||
</md:AttributeConsumingService>
|
||||
</md:SPSSODescriptor>
|
||||
<md:Organization>
|
||||
<md:OrganizationName xml:lang="en">Service Provider</md:OrganizationName>
|
||||
<md:OrganizationDisplayName xml:lang="en">Service Provider</md:OrganizationDisplayName>
|
||||
</md:Organization>
|
||||
</md:EntityDescriptor>
|
||||
</md:EntitiesDescriptor>
|
||||
+2
-2
@@ -42,9 +42,9 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = SecurityMockMvcResultHandlersTest.Config.class)
|
||||
@ContextConfiguration(classes = SecurityMockMvcResultHandlersTests.Config.class)
|
||||
@WebAppConfiguration
|
||||
public class SecurityMockMvcResultHandlersTest {
|
||||
public class SecurityMockMvcResultHandlersTests {
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext context;
|
||||
@@ -26,6 +26,7 @@ import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -257,7 +258,11 @@ public class FilterInvocation {
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaders(String name) {
|
||||
return Collections.enumeration(this.headers.get(name));
|
||||
List<String> headerList = this.headers.get(name);
|
||||
if (headerList == null) {
|
||||
return Collections.emptyEnumeration();
|
||||
}
|
||||
return Collections.enumeration(headerList);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+16
@@ -28,6 +28,8 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -54,6 +56,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
|
||||
|
||||
private boolean clearAuthentication = true;
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
/**
|
||||
* Requires the request to be passed in.
|
||||
* @param request from which to obtain a HTTP session (cannot be null)
|
||||
@@ -77,6 +81,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
|
||||
if (this.clearAuthentication) {
|
||||
context.setAuthentication(null);
|
||||
}
|
||||
SecurityContext emptyContext = this.securityContextHolderStrategy.createEmptyContext();
|
||||
this.securityContextRepository.saveContext(emptyContext, request, response);
|
||||
}
|
||||
|
||||
public boolean isInvalidateHttpSession() {
|
||||
@@ -115,4 +121,14 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
|
||||
this.clearAuthentication = clearAuthentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextRepository} to use. Default is
|
||||
* {@link HttpSessionSecurityContextRepository}.
|
||||
* @param securityContextRepository the {@link SecurityContextRepository} to use.
|
||||
*/
|
||||
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
|
||||
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
@@ -59,6 +59,9 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -146,6 +149,8 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
|
||||
private AuthenticationFailureHandler failureHandler;
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
|
||||
@@ -183,6 +188,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
context.setAuthentication(targetUser);
|
||||
this.securityContextHolderStrategy.setContext(context);
|
||||
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", targetUser));
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
// redirect to target url
|
||||
this.successHandler.onAuthenticationSuccess(request, response, targetUser);
|
||||
}
|
||||
@@ -200,6 +206,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
context.setAuthentication(originalUser);
|
||||
this.securityContextHolderStrategy.setContext(context);
|
||||
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", originalUser));
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
// redirect to target url
|
||||
this.successHandler.onAuthenticationSuccess(request, response, originalUser);
|
||||
return;
|
||||
@@ -525,6 +532,19 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
|
||||
* switch user success. The default is
|
||||
* {@link RequestAttributeSecurityContextRepository}.
|
||||
* @param securityContextRepository the {@link SecurityContextRepository} to use.
|
||||
* Cannot be null.
|
||||
* @since 5.7.7
|
||||
*/
|
||||
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
|
||||
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
private static RequestMatcher createMatcher(String pattern) {
|
||||
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
|
||||
}
|
||||
|
||||
+9
-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.
|
||||
@@ -46,7 +46,14 @@ public final class DelegatingSecurityContextRepository implements SecurityContex
|
||||
|
||||
@Override
|
||||
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
|
||||
return loadContext(requestResponseHolder.getRequest()).get();
|
||||
SecurityContext result = null;
|
||||
for (SecurityContextRepository delegate : this.delegates) {
|
||||
SecurityContext delegateResult = delegate.loadContext(requestResponseHolder);
|
||||
if (result == null || delegate.containsContext(requestResponseHolder.getRequest())) {
|
||||
result = delegateResult;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+38
-8
@@ -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.
|
||||
@@ -150,13 +150,46 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
|
||||
SaveContextOnUpdateOrErrorResponseWrapper.class);
|
||||
if (responseWrapper == null) {
|
||||
boolean httpSessionExists = request.getSession(false) != null;
|
||||
SecurityContext initialContext = this.securityContextHolderStrategy.createEmptyContext();
|
||||
responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
|
||||
saveContextInHttpSession(context, request);
|
||||
return;
|
||||
}
|
||||
responseWrapper.saveContext(context);
|
||||
}
|
||||
|
||||
private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
|
||||
if (isTransient(context) || isTransient(context.getAuthentication())) {
|
||||
return;
|
||||
}
|
||||
SecurityContext emptyContext = generateNewContext();
|
||||
if (emptyContext.equals(context)) {
|
||||
HttpSession session = request.getSession(false);
|
||||
removeContextFromSession(context, session);
|
||||
}
|
||||
else {
|
||||
boolean createSession = this.allowSessionCreation;
|
||||
HttpSession session = request.getSession(createSession);
|
||||
setContextInSession(context, session);
|
||||
}
|
||||
}
|
||||
|
||||
private void setContextInSession(SecurityContext context, HttpSession session) {
|
||||
if (session != null) {
|
||||
session.setAttribute(this.springSecurityContextKey, context);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeContextFromSession(SecurityContext context, HttpSession session) {
|
||||
if (session != null) {
|
||||
session.removeAttribute(this.springSecurityContextKey);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format("Removed %s from HttpSession [%s]", context, session));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsContext(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
@@ -393,11 +426,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
// We may have a new session, so check also whether the context attribute
|
||||
// is set SEC-1561
|
||||
if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
|
||||
httpSession.setAttribute(springSecurityContextKey, context);
|
||||
HttpSessionSecurityContextRepository.this.saveContextInHttpSession(context, this.request);
|
||||
this.isSaveContextInvoked = true;
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -108,6 +108,7 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
|
||||
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
|
||||
this.requestHandler.handle(request, response, deferredCsrfToken::get);
|
||||
if (!this.requireCsrfProtectionMatcher.matches(request)) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
|
||||
+3
-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.
|
||||
@@ -226,7 +226,8 @@ public class DefaultSavedRequest implements SavedRequest {
|
||||
if (!propertyEquals(this.pathInfo, request.getPathInfo())) {
|
||||
return false;
|
||||
}
|
||||
if (!propertyEquals(this.queryString, request.getQueryString())) {
|
||||
if (!propertyEquals(createQueryString(this.queryString, this.matchingRequestParameterName),
|
||||
request.getQueryString())) {
|
||||
return false;
|
||||
}
|
||||
if (!propertyEquals(this.requestURI, request.getRequestURI())) {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -142,4 +145,23 @@ public class FilterInvocationTests {
|
||||
assertThat(filterInvocation.getRequest().getServletContext()).isSameAs(mockServletContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDummyRequestGetHeaders() {
|
||||
DummyRequest request = new DummyRequest();
|
||||
request.addHeader("known", "val");
|
||||
Enumeration<String> headers = request.getHeaders("known");
|
||||
assertThat(headers.hasMoreElements()).isTrue();
|
||||
assertThat(headers.nextElement()).isEqualTo("val");
|
||||
assertThat(headers.hasMoreElements()).isFalse();
|
||||
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDummyRequestGetHeadersNull() {
|
||||
DummyRequest request = new DummyRequest();
|
||||
Enumeration<String> headers = request.getHeaders("unknown");
|
||||
assertThat(headers.hasMoreElements()).isFalse();
|
||||
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.
|
||||
@@ -143,12 +143,14 @@ public class DefaultLoginPageGeneratingFilterTests {
|
||||
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(
|
||||
new UsernamePasswordAuthenticationFilter());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/login");
|
||||
request.addParameter("login_error", "true");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
request.setQueryString("error");
|
||||
MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||
String message = messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
|
||||
"Bad credentials", Locale.KOREA);
|
||||
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new BadCredentialsException(message));
|
||||
filter.doFilter(request, new MockHttpServletResponse(), this.chain);
|
||||
filter.doFilter(request, response, this.chain);
|
||||
assertThat(response.getContentAsString()).contains(message);
|
||||
}
|
||||
|
||||
// gh-5394
|
||||
|
||||
+22
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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,11 +16,16 @@
|
||||
|
||||
package org.springframework.security.web.authentication;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -108,4 +113,20 @@ public class SimpleUrlAuthenticationSuccessHandlerTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ash.setTargetUrlParameter(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRemoveAuthenticationAttributeWhenOnAuthenticationSuccess() throws Exception {
|
||||
SimpleUrlAuthenticationSuccessHandler ash = new SimpleUrlAuthenticationSuccessHandler();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpSession session = request.getSession();
|
||||
assertThat(session).isNotNull();
|
||||
session.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
|
||||
new BadCredentialsException("Invalid credentials"));
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNotNull();
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION))
|
||||
.isInstanceOf(AuthenticationException.class);
|
||||
ash.onAuthenticationSuccess(request, response, mock(Authentication.class));
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user