Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff3c560c53 | |||
| a484044591 | |||
| 6cf8c53aaa | |||
| 2d52fb8e4b | |||
| cc56fc647c | |||
| 67d8df790e | |||
| fe6cfe819a | |||
| 4162bdadd9 | |||
| 9c181890ea | |||
| af41a4053e | |||
| 440b575dd7 | |||
| 8793639fe4 | |||
| 95f2b822a7 | |||
| 8eaf90a5e9 | |||
| 6d6042485b | |||
| 09632dbe51 | |||
| 7e49d70e77 | |||
| cfcdb15d6b | |||
| e9ee2cefb9 | |||
| 15dc917c23 | |||
| 46368f0a75 | |||
| 0448e2804c | |||
| 7571ab9b7d | |||
| 8566bbcdac | |||
| 609d5a71c6 | |||
| c73876e596 | |||
| 8a07d17c69 | |||
| 37d8846652 | |||
| 1a4a2a9055 | |||
| 54117d7d27 | |||
| 74b8c6715b | |||
| 8c9275067e | |||
| 4b8bfee7c5 | |||
| b727f24c95 | |||
| d55c8aac4f | |||
| b423db5f93 | |||
| 28be37238d | |||
| c4e9fb885d | |||
| 4813ec1e09 | |||
| dad1fba1bf | |||
| 442faccb5f | |||
| 34015944f2 | |||
| 68fd755abc | |||
| e25117856e | |||
| 9c3f91a2d3 | |||
| 16dcfd1cfe | |||
| c69df9fba0 | |||
| 54de5c9537 | |||
| 64a1ad5cd6 | |||
| 5ffebaf12b | |||
| 39cee36065 | |||
| 44c4a4ae86 | |||
| 05675e83dc | |||
| d5603a944d | |||
| b4b6469d83 | |||
| a513fc0f38 | |||
| b4b4cd0ffa | |||
| eb58655fa9 | |||
| 20358e769d | |||
| c15589ede1 | |||
| a106188add | |||
| 6db2b0dcd0 | |||
| 834e361898 | |||
| 6bda1d2bf3 | |||
| 97b53f0472 | |||
| aea13b656e | |||
| 9ec9e77c6b | |||
| a708007536 | |||
| 67645b32f4 | |||
| ed42dc4a09 | |||
| 2fa1bbc9d1 | |||
| fd65dc6756 | |||
| 5eefe9dcff | |||
| 8a2b96795e | |||
| c75ee25a6d | |||
| 177514b6c5 | |||
| 8d664bc4c2 | |||
| cbb4e40166 | |||
| f03c904c02 | |||
| 889fa55c9c | |||
| 98321b769a | |||
| 3fbb64db96 | |||
| 229325a0bb | |||
| a74008cc79 | |||
| 3d7e22a4e9 | |||
| cfeb1ce303 | |||
| 6935045172 | |||
| abd51f7b63 | |||
| b22dd9a3e9 | |||
| 4154ed543a | |||
| 8ad91287d9 | |||
| ffe029d5bd | |||
| cdc0fa0e5b | |||
| 2e92dad761 | |||
| 84cca81edf | |||
| c06e604278 | |||
| acf48721cd | |||
| ebabcaa51a | |||
| 094bf1b527 | |||
| 14ee873b74 | |||
| e7b14b3ca2 | |||
| 66665344c5 | |||
| 6c7703789a | |||
| e144a84163 | |||
| a6918f9e0c | |||
| fabf7f649c | |||
| 8b2ce79341 | |||
| 1eff924598 | |||
| b2240f376e | |||
| 7b88ab289d | |||
| 82ffc954d3 | |||
| ad57c0567f | |||
| 26566af431 | |||
| 2a4fe7de58 | |||
| 86578ea9de | |||
| 5257e36ffc | |||
| e28ea6dbad | |||
| ddad623abf | |||
| 383e0c2cf0 | |||
| 0421e25cba | |||
| 1c885cf3a3 | |||
| 109f6e7028 | |||
| bbd31f0e33 | |||
| 7d22e02593 | |||
| 97ba596ca3 | |||
| 1c3ce1e401 | |||
| d3a65dbbbe | |||
| 79887fa213 | |||
| 978c736169 | |||
| 35cf52d3bd | |||
| 7fdc6070fd | |||
| 6bf11181ef | |||
| 70219e4a80 | |||
| 5be58b2622 | |||
| b851a8bf6d | |||
| 7dcdb4b1a8 | |||
| faadadf71a | |||
| 207e41f6f6 | |||
| ce46f06d81 |
+1
-1
@@ -68,7 +68,7 @@ Discover more commands with `./gradlew tasks`.
|
||||
|
||||
== 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://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/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.
|
||||
|
||||
@@ -87,7 +87,7 @@ dependencies {
|
||||
implementation localGroovy()
|
||||
|
||||
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.3'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.5'
|
||||
implementation 'org.gretty:gretty:3.0.9'
|
||||
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.method.configuration;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
|
||||
|
||||
private final Supplier<ReactiveAuthorizationManager<T>> delegate;
|
||||
|
||||
DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
|
||||
ReactiveAuthorizationManager<T> delegate) {
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
ObservationRegistry registry = provider.getIfAvailable(() -> ObservationRegistry.NOOP);
|
||||
if (registry.isNoop()) {
|
||||
return delegate;
|
||||
}
|
||||
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
|
||||
return this.delegate.get().check(authentication, object);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-7
@@ -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.
|
||||
@@ -28,7 +28,6 @@ import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
|
||||
@@ -93,11 +92,7 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
|
||||
|
||||
static <T> ReactiveAuthorizationManager<T> manager(ReactiveAuthorizationManager<T> delegate,
|
||||
ObjectProvider<ObservationRegistry> registryProvider) {
|
||||
ObservationRegistry registry = registryProvider.getIfAvailable(() -> ObservationRegistry.NOOP);
|
||||
if (registry.isNoop()) {
|
||||
return delegate;
|
||||
}
|
||||
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
|
||||
return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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
@@ -324,7 +324,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
|
||||
*/
|
||||
|
||||
+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) {
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class HttpSecurityDeferAddFilterTest {
|
||||
public class HttpSecurityDeferAddFilterTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
+82
-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,8 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -25,6 +27,8 @@ 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.web.builders.HttpSecurity;
|
||||
@@ -39,6 +43,8 @@ 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;
|
||||
@@ -48,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;
|
||||
@@ -324,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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class NullLogoutSuccessHandlerConfig {
|
||||
|
||||
+1
-1
@@ -53,7 +53,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);
|
||||
|
||||
+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();
|
||||
|
||||
+4
-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.
|
||||
@@ -25,13 +25,13 @@ import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Ensures that the MockMvcResult redirects to the saved ReqeuestCache.getRedirectUrl().
|
||||
* Ensures that the MockMvcResult redirects to the saved RequestCache.getRedirectUrl().
|
||||
*/
|
||||
public final class RequestCacheResultMatcher {
|
||||
|
||||
/**
|
||||
* Verifies that the MockMvcResult redirects to the saved
|
||||
* ReqeustCache.getRedirectUrl().
|
||||
* RequestCache.getRedirectUrl().
|
||||
* @return a ResultMatcher that performs the verification.
|
||||
*/
|
||||
public static ResultMatcher redirectToCachedRequest() {
|
||||
@@ -39,7 +39,7 @@ public final class RequestCacheResultMatcher {
|
||||
RequestCache requestCache = new HttpSessionRequestCache();
|
||||
MockHttpServletResponse response = mvcResult.getResponse();
|
||||
SavedRequest savedRequest = requestCache.getRequest(mvcResult.getRequest(), response);
|
||||
assertThat(savedRequest).describedAs("savedReqeust cannot be null").isNotNull();
|
||||
assertThat(savedRequest).describedAs("savedRequest cannot be null").isNotNull();
|
||||
String cachedRedirectUrl = savedRequest.getRedirectUrl();
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo(cachedRedirectUrl);
|
||||
};
|
||||
|
||||
+10
-1
@@ -61,7 +61,16 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
|
||||
private UserDetailsPasswordService userDetailsPasswordService;
|
||||
|
||||
public DaoAuthenticationProvider() {
|
||||
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
|
||||
this(PasswordEncoderFactories.createDelegatingPasswordEncoder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance using the provided {@link PasswordEncoder}
|
||||
* @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null.
|
||||
* @since 6.0.3
|
||||
*/
|
||||
public DaoAuthenticationProvider(PasswordEncoder passwordEncoder) {
|
||||
setPasswordEncoder(passwordEncoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+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;
|
||||
+7
@@ -441,6 +441,13 @@ public class DaoAuthenticationProviderTests {
|
||||
assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(() -> provider.authenticate(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructWhenPasswordEncoderProvidedThenSets() {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(
|
||||
NoOpPasswordEncoder.getInstance());
|
||||
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
|
||||
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
|
||||
|
||||
+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))
|
||||
|
||||
+10
-10
@@ -12,20 +12,20 @@ dependencies {
|
||||
api platform("io.rsocket:rsocket-bom:1.1.3")
|
||||
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:2022.0.2")
|
||||
api platform("org.springframework.data:spring-data-bom:2022.0.5")
|
||||
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.14.2")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.4.5"
|
||||
api "ch.qos.logback:logback-classic:1.4.6"
|
||||
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"
|
||||
api "com.squareup.okhttp3:mockwebserver:3.14.9"
|
||||
api "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
api "com.unboundid:unboundid-ldapsdk:6.0.7"
|
||||
api "com.unboundid:unboundid-ldapsdk:6.0.8"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.13.4"
|
||||
api "io.mockk:mockk:1.13.5"
|
||||
api "io.micrometer:micrometer-observation:$micrometerVersion"
|
||||
api "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
api "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
@@ -48,11 +48,11 @@ dependencies {
|
||||
api "org.apache.httpcomponents:httpclient:4.5.14"
|
||||
api "org.aspectj:aspectjrt:$aspectjVersion"
|
||||
api "org.aspectj:aspectjweaver:$aspectjVersion"
|
||||
api "org.assertj:assertj-core:3.23.1"
|
||||
api "org.assertj:assertj-core:3.24.2"
|
||||
api "org.bouncycastle:bcpkix-jdk15on:1.70"
|
||||
api "org.bouncycastle:bcprov-jdk15on:1.70"
|
||||
api "org.eclipse.jetty:jetty-server:11.0.13"
|
||||
api "org.eclipse.jetty:jetty-servlet:11.0.13"
|
||||
api "org.eclipse.jetty:jetty-server:11.0.15"
|
||||
api "org.eclipse.jetty:jetty-servlet:11.0.15"
|
||||
api "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate.orm:hibernate-core:6.1.7.Final"
|
||||
@@ -67,13 +67,13 @@ dependencies {
|
||||
api "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
api "org.skyscreamer:jsonassert:1.5.1"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
api "org.slf4j:slf4j-api:2.0.6"
|
||||
api "org.springframework.ldap:spring-ldap-core:3.0.1"
|
||||
api "org.slf4j:slf4j-api:2.0.7"
|
||||
api "org.springframework.ldap:spring-ldap-core:3.0.2"
|
||||
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
api 'org.apache.maven.resolver:maven-resolver-connector-basic:1.8.2'
|
||||
api 'org.apache.maven.resolver:maven-resolver-impl:1.8.2'
|
||||
api 'org.apache.maven.resolver:maven-resolver-transport-http:1.8.2'
|
||||
api 'org.apache.maven:maven-resolver-provider:3.8.7'
|
||||
api 'org.apache.maven:maven-resolver-provider:3.8.8'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ There are convenience mechanisms to make this easier, but this is still not inte
|
||||
.Java
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
User user = User.withDefaultPasswordEncoder()
|
||||
UserDetails user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("user")
|
||||
@@ -243,12 +243,12 @@ If you are creating multiple users, you can also reuse the builder:
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||||
User user = users
|
||||
UserDetails user = users
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
User admin = users
|
||||
UserDetails admin = users
|
||||
.username("admin")
|
||||
.password("password")
|
||||
.roles("USER","ADMIN")
|
||||
|
||||
@@ -13,8 +13,14 @@ 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].
|
||||
====
|
||||
|
||||
If you are explicitly opting into Spring Security 6's new defaults, the following configuration can be removed to accept the Spring Security 6 defaults.
|
||||
|
||||
|
||||
include::partial$servlet/architecture/security-context-explicit.adoc[]
|
||||
|
||||
== Multiple SecurityContextRepository
|
||||
|
||||
@@ -6,6 +6,8 @@ After xref:reactive/test/web/setup.adoc[applying the Spring Security support to
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
||||
|
||||
@Test
|
||||
public void messageWhenNotAuthenticated() throws Exception {
|
||||
this.rest
|
||||
@@ -66,6 +68,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
|
||||
|
||||
//...
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ Spring Security also provides support for CSRF testing with `WebTestClient` -- f
|
||||
.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())
|
||||
@@ -17,6 +19,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 {
|
||||
|
||||
@@ -152,7 +152,7 @@ Assuming that no other `SecurityFilterChain` instances match, `SecurityFilterCha
|
||||
// FIXME: add link to pattern matching
|
||||
|
||||
Notice that `SecurityFilterChain~0~` has only three security `Filter` instances configured.
|
||||
However, `SecurityFilterChain~n~` has four security `Filter` instanes configured.
|
||||
However, `SecurityFilterChain~n~` has four security `Filter` instances configured.
|
||||
It is important to note that each `SecurityFilterChain` can be unique and can be configured in isolation.
|
||||
In fact, a `SecurityFilterChain` might have zero security `Filter` instances if the application wants Spring Security to ignore certain requests.
|
||||
// FIXME: add link to configuring multiple `SecurityFilterChain` instances
|
||||
@@ -217,7 +217,6 @@ image::{figures}/exceptiontranslationfilter.png[]
|
||||
* image:{icondir}/number_2.png[] If the user is not authenticated or it is an `AuthenticationException`, then __Start Authentication__.
|
||||
** The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out.
|
||||
** The `HttpServletRequest` is <<savedrequests,saved>> so that it can be used to replay the original request once authentication is successful.
|
||||
|
||||
// FIXME: add link to authentication success
|
||||
** The `AuthenticationEntryPoint` is used to request credentials from the client.
|
||||
For example, it might redirect to a log in page or send a `WWW-Authenticate` header.
|
||||
|
||||
@@ -248,8 +248,9 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
|
||||
* `SessionAuthenticationStrategy` is notified of a new login.
|
||||
See the {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface.
|
||||
* The <<servlet-authentication-authentication>> is set on the <<servlet-authentication-securitycontextholder>>.
|
||||
Later, the `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession`.
|
||||
See the {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class.
|
||||
Later, if you need to save the `SecurityContext` so that it can be automatically set on future requests, `SecurityContextRepository#saveContext` must be explicitly invoked.
|
||||
See the {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] class.
|
||||
|
||||
* `RememberMeServices.loginSuccess` is invoked.
|
||||
If remember me is not configured, this is a no-op.
|
||||
See the {security-api-url}org/springframework/security/web/authentication/rememberme/package-frame.html[`rememberme`] package.
|
||||
|
||||
@@ -12,6 +12,7 @@ The default is that accessing the URL `/logout` logs the user out by:
|
||||
- Invalidating the HTTP Session
|
||||
- Cleaning up any RememberMe authentication that was configured
|
||||
- Clearing the `SecurityContextHolder`
|
||||
- Clearing the `SecurityContextRepository`
|
||||
- Redirecting to `/login?logout`
|
||||
|
||||
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
|
||||
@@ -141,6 +142,7 @@ If not configured, a status code 200 is 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()`]
|
||||
|
||||
@@ -47,17 +47,17 @@ image:{icondir}/number_3.png[] If authentication fails, then __Failure__.
|
||||
If remember me is not configured, this is a no-op.
|
||||
See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc.
|
||||
. `AuthenticationFailureHandler` is invoked.
|
||||
See the {security-api-url}springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] class in the Javadoc
|
||||
See the {security-api-url}org/springframework/security/web/authentication/AuthenticationFailureHandler.html[`AuthenticationFailureHandler`] class in the Javadoc
|
||||
|
||||
image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
|
||||
|
||||
. `SessionAuthenticationStrategy` is notified of a new login.
|
||||
See the {security-api-url}springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface in the Javadoc.
|
||||
See the {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface in the Javadoc.
|
||||
. The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
|
||||
See the {security-api-url}springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class in the Javadoc.
|
||||
See the {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class in the Javadoc.
|
||||
. `RememberMeServices.loginSuccess` is invoked.
|
||||
If remember me is not configured, this is a no-op.
|
||||
See the {security-api-url}springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc.
|
||||
See the {security-api-url}org/springframework/security/web/authentication/RememberMeServices.html[`RememberMeServices`] interface in the Javadoc.
|
||||
. `ApplicationEventPublisher` publishes an `InteractiveAuthenticationSuccessEvent`.
|
||||
. The `AuthenticationSuccessHandler` is invoked. Typically, this is a `SimpleUrlAuthenticationSuccessHandler`, which redirects to a request saved by xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] when we redirect to the login page.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ But before you leave, consider if any of these use cases fit your application:
|
||||
* I want to <<understanding-session-management-components,Understand Session Management's components>>
|
||||
* I want to <<ns-concurrent-sessions,restrict the number of times>> a user can be logged in concurrently
|
||||
* I want <<store-authentication-manually,to store the authentication directly>> myself instead of Spring Security doing it for me
|
||||
* I am storing the authentication manually and I want <<properly-clearing-authentication,to remove it>>
|
||||
* I am using <<the-sessionmanagementfilter, `SessionManagementFilter`>> and I need <<moving-away-from-sessionmanagementfilter,guidance on moving away from that>>
|
||||
* I want to store the authentication <<customizing-where-authentication-is-stored,in something other than the session>>
|
||||
* I am using a <<stateless-authentication, stateless authentication>>, but <<storing-stateless-authentication-in-the-session,I'd still like to store it in the session>>
|
||||
@@ -84,12 +85,6 @@ By default, Spring Security stores the security context for you in the HTTP sess
|
||||
|
||||
First, you need to create an implementation of `SecurityContextRepository` or use an existing implementation like `HttpSessionSecurityContextRepository`, then you can set it in `HttpSecurity`.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The above configuration sets the `SecurityContextRepository` on the `SecurityContextHolderFilter` and **participating** authentication filters, like `UsernamePasswordAuthenticationFilter`.
|
||||
To also set it in stateless filters, please see <<storing-stateless-authentication-in-the-session,how to customize the `SecurityContextRepository` for Stateless Authentication>>.
|
||||
====
|
||||
|
||||
[[customizing-the-securitycontextrepository]]
|
||||
.Customizing the `SecurityContextRepository`
|
||||
====
|
||||
@@ -134,6 +129,12 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The above configuration sets the `SecurityContextRepository` on the `SecurityContextHolderFilter` and **participating** authentication filters, like `UsernamePasswordAuthenticationFilter`.
|
||||
To also set it in stateless filters, please see <<storing-stateless-authentication-in-the-session,how to customize the `SecurityContextRepository` for Stateless Authentication>>.
|
||||
====
|
||||
|
||||
If you are using a custom authentication mechanism, you might want to <<store-authentication-manually,store the `Authentication` by yourself>>.
|
||||
|
||||
[[store-authentication-manually]]
|
||||
@@ -157,7 +158,7 @@ public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest req
|
||||
Authentication authentication = authenticationManager.authenticate(token); <4>
|
||||
SecurityContext context = securityContextHolderStrategy.createEmptyContext();
|
||||
context.setAuthentication(authentication); <5>
|
||||
securityContextHolderStrategy.setContext(authentication);
|
||||
securityContextHolderStrategy.setContext(context);
|
||||
securityContextRepository.saveContext(context, request, response); <6>
|
||||
}
|
||||
|
||||
@@ -181,6 +182,32 @@ class LoginRequest {
|
||||
And that's it.
|
||||
If you are not sure what `securityContextHolderStrategy` is in the above example, you can read more about it in the <<use-securitycontextholderstrategy, Using `SecurityContextStrategy` section>>.
|
||||
|
||||
[[properly-clearing-authentication]]
|
||||
=== Properly Clearing an Authentication
|
||||
|
||||
If you are using Spring Security's xref:servlet/authentication/logout.adoc[Logout Support] then it handles a lot of stuff for you including clearing and saving the context.
|
||||
But, let's say you need to manually log users out of your app. In that case, you'll need to make sure you're clearing and saving the context properly.
|
||||
|
||||
Now, you might already be familiar with clearing the `SecurityContextHolder` by doing `SecurityContextHolderStrategy#clearContext()`.
|
||||
That's great, but if your app requires an xref:migration/servlet/session-management.adoc#_require_explicit_saving_of_securitycontextrepository[explicit save of the context], simply clearing it isn't enough.
|
||||
The reason is that it doesn't remove it from the `SecurityContextRepository`, which means the `SecurityContext` could still be available for the next requests, and we definitely don't want that.
|
||||
|
||||
To make sure the authentication is properly cleared and saved, you can invoke {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[the `SecurityContextLogoutHandler`] which does that for us, like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
SecurityContextLogoutHandler handler = new SecurityContextLogoutHandler(); <1>
|
||||
handler.logout(httpServletRequest, httpServletResponse, null); <2>
|
||||
----
|
||||
====
|
||||
|
||||
<1> Create a new instance of `SecurityContextLogoutHandler`
|
||||
<2> Call the `logout` method passing in the `HttpServletRequest`, `HttpServletResponse` and a `null` authentication because it is not required for this handler.
|
||||
|
||||
It's important to remember that clearing and saving the context is just one piece of the logout process, therefore we recommend having Spring Security take care of it.
|
||||
|
||||
[[stateless-authentication]]
|
||||
=== Configuring Persistence for Stateless Authentication
|
||||
|
||||
@@ -793,7 +820,7 @@ Authentication authentication = this.authenticationManager.authenticate(token);
|
||||
// ...
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext(); <1>
|
||||
context.setAuthentication(authentication); <2>
|
||||
SecurityContextHolder.setContext(authentication); <3>
|
||||
SecurityContextHolder.setContext(context); <3>
|
||||
----
|
||||
====
|
||||
|
||||
@@ -825,7 +852,7 @@ public class SomeClass {
|
||||
// ...
|
||||
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); <1>
|
||||
context.setAuthentication(authentication); <2>
|
||||
this.securityContextHolderStrategy.setContext(authentication); <3>
|
||||
this.securityContextHolderStrategy.setContext(context); <3>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -225,6 +225,11 @@ AccessDecisionVoter hierarchyVoter() {
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
`RoleHierarchy` bean configuration is not yet ported over to `@EnableMethodSecurity`.
|
||||
As such this example is using `AccessDecisionVoter`.
|
||||
If you need `RoleHierarchy` support for method security, please continue using `@EnableGlobalMethodSecurity` until https://github.com/spring-projects/spring-security/issues/12783 is complete.
|
||||
|
||||
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
|
||||
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AuthorizationManager` adapted to call the above `RoleHierarchyVoter`.
|
||||
The `>` symbol can be thought of as meaning "includes".
|
||||
|
||||
@@ -408,3 +408,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`.
|
||||
|
||||
@@ -210,7 +210,7 @@ You could then refer to the method as follows:
|
||||
----
|
||||
http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
|
||||
.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@webSecurity.checkUserId(authentication,#userId)"))
|
||||
...
|
||||
);
|
||||
----
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login.
|
||||
|
||||
This section shows how to configure the {gh-samples-url}/boot/oauth2login[*OAuth 2.0 Login sample*] by using _Google_ as the _Authentication Provider_ and covers the following topics:
|
||||
This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] by using _Google_ as the _Authentication Provider_ and covers the following topics:
|
||||
|
||||
* <<oauth2login-sample-initial-setup>>
|
||||
* <<oauth2login-sample-redirect-uri>>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -41,6 +41,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`.
|
||||
@@ -150,7 +151,7 @@ To achieve this, any interfaces or classes where Spring Security uses OpenSAML i
|
||||
This makes it possible for you to switch out OpenSAML for some other library or an unsupported version of OpenSAML.
|
||||
|
||||
As a natural outcome of these two goals, Spring Security's SAML API is quite small relative to other modules.
|
||||
Instead, such classes as `OpenSamlAuthenticationRequestFactory` and `OpenSamlAuthenticationProvider` expose `Converter` implementationss that customize various steps in the authentication process.
|
||||
Instead, such classes as `OpenSamlAuthenticationRequestFactory` and `OpenSamlAuthenticationProvider` expose `Converter` implementations that customize various steps in the authentication process.
|
||||
|
||||
For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter delegates to `OpenSamlAuthenticationProvider`:
|
||||
|
||||
@@ -662,6 +663,16 @@ In a deployed application, that translates 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
|
||||
|
||||
@@ -736,58 +747,6 @@ resource.inputStream.use {
|
||||
When you specify the locations of these files as the appropriate Spring Boot properties, Spring Boot performs 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.
|
||||
|
||||
You may want to customize for a number of reasons, including:
|
||||
|
||||
* You may know that your application is never going to be a multi-tenant application and, as a result, want 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>` instances], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate `<saml2:Response>` instances], 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
|
||||
|
||||
@@ -884,3 +843,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")
|
||||
----
|
||||
====
|
||||
|
||||
@@ -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/.*
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
aspectjVersion=1.9.19
|
||||
reactorVersion=2022.0.3
|
||||
springJavaformatVersion=0.0.35
|
||||
reactorVersion=2022.0.6
|
||||
springJavaformatVersion=0.0.38
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=6.0.5
|
||||
micrometerVersion=1.10.4
|
||||
springFrameworkVersion=6.0.8
|
||||
micrometerVersion=1.10.6
|
||||
openSamlVersion=4.1.1
|
||||
version=6.0.2
|
||||
version=6.0.3
|
||||
kotlinVersion=1.7.22
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
+26
-46
@@ -18,6 +18,7 @@ package org.springframework.security.messaging.access.intercept;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -37,6 +38,7 @@ import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatche
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> {
|
||||
|
||||
@@ -87,6 +89,10 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
||||
SimpDestinationMessageMatcher simp = (SimpDestinationMessageMatcher) matcher;
|
||||
return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message));
|
||||
}
|
||||
if (matcher instanceof Builder.LazySimpDestinationMessageMatcher) {
|
||||
Builder.LazySimpDestinationMessageMatcher path = (Builder.LazySimpDestinationMessageMatcher) matcher;
|
||||
return new MessageAuthorizationContext<>(message, path.extractPathVariables(message));
|
||||
}
|
||||
return new MessageAuthorizationContext<>(message);
|
||||
}
|
||||
|
||||
@@ -192,8 +198,7 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
||||
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
|
||||
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
|
||||
for (String pattern : patterns) {
|
||||
Supplier<MessageMatcher<Object>> supplier = new Builder.PathMatcherMessageMatcherBuilder(pattern, type);
|
||||
MessageMatcher<?> matcher = new Builder.SupplierMessageMatcher(supplier);
|
||||
MessageMatcher<Object> matcher = new LazySimpDestinationMessageMatcher(pattern, type);
|
||||
matchers.add(matcher);
|
||||
}
|
||||
return new Builder.Constraint(matchers);
|
||||
@@ -375,58 +380,33 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
||||
|
||||
}
|
||||
|
||||
private static final class SupplierMessageMatcher implements MessageMatcher<Object> {
|
||||
private final class LazySimpDestinationMessageMatcher implements MessageMatcher<Object> {
|
||||
|
||||
private final Supplier<MessageMatcher<Object>> supplier;
|
||||
private final Supplier<SimpDestinationMessageMatcher> delegate;
|
||||
|
||||
private volatile MessageMatcher<Object> delegate;
|
||||
|
||||
SupplierMessageMatcher(Supplier<MessageMatcher<Object>> supplier) {
|
||||
this.supplier = supplier;
|
||||
private LazySimpDestinationMessageMatcher(String pattern, SimpMessageType type) {
|
||||
this.delegate = SingletonSupplier.of(() -> {
|
||||
PathMatcher pathMatcher = Builder.this.pathMatcher.get();
|
||||
if (type == null) {
|
||||
return new SimpDestinationMessageMatcher(pattern, pathMatcher);
|
||||
}
|
||||
if (SimpMessageType.MESSAGE == type) {
|
||||
return SimpDestinationMessageMatcher.createMessageMatcher(pattern, pathMatcher);
|
||||
}
|
||||
if (SimpMessageType.SUBSCRIBE == type) {
|
||||
return SimpDestinationMessageMatcher.createSubscribeMatcher(pattern, pathMatcher);
|
||||
}
|
||||
throw new IllegalStateException(type + " is not supported since it does not have a destination");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Message<?> message) {
|
||||
if (this.delegate == null) {
|
||||
synchronized (this.supplier) {
|
||||
if (this.delegate == null) {
|
||||
this.delegate = this.supplier.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.delegate.matches(message);
|
||||
return this.delegate.get().matches(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class PathMatcherMessageMatcherBuilder implements Supplier<MessageMatcher<Object>> {
|
||||
|
||||
private final String pattern;
|
||||
|
||||
private final SimpMessageType type;
|
||||
|
||||
private PathMatcherMessageMatcherBuilder(String pattern, SimpMessageType type) {
|
||||
this.pattern = pattern;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
private PathMatcher resolvePathMatcher() {
|
||||
return Builder.this.pathMatcher.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageMatcher<Object> get() {
|
||||
PathMatcher pathMatcher = resolvePathMatcher();
|
||||
if (this.type == null) {
|
||||
return new SimpDestinationMessageMatcher(this.pattern, pathMatcher);
|
||||
}
|
||||
if (SimpMessageType.MESSAGE == this.type) {
|
||||
return SimpDestinationMessageMatcher.createMessageMatcher(this.pattern, pathMatcher);
|
||||
}
|
||||
if (SimpMessageType.SUBSCRIBE == this.type) {
|
||||
return SimpDestinationMessageMatcher.createSubscribeMatcher(this.pattern, pathMatcher);
|
||||
}
|
||||
throw new IllegalStateException(this.type + " is not supported since it does not have a destination");
|
||||
Map<String, String> extractPathVariables(Message<?> message) {
|
||||
return this.delegate.get().extractPathVariables(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.access.intercept;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link MessageMatcherDelegatingAuthorizationManager}
|
||||
*/
|
||||
public final class MessageMatcherDelegatingAuthorizationManagerTests {
|
||||
|
||||
@Test
|
||||
void checkWhenPermitAllThenPermits() {
|
||||
AuthorizationManager<Message<?>> authorizationManager = builder().anyMessage().permitAll().build();
|
||||
Message<?> message = new GenericMessage<>(new Object());
|
||||
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenAnyMessageHasRoleThenRequires() {
|
||||
AuthorizationManager<Message<?>> authorizationManager = builder().anyMessage().hasRole("USER").build();
|
||||
Message<?> message = new GenericMessage<>(new Object());
|
||||
Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
|
||||
assertThat(authorizationManager.check(() -> user, message).isGranted()).isTrue();
|
||||
Authentication admin = new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
|
||||
assertThat(authorizationManager.check(() -> admin, message).isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenSimpDestinationMatchesThenUses() {
|
||||
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("destination").permitAll()
|
||||
.anyMessage().denyAll().build();
|
||||
MessageHeaders headers = new MessageHeaders(
|
||||
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination"));
|
||||
Message<?> message = new GenericMessage<>(new Object(), headers);
|
||||
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenNullDestinationHeaderMatchesThenUses() {
|
||||
AuthorizationManager<Message<?>> authorizationManager = builder().nullDestMatcher().permitAll().anyMessage()
|
||||
.denyAll().build();
|
||||
Message<?> message = new GenericMessage<>(new Object());
|
||||
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
|
||||
MessageHeaders headers = new MessageHeaders(
|
||||
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination"));
|
||||
message = new GenericMessage<>(new Object(), headers);
|
||||
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkWhenSimpTypeMatchesThenUses() {
|
||||
AuthorizationManager<Message<?>> authorizationManager = builder().simpTypeMatchers(SimpMessageType.CONNECT)
|
||||
.permitAll().anyMessage().denyAll().build();
|
||||
MessageHeaders headers = new MessageHeaders(
|
||||
Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.CONNECT));
|
||||
Message<?> message = new GenericMessage<>(new Object(), headers);
|
||||
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
|
||||
}
|
||||
|
||||
// gh-12540
|
||||
@Test
|
||||
void checkWhenSimpDestinationMatchesThenVariablesExtracted() {
|
||||
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("destination/{id}")
|
||||
.access(variable("id").isEqualTo("3")).anyMessage().denyAll().build();
|
||||
MessageHeaders headers = new MessageHeaders(
|
||||
Map.of(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination/3"));
|
||||
Message<?> message = new GenericMessage<>(new Object(), headers);
|
||||
assertThat(authorizationManager.check(mock(Supplier.class), message).isGranted()).isTrue();
|
||||
}
|
||||
|
||||
private MessageMatcherDelegatingAuthorizationManager.Builder builder() {
|
||||
return MessageMatcherDelegatingAuthorizationManager.builder();
|
||||
}
|
||||
|
||||
private Builder variable(String name) {
|
||||
return new Builder(name);
|
||||
|
||||
}
|
||||
|
||||
private static final class Builder {
|
||||
|
||||
private final String name;
|
||||
|
||||
private Builder(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
AuthorizationManager<MessageAuthorizationContext<?>> isEqualTo(String value) {
|
||||
return (authentication, object) -> {
|
||||
String extracted = object.getVariables().get(this.name);
|
||||
return new AuthorizationDecision(value.equals(extracted));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+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) {
|
||||
|
||||
+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
|
||||
|
||||
+17
-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.
|
||||
@@ -26,6 +26,7 @@ import java.util.function.Consumer;
|
||||
|
||||
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;
|
||||
@@ -737,7 +738,7 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String registrationId;
|
||||
private Converter<AssertingPartyDetails, String> registrationId = AssertingPartyDetails::getEntityId;
|
||||
|
||||
private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
|
||||
|
||||
@@ -757,10 +758,15 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
private String nameIdFormat = null;
|
||||
|
||||
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
|
||||
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder;
|
||||
|
||||
private Builder(String registrationId) {
|
||||
this.registrationId = registrationId;
|
||||
this.registrationId = (party) -> registrationId;
|
||||
this.assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
|
||||
}
|
||||
|
||||
Builder(AssertingPartyDetails.Builder builder) {
|
||||
this.assertingPartyDetailsBuilder = builder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -769,7 +775,7 @@ public final class RelyingPartyRegistration {
|
||||
* @return this object
|
||||
*/
|
||||
public Builder registrationId(String id) {
|
||||
this.registrationId = id;
|
||||
this.registrationId = (party) -> id;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -967,11 +973,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.assertingPartyDetailsBuilder.build(), this.nameIdFormat,
|
||||
this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
AssertingPartyDetails party = this.assertingPartyDetailsBuilder.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.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.
|
||||
@@ -30,10 +30,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;
|
||||
@@ -69,6 +71,8 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
|
||||
private final NameIDBuilder nameIdBuilder;
|
||||
|
||||
private final NameIDPolicyBuilder nameIdPolicyBuilder;
|
||||
|
||||
private RequestMatcher requestMatcher = new AntPathRequestMatcher(
|
||||
Saml2AuthenticationRequestResolver.DEFAULT_AUTHENTICATION_REQUEST_URI);
|
||||
|
||||
@@ -94,6 +98,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) {
|
||||
@@ -129,6 +136,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));
|
||||
|
||||
+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-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.
|
||||
@@ -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)
|
||||
.signingX509Credentials((c) -> c.add(signingCredential)).assertingPartyDetails(
|
||||
|
||||
+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())
|
||||
@@ -76,6 +77,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())
|
||||
@@ -114,6 +116,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())
|
||||
@@ -137,6 +140,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
@@ -43,9 +43,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 jakarta.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
|
||||
|
||||
+56
-30
@@ -18,9 +18,8 @@ package org.springframework.security.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.micrometer.common.KeyValues;
|
||||
@@ -227,49 +226,38 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F
|
||||
|
||||
class SimpleAroundFilterObservation implements AroundFilterObservation {
|
||||
|
||||
private final Iterator<Observation> observations;
|
||||
private final ObservationReference before;
|
||||
|
||||
private final Observation before;
|
||||
private final ObservationReference after;
|
||||
|
||||
private final Observation after;
|
||||
|
||||
private final AtomicReference<Observation.Scope> currentScope = new AtomicReference<>(null);
|
||||
private final AtomicReference<ObservationReference> reference = new AtomicReference<>(
|
||||
ObservationReference.NOOP);
|
||||
|
||||
SimpleAroundFilterObservation(Observation before, Observation after) {
|
||||
this.before = before;
|
||||
this.after = after;
|
||||
this.observations = Arrays.asList(before, after).iterator();
|
||||
this.before = new ObservationReference(before);
|
||||
this.after = new ObservationReference(after);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (this.observations.hasNext()) {
|
||||
stop();
|
||||
Observation observation = this.observations.next();
|
||||
observation.start();
|
||||
Observation.Scope scope = observation.openScope();
|
||||
this.currentScope.set(scope);
|
||||
if (this.reference.compareAndSet(ObservationReference.NOOP, this.before)) {
|
||||
this.before.start();
|
||||
return;
|
||||
}
|
||||
if (this.reference.compareAndSet(this.before, this.after)) {
|
||||
this.before.stop();
|
||||
this.after.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(Throwable ex) {
|
||||
Observation.Scope scope = this.currentScope.get();
|
||||
if (scope == null) {
|
||||
return;
|
||||
}
|
||||
scope.close();
|
||||
scope.getCurrentObservation().error(ex);
|
||||
this.reference.get().error(ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
Observation.Scope scope = this.currentScope.getAndSet(null);
|
||||
if (scope == null) {
|
||||
return;
|
||||
}
|
||||
scope.close();
|
||||
scope.getCurrentObservation().stop();
|
||||
this.reference.get().stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -304,12 +292,50 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F
|
||||
|
||||
@Override
|
||||
public Observation before() {
|
||||
return this.before;
|
||||
return this.before.observation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation after() {
|
||||
return this.after;
|
||||
return this.after.observation;
|
||||
}
|
||||
|
||||
private static final class ObservationReference {
|
||||
|
||||
private static final ObservationReference NOOP = new ObservationReference(Observation.NOOP);
|
||||
|
||||
private final AtomicInteger state = new AtomicInteger(0);
|
||||
|
||||
private final Observation observation;
|
||||
|
||||
private volatile Observation.Scope scope;
|
||||
|
||||
private ObservationReference(Observation observation) {
|
||||
this.observation = observation;
|
||||
this.scope = Observation.Scope.NOOP;
|
||||
}
|
||||
|
||||
private void start() {
|
||||
if (this.state.compareAndSet(0, 1)) {
|
||||
this.observation.start();
|
||||
this.scope = this.observation.openScope();
|
||||
}
|
||||
}
|
||||
|
||||
private void error(Throwable error) {
|
||||
if (this.state.get() == 1) {
|
||||
this.scope.close();
|
||||
this.scope.getCurrentObservation().error(error);
|
||||
}
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
if (this.state.compareAndSet(1, 2)) {
|
||||
this.scope.close();
|
||||
this.scope.getCurrentObservation().stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
@@ -27,6 +27,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;
|
||||
|
||||
/**
|
||||
@@ -53,6 +55,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)
|
||||
@@ -76,6 +80,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() {
|
||||
@@ -114,4 +120,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -59,6 +59,7 @@ 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;
|
||||
@@ -148,7 +149,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
|
||||
private AuthenticationFailureHandler failureHandler;
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
+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 loadDeferredContext(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.
|
||||
@@ -149,13 +149,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);
|
||||
@@ -392,11 +425,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+199
-39
@@ -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.
|
||||
@@ -17,12 +17,12 @@
|
||||
package org.springframework.security.web.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.common.KeyValues;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
@@ -253,46 +253,81 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
|
||||
class SimpleAroundWebFilterObservation implements AroundWebFilterObservation {
|
||||
|
||||
private final Iterator<Observation> observations;
|
||||
private final ObservationReference before;
|
||||
|
||||
private final Observation before;
|
||||
private final ObservationReference after;
|
||||
|
||||
private final Observation after;
|
||||
|
||||
private final AtomicReference<Observation> currentObservation = new AtomicReference<>(null);
|
||||
private final AtomicReference<ObservationReference> currentObservation = new AtomicReference<>(
|
||||
ObservationReference.NOOP);
|
||||
|
||||
SimpleAroundWebFilterObservation(Observation before, Observation after) {
|
||||
this.before = before;
|
||||
this.after = after;
|
||||
this.observations = Arrays.asList(before, after).iterator();
|
||||
this.before = new ObservationReference(before);
|
||||
this.after = new ObservationReference(after);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (this.observations.hasNext()) {
|
||||
stop();
|
||||
Observation observation = this.observations.next();
|
||||
observation.start();
|
||||
this.currentObservation.set(observation);
|
||||
public Observation start() {
|
||||
if (this.currentObservation.compareAndSet(ObservationReference.NOOP, this.before)) {
|
||||
this.before.start();
|
||||
return this.before.observation;
|
||||
}
|
||||
if (this.currentObservation.compareAndSet(this.before, this.after)) {
|
||||
this.before.stop();
|
||||
this.after.start();
|
||||
return this.after.observation;
|
||||
}
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(Throwable ex) {
|
||||
Observation observation = this.currentObservation.get();
|
||||
if (observation == null) {
|
||||
return;
|
||||
}
|
||||
observation.error(ex);
|
||||
public Observation error(Throwable ex) {
|
||||
this.currentObservation.get().error(ex);
|
||||
return this.currentObservation.get().observation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
Observation observation = this.currentObservation.getAndSet(null);
|
||||
if (observation == null) {
|
||||
return;
|
||||
}
|
||||
observation.stop();
|
||||
this.currentObservation.get().stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation contextualName(String contextualName) {
|
||||
return this.currentObservation.get().observation.contextualName(contextualName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation parentObservation(Observation parentObservation) {
|
||||
return this.currentObservation.get().observation.parentObservation(parentObservation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation lowCardinalityKeyValue(KeyValue keyValue) {
|
||||
return this.currentObservation.get().observation.lowCardinalityKeyValue(keyValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation highCardinalityKeyValue(KeyValue keyValue) {
|
||||
return this.currentObservation.get().observation.highCardinalityKeyValue(keyValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation observationConvention(ObservationConvention<?> observationConvention) {
|
||||
return this.currentObservation.get().observation.observationConvention(observationConvention);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation event(Event event) {
|
||||
return this.currentObservation.get().observation.event(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return this.currentObservation.get().observation.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scope openScope() {
|
||||
return this.currentObservation.get().observation.openScope();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -322,26 +357,64 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
.doOnError((t) -> {
|
||||
error(t);
|
||||
stop();
|
||||
});
|
||||
})
|
||||
.contextWrite((context) -> context.put(ObservationThreadLocalAccessor.KEY, this));
|
||||
// @formatter:on
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation before() {
|
||||
return this.before;
|
||||
return this.before.observation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation after() {
|
||||
return this.after;
|
||||
return this.after.observation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.currentObservation.get().observation.toString();
|
||||
}
|
||||
|
||||
private static final class ObservationReference {
|
||||
|
||||
private static final ObservationReference NOOP = new ObservationReference(Observation.NOOP);
|
||||
|
||||
private final AtomicInteger state = new AtomicInteger(0);
|
||||
|
||||
private final Observation observation;
|
||||
|
||||
private ObservationReference(Observation observation) {
|
||||
this.observation = observation;
|
||||
}
|
||||
|
||||
private void start() {
|
||||
if (this.state.compareAndSet(0, 1)) {
|
||||
this.observation.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void error(Throwable ex) {
|
||||
if (this.state.get() == 1) {
|
||||
this.observation.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
if (this.state.compareAndSet(1, 2)) {
|
||||
this.observation.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface WebFilterObservation {
|
||||
interface WebFilterObservation extends Observation {
|
||||
|
||||
WebFilterObservation NOOP = new WebFilterObservation() {
|
||||
};
|
||||
@@ -353,13 +426,59 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
return new SimpleWebFilterObservation(observation);
|
||||
}
|
||||
|
||||
default void start() {
|
||||
@Override
|
||||
default Observation contextualName(String contextualName) {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
default void error(Throwable ex) {
|
||||
@Override
|
||||
default Observation parentObservation(Observation parentObservation) {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default Observation lowCardinalityKeyValue(KeyValue keyValue) {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default Observation highCardinalityKeyValue(KeyValue keyValue) {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default Observation observationConvention(ObservationConvention<?> observationConvention) {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default Observation error(Throwable error) {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default Observation event(Event event) {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default Observation start() {
|
||||
return Observation.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default Context getContext() {
|
||||
return new Observation.Context();
|
||||
}
|
||||
|
||||
@Override
|
||||
default void stop() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
default Scope openScope() {
|
||||
return Scope.NOOP;
|
||||
}
|
||||
|
||||
default WebFilter wrap(WebFilter filter) {
|
||||
@@ -379,13 +498,13 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.observation.start();
|
||||
public Observation start() {
|
||||
return this.observation.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(Throwable ex) {
|
||||
this.observation.error(ex);
|
||||
public Observation error(Throwable ex) {
|
||||
return this.observation.error(ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -393,6 +512,46 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
this.observation.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation contextualName(String contextualName) {
|
||||
return this.observation.contextualName(contextualName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation parentObservation(Observation parentObservation) {
|
||||
return this.observation.parentObservation(parentObservation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation lowCardinalityKeyValue(KeyValue keyValue) {
|
||||
return this.observation.lowCardinalityKeyValue(keyValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation highCardinalityKeyValue(KeyValue keyValue) {
|
||||
return this.observation.highCardinalityKeyValue(keyValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation observationConvention(ObservationConvention<?> observationConvention) {
|
||||
return this.observation.observationConvention(observationConvention);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observation event(Event event) {
|
||||
return this.observation.event(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return this.observation.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scope openScope() {
|
||||
return this.observation.openScope();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebFilter wrap(WebFilter filter) {
|
||||
if (this.observation.isNoop()) {
|
||||
@@ -419,7 +578,8 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
.doOnCancel(this.observation::stop).doOnError((t) -> {
|
||||
this.observation.error(t);
|
||||
this.observation.stop();
|
||||
});
|
||||
}).contextWrite(
|
||||
(context) -> context.put(ObservationThreadLocalAccessor.KEY, this.observation));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -141,4 +144,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.
|
||||
@@ -132,12 +132,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
|
||||
|
||||
+21
-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,15 @@
|
||||
|
||||
package org.springframework.security.web.authentication;
|
||||
|
||||
import jakarta.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 +112,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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
-2
@@ -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.
|
||||
@@ -27,12 +27,19 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
public class SecurityContextLogoutHandlerTests {
|
||||
|
||||
@@ -76,4 +83,35 @@ public class SecurityContextLogoutHandlerTests {
|
||||
assertThat(beforeContext.getAuthentication()).isSameAs(beforeAuthentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenSecurityContextRepositoryThenSaveEmptyContext() {
|
||||
SecurityContextRepository repository = mock(SecurityContextRepository.class);
|
||||
this.handler.setSecurityContextRepository(repository);
|
||||
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
|
||||
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenClearAuthenticationFalseThenSaveEmptyContext() {
|
||||
SecurityContextRepository repository = mock(SecurityContextRepository.class);
|
||||
this.handler.setSecurityContextRepository(repository);
|
||||
this.handler.setClearAuthentication(false);
|
||||
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
|
||||
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenDefaultSecurityContextRepositoryThenHttpSessionSecurityContextRepository() {
|
||||
SecurityContextRepository securityContextRepository = (SecurityContextRepository) ReflectionTestUtils
|
||||
.getField(this.handler, "securityContextRepository");
|
||||
assertThat(securityContextRepository).isInstanceOf(HttpSessionSecurityContextRepository.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSecurityContextRepositoryWhenNullThenException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.handler.setSecurityContextRepository(null))
|
||||
.withMessage("securityContextRepository cannot be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -49,7 +49,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.util.FieldUtils;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -510,10 +510,10 @@ public class SwitchUserFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenDefaultSecurityContextRepositoryThenRequestAttributeRepository() {
|
||||
void filterWhenDefaultSecurityContextRepositoryThenHttpSessionRepository() {
|
||||
SwitchUserFilter switchUserFilter = new SwitchUserFilter();
|
||||
assertThat(ReflectionTestUtils.getField(switchUserFilter, "securityContextRepository"))
|
||||
.isInstanceOf(RequestAttributeSecurityContextRepository.class);
|
||||
.isInstanceOf(HttpSessionSecurityContextRepository.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+61
-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.
|
||||
@@ -141,4 +141,64 @@ public class DelegatingSecurityContextRepositoryTests {
|
||||
verifyNoInteractions(delegates.get(2));
|
||||
}
|
||||
|
||||
// gh-12314
|
||||
@Test
|
||||
public void loadContextWhenSecondDelegateReturnsThenContextFromSecondDelegate() {
|
||||
SecurityContextRepository delegate1 = mock(SecurityContextRepository.class);
|
||||
SecurityContextRepository delegate2 = mock(SecurityContextRepository.class);
|
||||
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(this.request, this.response);
|
||||
SecurityContext securityContext1 = mock(SecurityContext.class);
|
||||
SecurityContext securityContext2 = mock(SecurityContext.class);
|
||||
|
||||
given(delegate1.loadContext(holder)).willReturn(securityContext1);
|
||||
given(delegate1.containsContext(holder.getRequest())).willReturn(false);
|
||||
given(delegate2.loadContext(holder)).willReturn(securityContext2);
|
||||
given(delegate2.containsContext(holder.getRequest())).willReturn(true);
|
||||
|
||||
DelegatingSecurityContextRepository repository = new DelegatingSecurityContextRepository(delegate1, delegate2);
|
||||
SecurityContext returnedSecurityContext = repository.loadContext(holder);
|
||||
|
||||
assertThat(returnedSecurityContext).isSameAs(securityContext2);
|
||||
}
|
||||
|
||||
// gh-12314
|
||||
@Test
|
||||
public void loadContextWhenBothDelegateReturnsThenContextFromSecondDelegate() {
|
||||
SecurityContextRepository delegate1 = mock(SecurityContextRepository.class);
|
||||
SecurityContextRepository delegate2 = mock(SecurityContextRepository.class);
|
||||
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(this.request, this.response);
|
||||
SecurityContext securityContext1 = mock(SecurityContext.class);
|
||||
SecurityContext securityContext2 = mock(SecurityContext.class);
|
||||
|
||||
given(delegate1.loadContext(holder)).willReturn(securityContext1);
|
||||
given(delegate1.containsContext(holder.getRequest())).willReturn(true);
|
||||
given(delegate2.loadContext(holder)).willReturn(securityContext2);
|
||||
given(delegate2.containsContext(holder.getRequest())).willReturn(true);
|
||||
|
||||
DelegatingSecurityContextRepository repository = new DelegatingSecurityContextRepository(delegate1, delegate2);
|
||||
SecurityContext returnedSecurityContext = repository.loadContext(holder);
|
||||
|
||||
assertThat(returnedSecurityContext).isSameAs(securityContext2);
|
||||
}
|
||||
|
||||
// gh-12314
|
||||
@Test
|
||||
public void loadContextWhenFirstDelegateReturnsThenContextFromFirstDelegate() {
|
||||
SecurityContextRepository delegate1 = mock(SecurityContextRepository.class);
|
||||
SecurityContextRepository delegate2 = mock(SecurityContextRepository.class);
|
||||
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(this.request, this.response);
|
||||
SecurityContext securityContext1 = mock(SecurityContext.class);
|
||||
SecurityContext securityContext2 = mock(SecurityContext.class);
|
||||
|
||||
given(delegate1.loadContext(holder)).willReturn(securityContext1);
|
||||
given(delegate1.containsContext(holder.getRequest())).willReturn(true);
|
||||
given(delegate2.loadContext(holder)).willReturn(securityContext2);
|
||||
given(delegate2.containsContext(holder.getRequest())).willReturn(false);
|
||||
|
||||
DelegatingSecurityContextRepository repository = new DelegatingSecurityContextRepository(delegate1, delegate2);
|
||||
SecurityContext returnedSecurityContext = repository.loadContext(holder);
|
||||
|
||||
assertThat(returnedSecurityContext).isSameAs(securityContext1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+49
-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 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.core.context.TransientSecurityContext;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
@@ -748,6 +749,53 @@ public class HttpSessionSecurityContextRepositoryTests {
|
||||
assertThat(session).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenSecurityContextEmptyThenRemoveAttributeFromSession() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
|
||||
MockHttpSession session = (MockHttpSession) request.getSession(true);
|
||||
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, emptyContext);
|
||||
repo.saveContext(emptyContext, request, response);
|
||||
Object attributeAfterSave = session
|
||||
.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
|
||||
assertThat(attributeAfterSave).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenSecurityContextEmptyAndNoSessionThenDoesNotCreateSession() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
|
||||
repo.saveContext(emptyContext, request, response);
|
||||
assertThat(request.getSession(false)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenSecurityContextThenSaveInSession() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext context = createSecurityContext(PasswordEncodedUser.user());
|
||||
repo.saveContext(context, request, response);
|
||||
Object savedContext = request.getSession()
|
||||
.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
|
||||
assertThat(savedContext).isEqualTo(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveContextWhenTransientAuthenticationThenDoNotSave() {
|
||||
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(new SomeTransientAuthentication());
|
||||
repo.saveContext(context, request, response);
|
||||
assertThat(request.getSession(false)).isNull();
|
||||
}
|
||||
|
||||
private SecurityContext createSecurityContext(UserDetails userDetails) {
|
||||
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails,
|
||||
userDetails.getPassword(), userDetails.getAuthorities());
|
||||
|
||||
+150
@@ -16,15 +16,22 @@
|
||||
|
||||
package org.springframework.security.web.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationHandler;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -64,4 +71,147 @@ public class ObservationWebFilterChainDecoratorTests {
|
||||
verifyNoInteractions(handler);
|
||||
}
|
||||
|
||||
// gh-12849
|
||||
@Test
|
||||
void decorateWhenCustomAfterFilterThenObserves() {
|
||||
AccumulatingObservationHandler handler = new AccumulatingObservationHandler();
|
||||
ObservationRegistry registry = ObservationRegistry.create();
|
||||
registry.observationConfig().observationHandler(handler);
|
||||
ObservationWebFilterChainDecorator decorator = new ObservationWebFilterChainDecorator(registry);
|
||||
WebFilter mock = mock(WebFilter.class);
|
||||
given(mock.filter(any(), any())).willReturn(Mono.empty());
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
given(chain.filter(any())).willReturn(Mono.empty());
|
||||
WebFilterChain decorated = decorator.decorate(chain,
|
||||
List.of((e, c) -> c.filter(e).then(Mono.deferContextual((context) -> {
|
||||
Observation parentObservation = context.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
|
||||
Observation observation = Observation.createNotStarted("custom", registry)
|
||||
.parentObservation(parentObservation).contextualName("custom").start();
|
||||
return Mono.just("3").doOnSuccess((v) -> observation.stop()).doOnCancel(observation::stop)
|
||||
.doOnError((t) -> {
|
||||
observation.error(t);
|
||||
observation.stop();
|
||||
}).then(Mono.empty());
|
||||
}))));
|
||||
Observation http = Observation.start("http", registry).contextualName("http");
|
||||
try {
|
||||
decorated.filter(MockServerWebExchange.from(MockServerHttpRequest.get("/").build()))
|
||||
.contextWrite((context) -> context.put(ObservationThreadLocalAccessor.KEY, http)).block();
|
||||
}
|
||||
finally {
|
||||
http.stop();
|
||||
}
|
||||
handler.assertSpanStart(0, "http", null);
|
||||
handler.assertSpanStart(1, "spring.security.filterchains", "http");
|
||||
handler.assertSpanStop(2, "security filterchain before");
|
||||
handler.assertSpanStart(3, "secured request", "security filterchain before");
|
||||
handler.assertSpanStop(4, "secured request");
|
||||
handler.assertSpanStart(5, "spring.security.filterchains", "http");
|
||||
handler.assertSpanStart(6, "custom", "spring.security.filterchains");
|
||||
handler.assertSpanStop(7, "custom");
|
||||
handler.assertSpanStop(8, "security filterchain after");
|
||||
handler.assertSpanStop(9, "http");
|
||||
}
|
||||
|
||||
static class AccumulatingObservationHandler implements ObservationHandler<Observation.Context> {
|
||||
|
||||
List<Event> contexts = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public boolean supportsContext(Observation.Context context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(Observation.Context context) {
|
||||
this.contexts.add(new Event("start", context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Observation.Context context) {
|
||||
this.contexts.add(new Event("error", context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(Observation.Event event, Observation.Context context) {
|
||||
this.contexts.add(new Event("event", context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScopeOpened(Observation.Context context) {
|
||||
this.contexts.add(new Event("opened", context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScopeClosed(Observation.Context context) {
|
||||
this.contexts.add(new Event("closed", context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScopeReset(Observation.Context context) {
|
||||
this.contexts.add(new Event("reset", context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(Observation.Context context) {
|
||||
this.contexts.add(new Event("stop", context));
|
||||
}
|
||||
|
||||
private void assertSpanStart(int index, String name, String parentName) {
|
||||
Event event = this.contexts.get(index);
|
||||
assertThat(event.event).isEqualTo("start");
|
||||
if (event.contextualName == null) {
|
||||
assertThat(event.name).isEqualTo(name);
|
||||
}
|
||||
else {
|
||||
assertThat(event.contextualName).isEqualTo(name);
|
||||
}
|
||||
if (parentName == null) {
|
||||
return;
|
||||
}
|
||||
if (event.parentContextualName == null) {
|
||||
assertThat(event.parentName).isEqualTo(parentName);
|
||||
}
|
||||
else {
|
||||
assertThat(event.parentContextualName).isEqualTo(parentName);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSpanStop(int index, String name) {
|
||||
Event event = this.contexts.get(index);
|
||||
assertThat(event.event).isEqualTo("stop");
|
||||
if (event.contextualName == null) {
|
||||
assertThat(event.name).isEqualTo(name);
|
||||
}
|
||||
else {
|
||||
assertThat(event.contextualName).isEqualTo(name);
|
||||
}
|
||||
}
|
||||
|
||||
static class Event {
|
||||
|
||||
String event;
|
||||
|
||||
String name;
|
||||
|
||||
String contextualName;
|
||||
|
||||
String parentName;
|
||||
|
||||
String parentContextualName;
|
||||
|
||||
Event(String event, Observation.Context context) {
|
||||
this.event = event;
|
||||
this.name = context.getName();
|
||||
this.contextualName = context.getContextualName();
|
||||
if (context.getParentObservation() != null) {
|
||||
this.parentName = context.getParentObservation().getContextView().getName();
|
||||
this.parentContextualName = context.getParentObservation().getContextView().getContextualName();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user