Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6eeafb10c | |||
| 2d52fb8e4b | |||
| 37d8846652 | |||
| 74b8c6715b | |||
| 8c9275067e | |||
| 4b8bfee7c5 | |||
| b727f24c95 | |||
| d55c8aac4f | |||
| c4e9fb885d | |||
| 442faccb5f | |||
| e25117856e | |||
| c69df9fba0 | |||
| 39cee36065 | |||
| a106188add | |||
| 5eefe9dcff | |||
| 98321b769a | |||
| 3d7e22a4e9 | |||
| 84cca81edf | |||
| 094bf1b527 | |||
| 66665344c5 | |||
| 6c7703789a | |||
| fabf7f649c | |||
| 7b88ab289d | |||
| 26566af431 | |||
| 5257e36ffc | |||
| 0421e25cba | |||
| 1c885cf3a3 | |||
| 1c3ce1e401 | |||
| b851a8bf6d |
+1
-1
@@ -57,7 +57,7 @@ See also the https://github.com/spring-projects/spring-framework/wiki/Gradle-bui
|
||||
|
||||
== Getting Support
|
||||
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
|
||||
https://spring.io/services[Commercial support] is available too.
|
||||
https://spring.io/support[Commercial support] is available too.
|
||||
|
||||
== Contributing
|
||||
https://help.github.com/articles/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/main/CONTRIBUTING.adoc[contributor guidelines] for details.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -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;
|
||||
@@ -325,6 +327,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* @return the {@link LogoutFilter} to use.
|
||||
*/
|
||||
private LogoutFilter createLogoutFilter(H http) {
|
||||
this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
|
||||
this.logoutHandlers.add(this.contextLogoutHandler);
|
||||
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
|
||||
LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
|
||||
@@ -334,6 +337,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
@@ -288,7 +288,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) {
|
||||
|
||||
+85
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -23,7 +26,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@@ -31,9 +37,12 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
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;
|
||||
@@ -42,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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;
|
||||
@@ -302,6 +312,80 @@ public class LogoutConfigurerTests {
|
||||
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception {
|
||||
CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class);
|
||||
this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire();
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder logoutRequest = post("/logout")
|
||||
.with(csrf())
|
||||
.with(user("user"))
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
|
||||
this.mvc.perform(logoutRequest)
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"));
|
||||
// @formatter:on
|
||||
int invocationCount = 2; // 1 from user() post processor and 1 from
|
||||
// SecurityContextLogoutHandler
|
||||
verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(),
|
||||
any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception {
|
||||
this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire();
|
||||
MockHttpSession session = mock(MockHttpSession.class);
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder logoutRequest = post("/logout")
|
||||
.with(csrf())
|
||||
.with(user("user"))
|
||||
.session(session)
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
|
||||
this.mvc.perform(logoutRequest)
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(redirectedUrl("/login?logout"))
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class InvalidateHttpSessionFalseConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout((logout) -> logout.invalidateHttpSession(false))
|
||||
.securityContext((context) -> context.requireExplicitSave(true));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class CustomSecurityContextRepositoryConfig {
|
||||
|
||||
static SecurityContextRepository repository;
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.logout(Customizer.withDefaults())
|
||||
.securityContext((context) -> context
|
||||
.requireExplicitSave(true)
|
||||
.securityContextRepository(repository)
|
||||
);
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class NullLogoutSuccessHandlerConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
|
||||
+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();
|
||||
|
||||
+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.
|
||||
@@ -46,12 +46,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);
|
||||
}
|
||||
|
||||
@@ -70,7 +73,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
|
||||
*/
|
||||
@@ -109,7 +113,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
|
||||
*/
|
||||
|
||||
+20
-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.
|
||||
@@ -41,6 +41,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))
|
||||
@@ -73,6 +82,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))
|
||||
|
||||
+6
-6
@@ -8,15 +8,15 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.28")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.31")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.3")
|
||||
api platform("org.junit:junit-bom:5.8.2")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.8")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.11")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.5")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.2.11"
|
||||
api "ch.qos.logback:logback-classic:1.2.12"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.22"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.35"
|
||||
@@ -26,7 +26,7 @@ dependencies {
|
||||
api "commons-codec:commons-codec:1.15"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.12.8"
|
||||
api "io.projectreactor.tools:blockhound:1.0.7.RELEASE"
|
||||
api "io.projectreactor.tools:blockhound:1.0.8.RELEASE"
|
||||
api "jakarta.inject:jakarta.inject-api:1.0.5"
|
||||
api "jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7"
|
||||
@@ -50,8 +50,8 @@ dependencies {
|
||||
api "org.assertj:assertj-core:3.22.0"
|
||||
api "org.bouncycastle:bcpkix-jdk15on:1.70"
|
||||
api "org.bouncycastle:bcprov-jdk15on:1.70"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.50.v20221201"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.50.v20221201"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.51.v20230217"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.51.v20230217"
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
|
||||
@@ -7,6 +7,8 @@ For example:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
||||
|
||||
@Test
|
||||
public void messageWhenNotAuthenticated() throws Exception {
|
||||
this.rest
|
||||
@@ -67,6 +69,7 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception {
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser
|
||||
|
||||
//...
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ For example:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
this.rest
|
||||
// provide a valid CSRF token
|
||||
.mutateWith(csrf())
|
||||
@@ -18,6 +20,8 @@ this.rest
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
|
||||
|
||||
this.rest
|
||||
// provide a valid CSRF token
|
||||
.mutateWith(csrf())
|
||||
|
||||
@@ -6,6 +6,9 @@ The basic setup looks like this:
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
|
||||
public class HelloWebfluxMethodApplicationTests {
|
||||
@@ -31,6 +34,9 @@ public class HelloWebfluxMethodApplicationTests {
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class])
|
||||
class HelloWebfluxMethodApplicationTests {
|
||||
|
||||
@@ -10,6 +10,7 @@ The default is that accessing the URL `/logout` will log the user out by:
|
||||
- Invalidating the HTTP Session
|
||||
- Cleaning up any RememberMe authentication that was configured
|
||||
- Clearing the `SecurityContextHolder`
|
||||
- Clearing the `SecurityContextRepository`
|
||||
- Redirect to `/login?logout`
|
||||
|
||||
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
|
||||
|
||||
@@ -1,17 +1,97 @@
|
||||
[[servlet-saml2login-authenticate-responses]]
|
||||
= Authenticating ``<saml2:Response>``s
|
||||
|
||||
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default.
|
||||
To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it.
|
||||
|
||||
You can configure this in a number of ways including:
|
||||
|
||||
1. Setting a clock skew to timestamp validation
|
||||
2. Mapping the response to a list of `GrantedAuthority` instances
|
||||
3. Customizing the strategy for validating assertions
|
||||
4. Customizing the strategy for decrypting response and assertion elements
|
||||
1. Changing the way the `RelyingPartyRegistration` is Looked Up
|
||||
2. Setting a clock skew to timestamp validation
|
||||
3. Mapping the response to a list of `GrantedAuthority` instances
|
||||
4. Customizing the strategy for validating assertions
|
||||
5. Customizing the strategy for decrypting response and assertion elements
|
||||
|
||||
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
|
||||
|
||||
[[relyingpartyregistrationresolver-apply]]
|
||||
== Changing `RelyingPartyRegistration` Lookup
|
||||
|
||||
`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`].
|
||||
|
||||
To apply a `RelyingPartyRegistrationResolver` when processing `<saml2:Response>` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) {
|
||||
return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations));
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter {
|
||||
return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations));
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default.
|
||||
If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso"))
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
// ...
|
||||
.saml2Login {
|
||||
filterProcessingUrl = "/saml2/login/sso"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
and:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso")
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
|
||||
== Setting a Clock Skew
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
[[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
|
||||
image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
|
||||
This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
|
||||
This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
|
||||
@@ -640,6 +641,16 @@ which in a deployed application would translate to
|
||||
|
||||
`+https://rp.example.com/adfs+`
|
||||
|
||||
The prevailing URI patterns are as follows:
|
||||
|
||||
* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
|
||||
* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`] based on the configurations for that `RelyingPartyRegistration`
|
||||
* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state
|
||||
* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration`
|
||||
|
||||
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
|
||||
If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
|
||||
|
||||
[[servlet-saml2login-rpr-credentials]]
|
||||
=== Credentials
|
||||
|
||||
@@ -712,56 +723,6 @@ resource.inputStream.use {
|
||||
[TIP]
|
||||
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
|
||||
|
||||
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
||||
=== Resolving the Relying Party from the Request
|
||||
|
||||
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
||||
|
||||
There are a number of reasons you may want to customize. Among them:
|
||||
|
||||
* You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
|
||||
* You may identify tenants in a way other than by the URI path
|
||||
|
||||
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
|
||||
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
|
||||
|
||||
You can provide a simpler resolver that, for example, always returns the same relying party:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
|
||||
private final RelyingPartyRegistrationResolver delegate;
|
||||
|
||||
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
|
||||
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
return this.delegate.resolve(request, "single");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
|
||||
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
|
||||
return this.delegate.resolve(request, "single")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Then, you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce ``<saml2:AuthnRequest>``s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ``<saml2:Response>``s], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
|
||||
|
||||
[NOTE]
|
||||
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
|
||||
|
||||
[[servlet-saml2login-rpr-duplicated]]
|
||||
=== Duplicated Relying Party Configurations
|
||||
|
||||
@@ -856,3 +817,184 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
||||
=== Resolving the `RelyingPartyRegistration` from the Request
|
||||
|
||||
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
||||
|
||||
There are a number of reasons you may want to customize that. Among them:
|
||||
|
||||
* You may already <<relyingpartyregistrationresolver-single, know which `RelyingPartyRegistration` you need>>
|
||||
* You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>>
|
||||
|
||||
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
|
||||
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
|
||||
|
||||
[NOTE]
|
||||
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
|
||||
|
||||
[[relyingpartyregistrationresolver-single]]
|
||||
==== Resolving to a Single Consistent `RelyingPartyRegistration`
|
||||
|
||||
You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
|
||||
private final RelyingPartyRegistrationResolver delegate;
|
||||
|
||||
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
|
||||
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
return this.delegate.resolve(request, "single");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
|
||||
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
|
||||
return this.delegate.resolve(request, "single")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`<saml2:SPSSODescriptor>` metadata production].
|
||||
|
||||
[[relyingpartyregistrationresolver-entityid]]
|
||||
==== Resolving Based on the `<saml2:Response#Issuer>`
|
||||
|
||||
When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <<servlet-saml2login-rpr-duplicated, relying party information duplicated across each instance>>.
|
||||
|
||||
This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable.
|
||||
|
||||
You can instead resolve the `registrationId` via the `Issuer`.
|
||||
A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
private final InMemoryRelyingPartyRegistrationRepository registrations;
|
||||
|
||||
// ... constructor
|
||||
|
||||
@Override
|
||||
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
|
||||
if (registrationId != null) {
|
||||
return this.registrations.findByRegistrationId(registrationId);
|
||||
}
|
||||
String entityId = resolveEntityIdFromSamlResponse(request);
|
||||
for (RelyingPartyRegistration registration : this.registrations) {
|
||||
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
|
||||
RelyingPartyRegistrationResolver {
|
||||
@Override
|
||||
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
|
||||
if (registrationId != null) {
|
||||
return this.registrations.findByRegistrationId(registrationId)
|
||||
}
|
||||
String entityId = resolveEntityIdFromSamlResponse(request)
|
||||
for (val registration : this.registrations) {
|
||||
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
|
||||
return registration
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`<saml2:Response>` authentication].
|
||||
|
||||
[[federating-saml2-login]]
|
||||
=== Federating Login
|
||||
|
||||
One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties.
|
||||
In this case, the identity provider's metadata endpoint returns multiple `<md:IDPSSODescriptor>` elements.
|
||||
|
||||
These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
||||
.stream().map((builder) -> builder
|
||||
.registrationId(UUID.randomUUID().toString())
|
||||
.entityId("https://example.org/saml2/sp")
|
||||
.build()
|
||||
)
|
||||
.collect(Collectors.toList()));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
||||
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
|
||||
.registrationId(UUID.randomUUID().toString())
|
||||
.entityId("https://example.org/saml2/sp")
|
||||
.build()
|
||||
}
|
||||
.collect(Collectors.toList()));
|
||||
----
|
||||
====
|
||||
|
||||
Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable.
|
||||
There are several ways to address this; let's focus on a way that suits the specific use case of federation.
|
||||
|
||||
In many federation cases, all the asserting parties share service provider configuration.
|
||||
Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`.
|
||||
|
||||
There are two main URIs you will want to change along those lines:
|
||||
|
||||
* <<relyingpartyregistrationresolver-entityid,Resolve by `<saml2:Response#Issuer>`>>
|
||||
* <<relyingpartyregistrationresolver-single,Resolve with a default `RelyingPartyRegistration`>>
|
||||
|
||||
[NOTE]
|
||||
Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal.
|
||||
|
||||
You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|
||||
|
||||
[[using-spring-security-saml-extension-uris]]
|
||||
=== Using Spring Security SAML Extension URIs
|
||||
|
||||
In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.
|
||||
|
||||
For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|
||||
|
||||
@@ -103,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/.*
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
aspectjVersion=1.9.19
|
||||
springJavaformatVersion=0.0.31
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=5.3.25
|
||||
springFrameworkVersion=5.3.27
|
||||
openSamlVersion=3.4.6
|
||||
version=5.7.7
|
||||
version=5.7.8
|
||||
kotlinVersion=1.6.21
|
||||
samplesBranch=5.7.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
+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
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,10 +31,12 @@ import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.NameIDPolicy;
|
||||
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller;
|
||||
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
@@ -72,6 +74,8 @@ class OpenSamlAuthenticationRequestResolver {
|
||||
|
||||
private final NameIDBuilder nameIdBuilder;
|
||||
|
||||
private final NameIDPolicyBuilder nameIdPolicyBuilder;
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSamlAuthenticationRequestResolver} using the provided
|
||||
* parameters
|
||||
@@ -92,6 +96,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");
|
||||
}
|
||||
|
||||
<T extends AbstractSaml2AuthenticationRequest> T resolve(HttpServletRequest request) {
|
||||
@@ -119,6 +126,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));
|
||||
|
||||
+14
@@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -243,4 +244,17 @@ public class RelyingPartyRegistrationsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionFromMetadataLocationCanHandleFederationMetadata() {
|
||||
Collection<RelyingPartyRegistration.Builder> federationMetadataWithSkippedSPEntries = RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("classpath:test-federated-metadata.xml");
|
||||
assertThat(federationMetadataWithSkippedSPEntries.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionFromMetadataLocationWithoutIdpThenSaml2Exception() {
|
||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> RelyingPartyRegistrations
|
||||
.collectionFromMetadataLocation("classpath:test-metadata-without-idp.xml"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,7 +38,7 @@ public final class TestRelyingPartyRegistrations {
|
||||
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
||||
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId).nameIdFormat("format")
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
|
||||
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -52,6 +52,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -75,6 +76,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -110,6 +112,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
@@ -132,6 +135,7 @@ public class OpenSamlAuthenticationRequestResolverTests {
|
||||
.assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build();
|
||||
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
|
||||
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
|
||||
assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat());
|
||||
assertThat(authnRequest.getAssertionConsumerServiceURL())
|
||||
.isEqualTo(registration.getAssertionConsumerServiceLocation());
|
||||
assertThat(authnRequest.getProtocolBinding())
|
||||
|
||||
@@ -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>
|
||||
@@ -26,6 +26,7 @@ import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -257,7 +258,11 @@ public class FilterInvocation {
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaders(String name) {
|
||||
return Collections.enumeration(this.headers.get(name));
|
||||
List<String> headerList = this.headers.get(name);
|
||||
if (headerList == null) {
|
||||
return Collections.emptyEnumeration();
|
||||
}
|
||||
return Collections.enumeration(headerList);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+16
@@ -27,6 +27,8 @@ import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -50,6 +52,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)
|
||||
@@ -73,6 +77,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
|
||||
if (this.clearAuthentication) {
|
||||
context.setAuthentication(null);
|
||||
}
|
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
|
||||
this.securityContextRepository.saveContext(emptyContext, request, response);
|
||||
}
|
||||
|
||||
public boolean isInvalidateHttpSession() {
|
||||
@@ -100,4 +106,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
@@ -58,6 +58,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;
|
||||
@@ -144,7 +145,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() {
|
||||
|
||||
+38
-8
@@ -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.
|
||||
@@ -137,13 +137,46 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
|
||||
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
|
||||
SaveContextOnUpdateOrErrorResponseWrapper.class);
|
||||
if (responseWrapper == null) {
|
||||
boolean httpSessionExists = request.getSession(false) != null;
|
||||
SecurityContext initialContext = SecurityContextHolder.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);
|
||||
@@ -369,11 +402,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -142,4 +145,23 @@ public class FilterInvocationTests {
|
||||
assertThat(filterInvocation.getRequest().getServletContext()).isSameAs(mockServletContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDummyRequestGetHeaders() {
|
||||
DummyRequest request = new DummyRequest();
|
||||
request.addHeader("known", "val");
|
||||
Enumeration<String> headers = request.getHeaders("known");
|
||||
assertThat(headers.hasMoreElements()).isTrue();
|
||||
assertThat(headers.nextElement()).isEqualTo("val");
|
||||
assertThat(headers.hasMoreElements()).isFalse();
|
||||
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDummyRequestGetHeadersNull() {
|
||||
DummyRequest request = new DummyRequest();
|
||||
Enumeration<String> headers = request.getHeaders("unknown");
|
||||
assertThat(headers.hasMoreElements()).isFalse();
|
||||
assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(headers::nextElement);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,11 +16,16 @@
|
||||
|
||||
package org.springframework.security.web.authentication;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -108,4 +113,20 @@ public class SimpleUrlAuthenticationSuccessHandlerTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ash.setTargetUrlParameter(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRemoveAuthenticationAttributeWhenOnAuthenticationSuccess() throws Exception {
|
||||
SimpleUrlAuthenticationSuccessHandler ash = new SimpleUrlAuthenticationSuccessHandler();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
HttpSession session = request.getSession();
|
||||
assertThat(session).isNotNull();
|
||||
session.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
|
||||
new BadCredentialsException("Invalid credentials"));
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNotNull();
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION))
|
||||
.isInstanceOf(AuthenticationException.class);
|
||||
ash.onAuthenticationSuccess(request, response, mock(Authentication.class));
|
||||
assertThat(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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
@@ -47,7 +47,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;
|
||||
@@ -491,10 +491,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
|
||||
|
||||
+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.
|
||||
@@ -53,6 +53,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;
|
||||
|
||||
@@ -763,6 +764,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());
|
||||
|
||||
Reference in New Issue
Block a user