1
0
mirror of synced 2026-07-08 20:30:04 +00:00

Compare commits

...

29 Commits

Author SHA1 Message Date
github-actions[bot] d6eeafb10c Release 5.7.8 2023-04-17 15:21:42 +00:00
Marcus Da Coregio 2d52fb8e4b Clear Repository on Logout 2023-04-17 06:47:57 -03:00
Marcus Da Coregio 37d8846652 Update org.springframework.data to 2021.2.11
Closes gh-13019
2023-04-14 13:57:28 -03:00
Marcus Da Coregio 74b8c6715b Update org.springframework to 5.3.27
Closes gh-13018
2023-04-14 10:41:45 -03:00
Marcus Da Coregio 8c9275067e Update org.eclipse.jetty to 9.4.51.v20230217
Closes gh-13017
2023-04-14 10:41:41 -03:00
Marcus Da Coregio 4b8bfee7c5 Update blockhound to 1.0.8.RELEASE
Closes gh-13016
2023-04-14 10:41:37 -03:00
Marcus Da Coregio b727f24c95 Update io.projectreactor to 2020.0.31
Closes gh-13014
2023-04-14 10:41:31 -03:00
Marcus Da Coregio d55c8aac4f Update logback-classic to 1.2.12
Closes gh-13013
2023-04-14 10:41:27 -03:00
Nick Meverden c4e9fb885d Delay JWSVerificationKeySelector Construction
Updating the NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder processor to execute the ConfigurableJWTProcessor customizations before holding a reference to the JWSKeySelector.

Closes gh-12960
2023-04-12 12:45:00 -06:00
Christian Marck 442faccb5f Avoid NPE in FilterInvocation
Handle unknown headers in dummy request wrapper.

Closes gh-12998
2023-04-11 17:01:59 -06:00
Marcus Da Coregio e25117856e Add static imports to code snippets
Closes gh-6597
2023-04-11 09:56:19 -03:00
Yuanhang Guo c69df9fba0 Fix javadoc typo in ReactiveAuthorizationManager
Closes gh-12978
2023-04-10 11:24:49 -05:00
Josh Cummings 39cee36065 Use SingletonSupplier
Issue gh-9991
2023-04-04 13:25:31 -06:00
Tatiana Slednikova a106188add Add nameIdFormat processing to OpenSamlAuthenticationRequestResolver
Closes gh-12825
2023-03-27 16:33:11 -06:00
Martin Tarjányi 5eefe9dcff Fix typo in SessionManagementConfigurer javadoc 2023-03-22 10:07:44 -03:00
Siva Kumar Edupuganti 98321b769a Fixed Broken Support Link
Closes gh-12759
2023-03-20 16:59:04 -06:00
twosom 3d7e22a4e9 Add test to SimpleUrlAuthenticationSuccessHandlerTests 2023-03-20 16:20:30 -06:00
Marcus Da Coregio 84cca81edf Use HttpSessionSecurityContextRepository by default in SwitchUserFilter
Closes gh-12834
2023-03-07 13:27:18 -03:00
bist 094bf1b527 Validate hasRole Input
There are no check for role prefix in AuthorizeHttpRequestsConfigurer#XXXrole
methods. This PR adds check for the same. Now the configuration
will fail if role/s start with prefix for hasRole and hasAnyRole methods.

Closes #12581
2023-03-03 15:00:34 -07:00
Josh Cummings 66665344c5 Formatting
Issue gh-12664
2023-03-01 11:23:45 -07:00
stnor 6c7703789a Ignore SPSSODescriptor elements
Closes gh-12664
2023-03-01 11:23:45 -07:00
Steve Riesenberg fabf7f649c Fix path of copied schema files
Issue gh-12804
2023-03-01 11:49:36 -06:00
Steve Riesenberg 7b88ab289d Add nohttp exclusion
Issue gh-12804
2023-02-28 16:52:19 -06:00
Steve Riesenberg 26566af431 Copy spring-security-oauth schemas on deploy
Closes gh-12804
2023-02-28 16:16:08 -06:00
Steve Riesenberg 5257e36ffc Update SaganApi to work with Contentful backend
Also fixed an issue with incorrect referenceDocUrl for Antora
with SNAPSHOT versions.

Closes gh-12575
2023-02-28 14:09:51 -06:00
Josh Cummings 0421e25cba Document Common SAML URI Endpoints
Issue gh-12764
2023-02-28 12:45:48 -07:00
Josh Cummings 1c885cf3a3 Document Federation Usecase
Closes gh-12764
2023-02-28 12:35:04 -07:00
Marcus Da Coregio 1c3ce1e401 Fix entity-id ignored in RelyingPartyRegistration XML config
Closes gh-11898
2023-02-23 15:16:40 -03:00
github-actions[bot] b851a8bf6d Next development version 2023-02-21 15:20:58 +00:00
46 changed files with 1255 additions and 150 deletions
+1 -1
View File
@@ -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();
}
@@ -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();
}
}
@@ -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
}
}
@@ -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;
@@ -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
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -208,30 +208,49 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
ParserContext parserContext) {
String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID);
String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION);
RelyingPartyRegistration.Builder builder;
if (StringUtils.hasText(metadataLocation)) {
builder = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId);
}
else {
builder = RelyingPartyRegistration.withRegistrationId(registrationId)
.assertingPartyDetails((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt,
assertingParties, apBuilder, parserContext));
}
addRemainingProperties(relyingPartyRegistrationElt, builder);
return builder;
}
private static void addRemainingProperties(Element relyingPartyRegistrationElt,
RelyingPartyRegistration.Builder builder) {
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
String singleLogoutServiceLocation = relyingPartyRegistrationElt
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt
.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt);
if (StringUtils.hasText(metadataLocation)) {
return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId)
.singleLogoutServiceLocation(singleLogoutServiceLocation)
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
.singleLogoutServiceBinding(singleLogoutServiceBinding);
}
String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
String assertionConsumerServiceLocation = relyingPartyRegistrationElt
.getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION);
Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding(
relyingPartyRegistrationElt);
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId)
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
.assertionConsumerServiceBinding(assertionConsumerServiceBinding)
.singleLogoutServiceLocation(singleLogoutServiceLocation)
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
.singleLogoutServiceBinding(singleLogoutServiceBinding)
.assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties,
builder, parserContext));
if (StringUtils.hasText(entityId)) {
builder.entityId(entityId);
}
if (StringUtils.hasText(singleLogoutServiceLocation)) {
builder.singleLogoutServiceLocation(singleLogoutServiceLocation);
}
if (StringUtils.hasText(singleLogoutServiceResponseLocation)) {
builder.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation);
}
if (singleLogoutServiceBinding != null) {
builder.singleLogoutServiceBinding(singleLogoutServiceBinding);
}
if (StringUtils.hasText(assertionConsumerServiceLocation)) {
builder.assertionConsumerServiceLocation(assertionConsumerServiceLocation);
}
if (assertionConsumerServiceBinding != null) {
builder.assertionConsumerServiceBinding(assertionConsumerServiceBinding);
}
}
private static void buildAssertingParty(Element relyingPartyElt, Map<String, Map<String, Object>> assertingParties,
@@ -309,7 +328,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
if (StringUtils.hasText(assertionConsumerServiceBinding)) {
return Saml2MessageBinding.valueOf(assertionConsumerServiceBinding);
}
return Saml2MessageBinding.REDIRECT;
return null;
}
private static Saml2MessageBinding getSingleLogoutServiceBinding(Element relyingPartyRegistrationElt) {
@@ -317,7 +336,7 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
if (StringUtils.hasText(singleLogoutServiceBinding)) {
return Saml2MessageBinding.valueOf(singleLogoutServiceBinding);
}
return Saml2MessageBinding.POST;
return null;
}
private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) {
@@ -1,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 {
@@ -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();
@@ -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;
}
@@ -25,7 +25,7 @@ import org.springframework.security.core.Authentication;
* A reactive authorization manager which can determine if an {@link Authentication} has
* access to a specific object.
*
* @param <T> the type of object that the authorization check is being done one.
* @param <T> the type of object that the authorization check is being done on.
* @author Rob Winch
* @since 5.0
*/
@@ -1,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
View File
@@ -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")
----
====
+2 -1
View File
@@ -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
View File
@@ -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
@@ -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)));
});
};
}
@@ -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) {
@@ -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
@@ -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;
}
@@ -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));
@@ -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"));
}
}
@@ -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))
@@ -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
@@ -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;
}
}
@@ -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() {
@@ -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);
}
}
@@ -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();
}
}
@@ -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");
}
}
@@ -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
@@ -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());