Add Single Logout Support

Closes gh-8731
This commit is contained in:
Josh Cummings 2021-03-30 17:02:27 -06:00
parent 6488295cad
commit c63d618b26
49 changed files with 5738 additions and 34 deletions

View File

@ -1618,35 +1618,281 @@ filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
[[servlet-saml2login-logout]]
=== Performing Single Logout
Spring Security does not yet support single logout.
Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
Briefly, there are two use cases Spring Security supports:
* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party.
Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond
* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party.
Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party.
[NOTE]
In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot.
Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser.
=== Minimal Configuration for Single Logout
To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things:
* First, the asserting party must support SAML 2.0 Single Logout
* Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint
* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
==== RP-Initiated Single Logout
Given those, then for RP-initiated Single Logout, you can begin from the initial minimal example and add the following configuration:
[source,java]
----
@Value("${private.key}") RSAPrivateKey key;
@Value("${public.certificate}") X509Certificate certificate;
@Bean
RelyingPartyRegistrationRepository registrations() {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("id")
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
.build();
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}
@Bean
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
LogoutHandler logoutResponseHandler = logoutResponseHandler(registrationResolver);
LogoutSuccessHandler logoutRequestSuccessHandler = logoutRequestSuccessHandler(registrationResolver);
http
.authorizeRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.saml2Login(withDefaults())
.logout((logout) -> logout
.logoutUrl("/saml2/logout")
.logoutSuccessHandler(successHandler))
.addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class);
return http.build();
}
private LogoutSuccessHandler logoutRequestSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(registrationResolver);
return new Saml2LogoutRequestSuccessHandler(logoutRequestResolver);
}
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
}
----
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
<2> - Second, supply a `LogoutSuccessHandler` for initiating Single Logout, sending a `saml2:LogoutRequest` to the asserting party
<3> - Third, supply the `LogoutHandler` s needed to handle the `saml2:LogoutResponse` s sent from the asserting party.
==== Runtime Expectations for RP-Initiated
Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
Your application will then do the following:
1. Logout the user and invalidate the session
2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the currently logged-in user.
3. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
5. Redirect to any configured successful logout endpoint
[TIP]
If your asserting party does not send `<saml2:LogoutResponse>` s when logout is complete, the asserting party can still send a `POST /saml2/logout` and then there is no need to configure the `Saml2LogoutResponseHandler`.
==== AP-Initiated Single Logout
Instead of RP-initiated Single Logout, you can again begin from the initial minimal example and add the following configuration to achieve AP-initiated Single Logout:
[source,java]
----
@Value("${private.key}") RSAPrivateKey key;
@Value("${public.certificate}") X509Certificate certificate;
@Bean
RelyingPartyRegistrationRepository registrations() {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("id")
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
.build();
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}
@Bean
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
LogoutHandler logoutRequestHandler = logoutRequestHandler(registrationResolver);
LogoutSuccessHandler logoutResponseSuccessHandler = logoutResponseSuccessHandler(registrationResolver);
http
.authorizeRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.saml2Login(withDefaults())
.addFilterBefore(new Saml2LogoutRequestFilter(logoutResponseSuccessHandler, logoutRequestHandler), CsrfFilter.class);
return http.build();
}
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
return new CompositeLogoutHandler(
new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver),
new SecurityContextLogoutHandler(),
new LogoutSuccessEventPublishingLogoutHandler());
}
private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(registrationResolver);
return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver);
}
----
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
<2> - Second, supply the `LogoutHandler` needed to handle the `saml2:LogoutRequest` s sent from the asserting party.
<3> - Third, supply a `LogoutSuccessHandler` for completing Single Logout, sending a `saml2:LogoutResponse` to the asserting party
==== Runtime Expectations for AP-Initiated
Given the above configuration, an asserting party can send a `POST /logout/saml2` to your application that includes a `<saml2:LogoutRequest>`
Also, your application can participate in an AP-initated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
2. Logout the user and invalidate the session
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
[TIP]
If your asserting party does not expect you do send a `<saml2:LogoutResponse>` s when logout is complete, you may not need to configure a `LogoutSuccessHandler`
[NOTE]
In the event that you need to support both logout flows, you can combine the above to configurations.
=== Configuring Logout Endpoints
There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
* `/logout` - the endpoint for initiating single logout with an asserting party
* `/logout/saml2/slo` - the endpoint for receiving logout requests or responses from an asserting party
Because the user is already logged in, the `registrationId` is already known.
For this reason, `+{registrationId}+` is not part of these URLs by default.
These URLs are customizable in the DSL.
For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`.
To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
====
.Java
[source,java,role="primary"]
----
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
http
// ...
.logout(logout -> logout
.logoutSuccessHandler(myCustomSuccessHandler())
.logoutRequestMatcher(myRequestMatcher())
)
.addFilterBefore(filter, CsrfFilter.class);
----
.Kotlin
[source,kotlin,role="secondary"]
=== Customizing `<saml2:LogoutRequest>` Resolution
It's common to need to set other values in the `<saml2:LogoutRequest>` than the defaults that Spring Security provides.
By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation`
* The `ID` attribute - a GUID
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
* The `<NameID>` element - from `Authentication#getName`
To add other values, you can use delegation, like so:
[source,java]
----
http {
logout {
// ...
logoutSuccessHandler = myCustomSuccessHandler()
logoutRequestMatcher = myRequestMatcher()
OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver);
return (request, response, authentication) -> {
OpenSamlLogoutRequestBuilder builder = delegate.resolveLogoutRequest(request, response, authentication); <1>
builder.name(((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute")); <2>
builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now()));
return builder.logoutRequest(); <3>
};
----
<1> - Spring Security applies default values to a `<saml2:LogoutRequest>`
<2> - Your application specifies customizations
<3> - You complete the invocation by calling `request()`
[NOTE]
Support for OpenSAML 4 is coming.
In anticipation of that, `OpenSamlLogoutRequestResolver` does not add an `IssueInstant`.
Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it.
=== Customizing `<saml2:LogoutResponse>` Resolution
It's common to need to set other values in the `<saml2:LogoutResponse>` than the defaults that Spring Security provides.
By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation`
* The `ID` attribute - a GUID
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
* The `<Status>` element - `SUCCESS`
To add other values, you can use delegation, like so:
[source,java]
----
OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver);
return (request, response, authentication) -> {
OpenSamlLogoutResponseBuilder builder = delegate.resolveLogoutResponse(request, response, authentication); <1>
if (checkOtherPrevailingConditions()) {
builder.status(StatusCode.PARTIAL_LOGOUT); <2>
}
builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now()));
return builder.logoutResponse(); <3>
};
----
<1> - Spring Security applies default values to a `<saml2:LogoutResponse>`
<2> - Your application specifies customizations
<3> - You complete the invocation by calling `response()`
[NOTE]
Support for OpenSAML 4 is coming.
In anticipation of that, `OpenSamlLogoutResponseResolver` does not add an `IssueInstant`.
Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it.
=== Customizing `<saml2:LogoutRequest>` Validation
To customize validation, you can implement your own `LogoutHandler`.
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
[source,java]
----
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver);
return (request, response, authentication) -> {
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name
LogoutRequest logoutRequest = // ... parse using OpenSAML
// perform custom validation
}
}
----
====
The success handler will send logout requests to the asserting party.
=== Customizing `<saml2:LogoutResponse>` Validation
The request matcher will detect logout requests from the asserting party.
To customize validation, you can implement your own `LogoutHandler`.
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
[source,java]
----
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver);
return (request, response, authentication) -> {
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status
LogoutResponse logoutResponse = // ... parse using OpenSAML
// perform custom validation
}
}
----

View File

@ -37,6 +37,13 @@ public interface Saml2ErrorCodes {
*/
String MALFORMED_RESPONSE_DATA = "malformed_response_data";
/**
* Request is invalid in a general way.
*
* @since 5.6
*/
String INVALID_REQUEST = "invalid_request";
/**
* Response is invalid in a general way.
*

View File

@ -0,0 +1,175 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.function.Consumer;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlVerificationUtils.VerifierPartial;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
/**
* A {@link Saml2LogoutRequestValidator} that authenticates a SAML 2.0 Logout Requests
* received from a SAML 2.0 Asserting Party using OpenSAML.
*
* @author Josh Cummings
* @since 5.6
*/
public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
static {
OpenSamlInitializationService.initialize();
}
private final ParserPool parserPool;
private final LogoutRequestUnmarshaller unmarshaller;
/**
* Constructs a {@link OpenSamlLogoutRequestValidator}
*/
public OpenSamlLogoutRequestValidator() {
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.parserPool = registry.getParserPool();
this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) {
Saml2LogoutRequest request = parameters.getLogoutRequest();
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
Authentication authentication = parameters.getAuthentication();
byte[] b = Saml2Utils.samlDecode(request.getSamlRequest());
LogoutRequest logoutRequest = parse(inflateIfRequired(request, b));
return Saml2LogoutValidatorResult.withErrors().errors(verifySignature(request, logoutRequest, registration))
.errors(validateRequest(logoutRequest, registration, authentication)).build();
}
private String inflateIfRequired(Saml2LogoutRequest request, byte[] b) {
if (request.getBinding() == Saml2MessageBinding.REDIRECT) {
return Saml2Utils.samlInflate(b);
}
return new String(b, StandardCharsets.UTF_8);
}
private LogoutRequest parse(String request) throws Saml2Exception {
try {
Document document = this.parserPool
.parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8)));
Element element = document.getDocumentElement();
return (LogoutRequest) this.unmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize LogoutRequest", ex);
}
}
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest,
RelyingPartyRegistration registration) {
return (errors) -> {
VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(logoutRequest, registration);
if (logoutRequest.isSigned()) {
errors.addAll(partial.post(logoutRequest.getSignature()));
}
else {
errors.addAll(partial.redirect(request));
}
};
}
private Consumer<Collection<Saml2Error>> validateRequest(LogoutRequest request,
RelyingPartyRegistration registration, Authentication authentication) {
return (errors) -> {
validateIssuer(request, registration).accept(errors);
validateDestination(request, registration).accept(errors);
validateName(request, authentication).accept(errors);
};
}
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutRequest request,
RelyingPartyRegistration registration) {
return (errors) -> {
if (request.getIssuer() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
return;
}
String issuer = request.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) {
errors.add(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
};
}
private Consumer<Collection<Saml2Error>> validateDestination(LogoutRequest request,
RelyingPartyRegistration registration) {
return (errors) -> {
if (request.getDestination() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutResponse"));
return;
}
String destination = request.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceLocation())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
};
}
private Consumer<Collection<Saml2Error>> validateName(LogoutRequest request, Authentication authentication) {
return (errors) -> {
if (authentication == null) {
return;
}
NameID nameId = request.getNameID();
if (nameId == null) {
errors.add(
new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest"));
return;
}
String name = nameId.getValue();
if (!name.equals(authentication.getName())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST,
"Failed to match subject in LogoutRequest with currently logged in user"));
}
};
}
}

View File

@ -0,0 +1,187 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.function.Consumer;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.impl.LogoutResponseUnmarshaller;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlVerificationUtils.VerifierPartial;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
/**
* A {@link Saml2LogoutResponseValidator} that authenticates a SAML 2.0 Logout Responses
* received from a SAML 2.0 Asserting Party using OpenSAML.
*
* @author Josh Cummings
* @since 5.6
*/
public class OpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
static {
OpenSamlInitializationService.initialize();
}
private final ParserPool parserPool;
private final LogoutResponseUnmarshaller unmarshaller;
/**
* Constructs a {@link OpenSamlLogoutRequestValidator}
*/
public OpenSamlLogoutResponseValidator() {
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.parserPool = registry.getParserPool();
this.unmarshaller = (LogoutResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) {
Saml2LogoutResponse response = parameters.getLogoutResponse();
Saml2LogoutRequest request = parameters.getLogoutRequest();
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
byte[] b = Saml2Utils.samlDecode(response.getSamlResponse());
LogoutResponse logoutResponse = parse(inflateIfRequired(response, b));
return Saml2LogoutValidatorResult.withErrors().errors(verifySignature(response, logoutResponse, registration))
.errors(validateRequest(logoutResponse, registration))
.errors(validateLogoutRequest(logoutResponse, request.getId())).build();
}
private String inflateIfRequired(Saml2LogoutResponse response, byte[] b) {
if (response.getBinding() == Saml2MessageBinding.REDIRECT) {
return Saml2Utils.samlInflate(b);
}
return new String(b, StandardCharsets.UTF_8);
}
private LogoutResponse parse(String response) throws Saml2Exception {
try {
Document document = this.parserPool
.parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)));
Element element = document.getDocumentElement();
return (LogoutResponse) this.unmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize LogoutResponse", ex);
}
}
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutResponse response,
LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
return (errors) -> {
VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(logoutResponse, registration);
if (logoutResponse.isSigned()) {
errors.addAll(partial.post(logoutResponse.getSignature()));
}
else {
errors.addAll(partial.redirect(response));
}
};
}
private Consumer<Collection<Saml2Error>> validateRequest(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
validateIssuer(response, registration).accept(errors);
validateDestination(response, registration).accept(errors);
validateStatus(response).accept(errors);
};
}
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
if (response.getIssuer() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
return;
}
String issuer = response.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) {
errors.add(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
};
}
private Consumer<Collection<Saml2Error>> validateDestination(LogoutResponse response,
RelyingPartyRegistration registration) {
return (errors) -> {
if (response.getDestination() == null) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutResponse"));
return;
}
String destination = response.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
};
}
private Consumer<Collection<Saml2Error>> validateStatus(LogoutResponse response) {
return (errors) -> {
if (response.getStatus() == null) {
return;
}
if (response.getStatus().getStatusCode() == null) {
return;
}
if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) {
return;
}
if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) {
return;
}
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed"));
};
}
private Consumer<Collection<Saml2Error>> validateLogoutRequest(LogoutResponse response, String id) {
return (errors) -> {
if (response.getInResponseTo() == null) {
return;
}
if (response.getInResponseTo().equals(id)) {
return;
}
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE,
"LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest"));
};
}
}

View File

@ -0,0 +1,247 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.core.StatusResponseType;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.web.util.UriUtils;
/**
* Utility methods for verifying SAML component signatures with OpenSAML
*
* For internal use only.
*
* @author Josh Cummings
*/
final class OpenSamlVerificationUtils {
static VerifierPartial verifySignature(StatusResponseType object, RelyingPartyRegistration registration) {
return new VerifierPartial(object, registration);
}
static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyRegistration registration) {
return new VerifierPartial(object, registration);
}
static class VerifierPartial {
private final String id;
private final CriteriaSet criteria;
private final SignatureTrustEngine trustEngine;
VerifierPartial(StatusResponseType object, RelyingPartyRegistration registration) {
this.id = object.getID();
this.criteria = verificationCriteria(object.getIssuer());
this.trustEngine = trustEngine(registration);
}
VerifierPartial(RequestAbstractType object, RelyingPartyRegistration registration) {
this.id = object.getID();
this.criteria = verificationCriteria(object.getIssuer());
this.trustEngine = trustEngine(registration);
}
Collection<Saml2Error> redirect(Saml2LogoutRequest request) {
return redirect(new RedirectSignature(request));
}
Collection<Saml2Error> redirect(Saml2LogoutResponse response) {
return redirect(new RedirectSignature(response));
}
Collection<Saml2Error> redirect(RedirectSignature signature) {
if (signature.getAlgorithm() == null) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + this.id + "]"));
}
if (!signature.hasSignature()) {
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature for object [" + this.id + "]"));
}
Collection<Saml2Error> errors = new ArrayList<>();
String algorithmUri = signature.getAlgorithm();
try {
if (!this.trustEngine.validate(signature.getSignature(), signature.getContent(), algorithmUri,
this.criteria, null)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]: "));
}
return errors;
}
Collection<Saml2Error> post(Signature signature) {
Collection<Saml2Error> errors = new ArrayList<>();
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
try {
profileValidator.validate(signature);
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]: "));
}
try {
if (!this.trustEngine.validate(signature, this.criteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]"));
}
}
catch (Exception ex) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Invalid signature for object [" + this.id + "]: "));
}
return errors;
}
private CriteriaSet verificationCriteria(Issuer issuer) {
CriteriaSet criteria = new CriteriaSet();
criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())));
criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)));
criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
return criteria;
}
private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) {
Set<Credential> credentials = new HashSet<>();
Collection<Saml2X509Credential> keys = registration.getAssertingPartyDetails()
.getVerificationX509Credentials();
for (Saml2X509Credential key : keys) {
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(registration.getAssertingPartyDetails().getEntityId());
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
private static class RedirectSignature {
private final String algorithm;
private final byte[] signature;
private final byte[] content;
RedirectSignature(Saml2LogoutRequest request) {
this.algorithm = request.getParameter("SigAlg");
if (request.getParameter("Signature") != null) {
this.signature = Saml2Utils.samlDecode(request.getParameter("Signature"));
}
else {
this.signature = null;
}
this.content = content(request.getSamlRequest(), "SAMLRequest", request.getRelayState(),
request.getParameter("SigAlg"));
}
RedirectSignature(Saml2LogoutResponse response) {
this.algorithm = response.getParameter("SigAlg");
if (response.getParameter("Signature") != null) {
this.signature = Saml2Utils.samlDecode(response.getParameter("Signature"));
}
else {
this.signature = null;
}
this.content = content(response.getSamlResponse(), "SAMLResponse", response.getRelayState(),
response.getParameter("SigAlg"));
}
static byte[] content(String samlObject, String objectParameterName, String relayState, String algorithm) {
if (relayState != null) {
return String
.format("%s=%s&RelayState=%s&SigAlg=%s", objectParameterName,
UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1),
UriUtils.encode(relayState, StandardCharsets.ISO_8859_1),
UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1))
.getBytes(StandardCharsets.UTF_8);
}
else {
return String
.format("%s=%s&SigAlg=%s", objectParameterName,
UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1),
UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1))
.getBytes(StandardCharsets.UTF_8);
}
}
byte[] getContent() {
return this.content;
}
String getAlgorithm() {
return this.algorithm;
}
byte[] getSignature() {
return this.signature;
}
boolean hasSignature() {
return this.signature != null;
}
}
}
private OpenSamlVerificationUtils() {
}
}

View File

@ -0,0 +1,248 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
/**
* A class that represents a signed and serialized SAML 2.0 Logout Request
*
* @author Josh Cummings
* @since 5.6
*/
public final class Saml2LogoutRequest implements Serializable {
private final String location;
private final Saml2MessageBinding binding;
private final Map<String, String> parameters;
private final String id;
private final String relyingPartyRegistrationId;
private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map<String, String> parameters, String id,
String relyingPartyRegistrationId) {
this.location = location;
this.binding = binding;
this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters));
this.id = id;
this.relyingPartyRegistrationId = relyingPartyRegistrationId;
}
/**
* The unique identifier for this Logout Request
* @return the Logout Request identifier
*/
public String getId() {
return this.id;
}
/**
* Get the location of the asserting party's <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
* @return the SingleLogoutService location
*/
public String getLocation() {
return this.location;
}
/**
* Get the binding for the asserting party's <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
* @return the SingleLogoutService binding
*/
public Saml2MessageBinding getBinding() {
return this.binding;
}
/**
* Get the signed and serialized &lt;saml2:LogoutRequest&gt; payload
* @return the signed and serialized &lt;saml2:LogoutRequest&gt; payload
*/
public String getSamlRequest() {
return this.parameters.get("SAMLRequest");
}
/**
* The relay state associated with this Logout Request
* @return the relay state
*/
public String getRelayState() {
return this.parameters.get("RelayState");
}
/**
* Get the {@code name} parameters, a short-hand for <code>
* getParameters().get(name)
* </code>
*
* Useful when specifying additional query parameters for the Logout Request
* @param name the parameter's name
* @return the parameter's value
*/
public String getParameter(String name) {
return this.parameters.get(name);
}
/**
* Get all parameters
*
* Useful when specifying additional query parameters for the Logout Request
* @return the Logout Request query parameters
*/
public Map<String, String> getParameters() {
return this.parameters;
}
/**
* The identifier for the {@link RelyingPartyRegistration} associated with this Logout
* Request
* @return the {@link RelyingPartyRegistration} id
*/
public String getRelyingPartyRegistrationId() {
return this.relyingPartyRegistrationId;
}
/**
* Create a {@link Builder} instance from this {@link RelyingPartyRegistration}
*
* Specifically, this will pull the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
* location and binding from the {@link RelyingPartyRegistration}
* @param registration the {@link RelyingPartyRegistration} to use
* @return the {@link Builder} for further configurations
*/
public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) {
return new Builder(registration);
}
public static final class Builder {
private final RelyingPartyRegistration registration;
private String location;
private Saml2MessageBinding binding;
private Map<String, String> parameters = new HashMap<>();
private String id;
private Builder(RelyingPartyRegistration registration) {
this.registration = registration;
this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceLocation();
this.binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
}
/**
* Use this signed and serialized and Base64-encoded &lt;saml2:LogoutRequest&gt;
*
* Note that if using the Redirect binding, the value should be
* {@link java.util.zip.DeflaterOutputStream deflated} and then Base64-encoded.
*
* It should not be URL-encoded as this will be done when the request is sent
* @param samlRequest the &lt;saml2:LogoutRequest&gt; to use
* @return the {@link Builder} for further configurations
* @see Saml2LogoutRequestResolver
*/
public Builder samlRequest(String samlRequest) {
this.parameters.put("SAMLRequest", samlRequest);
return this;
}
/**
* Use this SAML 2.0 Message Binding
*
* By default, the asserting party's configured binding is used
* @param binding the SAML 2.0 Message Binding to use
* @return the {@link Builder} for further configurations
*/
public Builder binding(Saml2MessageBinding binding) {
this.binding = binding;
return this;
}
/**
* Use this location for the SAML 2.0 logout endpoint
*
* By default, the asserting party's endpoint is used
* @param location the SAML 2.0 location to use
* @return the {@link Builder} for further configurations
*/
public Builder location(String location) {
this.location = location;
return this;
}
/**
* Use this value for the relay state when sending the Logout Request to the
* asserting party
*
* It should not be URL-encoded as this will be done when the request is sent
* @param relayState the relay state
* @return the {@link Builder} for further configurations
*/
public Builder relayState(String relayState) {
this.parameters.put("RelayState", relayState);
return this;
}
/**
* This is the unique id used in the {@link #samlRequest}
* @param id the Logout Request id
* @return the {@link Builder} for further configurations
*/
public Builder id(String id) {
this.id = id;
return this;
}
/**
* Use this {@link Consumer} to modify the set of query parameters
*
* No parameter should be URL-encoded as this will be done when the request is
* sent
* @param parametersConsumer the {@link Consumer}
* @return the {@link Builder} for further configurations
*/
public Builder parameters(Consumer<Map<String, String>> parametersConsumer) {
parametersConsumer.accept(this.parameters);
return this;
}
/**
* Build the {@link Saml2LogoutRequest}
* @return a constructed {@link Saml2LogoutRequest}
*/
public Saml2LogoutRequest build() {
return new Saml2LogoutRequest(this.location, this.binding, this.parameters, this.id,
this.registration.getRegistrationId());
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
/**
* Validates SAML 2.0 Logout Requests
*
* @author Josh Cummings
* @since 5.6
*/
public interface Saml2LogoutRequestValidator {
/**
* Authenticates the SAML 2.0 Logout Request received from the SAML 2.0 Asserting
* Party.
*
* By default, verifies the signature, validates the issuer, destination, and user
* identifier.
* @param parameters the {@link Saml2LogoutRequestValidatorParameters} needed
* @return the authentication result
*/
Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters);
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
/**
* A holder of the parameters needed to invoke {@link Saml2LogoutRequestValidator}
*
* @author Josh Cummings
* @since 5.6
*/
public class Saml2LogoutRequestValidatorParameters {
private final Saml2LogoutRequest request;
private final RelyingPartyRegistration registration;
private final Authentication authentication;
/**
* Construct a {@link Saml2LogoutRequestValidatorParameters}
* @param request the SAML 2.0 Logout Request received from the asserting party
* @param registration the associated {@link RelyingPartyRegistration}
* @param authentication the current user
*/
public Saml2LogoutRequestValidatorParameters(Saml2LogoutRequest request, RelyingPartyRegistration registration,
Authentication authentication) {
this.request = request;
this.registration = registration;
this.authentication = authentication;
}
/**
* The SAML 2.0 Logout Request sent by the asserting party
* @return the logout request
*/
public Saml2LogoutRequest getLogoutRequest() {
return this.request;
}
/**
* The {@link RelyingPartyRegistration} representing this relying party
* @return the relying party
*/
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
/**
* The current {@link Authentication}
* @return the authenticated user
*/
public Authentication getAuthentication() {
return this.authentication;
}
}

View File

@ -0,0 +1,207 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
/**
* A class that represents a signed and serialized SAML 2.0 Logout Response
*
* @author Josh Cummings
* @since 5.6
*/
public final class Saml2LogoutResponse {
private final String location;
private final Saml2MessageBinding binding;
private final Map<String, String> parameters;
private Saml2LogoutResponse(String location, Saml2MessageBinding binding, Map<String, String> parameters) {
this.location = location;
this.binding = binding;
this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters));
}
/**
* Get the response location of the asserting party's <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
* @return the SingleLogoutService response location
*/
public String getResponseLocation() {
return this.location;
}
/**
* Get the binding for the asserting party's <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
* @return the SingleLogoutService binding
*/
public Saml2MessageBinding getBinding() {
return this.binding;
}
/**
* Get the signed and serialized &lt;saml2:LogoutResponse&gt; payload
* @return the signed and serialized &lt;saml2:LogoutResponse&gt; payload
*/
public String getSamlResponse() {
return this.parameters.get("SAMLResponse");
}
/**
* The relay state associated with this Logout Request
* @return the relay state
*/
public String getRelayState() {
return this.parameters.get("RelayState");
}
/**
* Get the {@code name} parameter, a short-hand for <code>
* getParameters().get(name)
* </code>
*
* Useful when specifying additional query parameters for the Logout Response
* @param name the parameter's name
* @return the parameter's value
*/
public String getParameter(String name) {
return this.parameters.get(name);
}
/**
* Get all parameters
*
* Useful when specifying additional query parameters for the Logout Response
* @return the Logout Response query parameters
*/
public Map<String, String> getParameters() {
return this.parameters;
}
/**
* Create a {@link Builder} instance from this {@link RelyingPartyRegistration}
*
* Specifically, this will pull the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
* response location and binding from the {@link RelyingPartyRegistration}
* @param registration the {@link RelyingPartyRegistration} to use
* @return the {@link Builder} for further configurations
*/
public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) {
return new Builder(registration);
}
public static final class Builder {
private String location;
private Saml2MessageBinding binding;
private Map<String, String> parameters = new HashMap<>();
private Builder(RelyingPartyRegistration registration) {
this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation();
this.binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
}
/**
* Use this signed and serialized and Base64-encoded &lt;saml2:LogoutResponse&gt;
*
* Note that if using the Redirect binding, the value should be
* {@link java.util.zip.DeflaterOutputStream deflated} and then Base64-encoded.
*
* It should not be URL-encoded as this will be done when the response is sent
* @param samlResponse the &lt;saml2:LogoutResponse&gt; to use
* @return the {@link Builder} for further configurations
* @see Saml2LogoutResponseResolver
*/
public Builder samlResponse(String samlResponse) {
this.parameters.put("SAMLResponse", samlResponse);
return this;
}
/**
* Use this SAML 2.0 Message Binding
*
* By default, the asserting party's configured binding is used
* @param binding the SAML 2.0 Message Binding to use
* @return the {@link Saml2LogoutRequest.Builder} for further configurations
*/
public Builder binding(Saml2MessageBinding binding) {
this.binding = binding;
return this;
}
/**
* Use this location for the SAML 2.0 logout endpoint
*
* By default, the asserting party's endpoint is used
* @param location the SAML 2.0 location to use
* @return the {@link Saml2LogoutRequest.Builder} for further configurations
*/
public Builder location(String location) {
this.location = location;
return this;
}
/**
* Use this value for the relay state when sending the Logout Request to the
* asserting party
*
* It should not be URL-encoded as this will be done when the response is sent
* @param relayState the relay state
* @return the {@link Builder} for further configurations
*/
public Builder relayState(String relayState) {
this.parameters.put("RelayState", relayState);
return this;
}
/**
* Use this {@link Consumer} to modify the set of query parameters
*
* No parameter should be URL-encoded as this will be done when the response is
* sent, though any signature specified should be Base64-encoded
* @param parametersConsumer the {@link Consumer}
* @return the {@link Builder} for further configurations
*/
public Builder parameters(Consumer<Map<String, String>> parametersConsumer) {
parametersConsumer.accept(this.parameters);
return this;
}
/**
* Build the {@link Saml2LogoutResponse}
* @return a constructed {@link Saml2LogoutResponse}
*/
public Saml2LogoutResponse build() {
return new Saml2LogoutResponse(this.location, this.binding, this.parameters);
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
/**
* Validates SAML 2.0 Logout Responses
*
* @author Josh Cummings
* @since 5.6
*/
public interface Saml2LogoutResponseValidator {
/**
* Authenticates the SAML 2.0 Logout Response received from the SAML 2.0 Asserting
* Party.
*
* By default, verifies the signature, validates the issuer, destination, and status.
* It also ensures that it aligns with the given logout request.
* @param parameters the {@link Saml2LogoutResponseValidatorParameters} needed
* @return the authentication result
*/
Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters);
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
/**
* A holder of the parameters needed to invoke {@link Saml2LogoutResponseValidator}
*
* @author Josh Cummings
* @since 5.6
*/
public class Saml2LogoutResponseValidatorParameters {
private final Saml2LogoutResponse response;
private final Saml2LogoutRequest request;
private final RelyingPartyRegistration registration;
/**
* Construct a {@link Saml2LogoutRequestValidatorParameters}
* @param response the SAML 2.0 Logout Response received from the asserting party
* @param request the SAML 2.0 Logout Request send by this application
* @param registration the associated {@link RelyingPartyRegistration}
*/
public Saml2LogoutResponseValidatorParameters(Saml2LogoutResponse response, Saml2LogoutRequest request,
RelyingPartyRegistration registration) {
this.response = response;
this.request = request;
this.registration = registration;
}
/**
* The SAML 2.0 Logout Response received from the asserting party
* @return the logout response
*/
public Saml2LogoutResponse getLogoutResponse() {
return this.response;
}
/**
* The SAML 2.0 Logout Request sent by this application
* @return the logout request
*/
public Saml2LogoutRequest getLogoutRequest() {
return this.request;
}
/**
* The {@link RelyingPartyRegistration} representing this relying party
* @return the relying party
*/
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Consumer;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.util.Assert;
/**
* A result emitted from a SAML 2.0 Logout validation attempt
*
* @author Josh Cummings
* @since 5.6
*/
public final class Saml2LogoutValidatorResult {
static final Saml2LogoutValidatorResult NO_ERRORS = new Saml2LogoutValidatorResult(Collections.emptyList());
private final Collection<Saml2Error> errors;
private Saml2LogoutValidatorResult(Collection<Saml2Error> errors) {
Assert.notNull(errors, "errors cannot be null");
this.errors = new ArrayList<>(errors);
}
/**
* Say whether this result indicates success
* @return whether this result has errors
*/
public boolean hasErrors() {
return !this.errors.isEmpty();
}
/**
* Return error details regarding the validation attempt
* @return the collection of results in this result, if any; returns an empty list
* otherwise
*/
public Collection<Saml2Error> getErrors() {
return Collections.unmodifiableCollection(this.errors);
}
/**
* Construct a successful {@link Saml2LogoutValidatorResult}
* @return an {@link Saml2LogoutValidatorResult} with no errors
*/
public static Saml2LogoutValidatorResult success() {
return NO_ERRORS;
}
/**
* Construct a {@link Saml2LogoutValidatorResult.Builder}, starting with the given
* {@code errors}.
*
* Note that a result with no errors is considered a success.
* @param errors
* @return
*/
public static Saml2LogoutValidatorResult.Builder withErrors(Saml2Error... errors) {
return new Builder(errors);
}
public static final class Builder {
private final Collection<Saml2Error> errors;
private Builder(Saml2Error... errors) {
this(Arrays.asList(errors));
}
private Builder(Collection<Saml2Error> errors) {
Assert.noNullElements(errors, "errors cannot have null elements");
this.errors = new ArrayList<>(errors);
}
public Builder errors(Consumer<Collection<Saml2Error>> errorsConsumer) {
errorsConsumer.accept(this.errors);
return this;
}
public Saml2LogoutValidatorResult build() {
return new Saml2LogoutValidatorResult(this.errors);
}
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;
import org.springframework.security.saml2.Saml2Exception;
/**
* Utility methods for working with serialized SAML messages.
*
* For internal use only.
*
* @author Josh Cummings
*/
final class Saml2Utils {
private Saml2Utils() {
}
static String samlEncode(byte[] b) {
return Base64.getEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
return Base64.getDecoder().decode(s);
}
static byte[] samlDeflate(String s) {
try {
ByteArrayOutputStream b = new ByteArrayOutputStream();
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true));
deflater.write(s.getBytes(StandardCharsets.UTF_8));
deflater.finish();
return b.toByteArray();
}
catch (IOException ex) {
throw new Saml2Exception("Unable to deflate string", ex);
}
}
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
catch (IOException ex) {
throw new Saml2Exception("Unable to inflate string", ex);
}
}
}

View File

@ -32,6 +32,7 @@ import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.signature.KeyInfo;
@ -85,6 +86,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
spSsoDescriptor.getKeyDescriptors()
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
return spSsoDescriptor;
}
@ -123,6 +125,14 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
return assertionConsumerService;
}
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration) {
SingleLogoutService singleLogoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation());
singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
singleLogoutService.setBinding(registration.getSingleLogoutServiceBinding().getUrn());
return singleLogoutService;
}
@SuppressWarnings("unchecked")
private <T> T build(QName elementName) {
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);

View File

@ -34,6 +34,7 @@ import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.Extensions;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
@ -105,6 +106,10 @@ class OpenSamlAssertingPartyMetadataConverter {
builder.assertingPartyDetails(
(party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())));
}
if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) {
throw new Saml2Exception(
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
}
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
Saml2MessageBinding binding;
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
@ -119,10 +124,27 @@ class OpenSamlAssertingPartyMetadataConverter {
builder.assertingPartyDetails(
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
.singleSignOnServiceBinding(binding));
return builder;
break;
}
throw new Saml2Exception(
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
Saml2MessageBinding binding;
if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
binding = Saml2MessageBinding.POST;
}
else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
binding = Saml2MessageBinding.REDIRECT;
}
else {
continue;
}
String responseLocation = (singleLogoutService.getResponseLocation() == null)
? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation();
builder.assertingPartyDetails(
(party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation())
.singleLogoutServiceResponseLocation(responseLocation).singleLogoutServiceBinding(binding));
break;
}
return builder;
}
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {

View File

@ -81,6 +81,12 @@ public final class RelyingPartyRegistration {
private final Saml2MessageBinding assertionConsumerServiceBinding;
private final String singleLogoutServiceLocation;
private final String singleLogoutServiceResponseLocation;
private final Saml2MessageBinding singleLogoutServiceBinding;
private final ProviderDetails providerDetails;
private final List<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials;
@ -90,7 +96,9 @@ public final class RelyingPartyRegistration {
private final Collection<Saml2X509Credential> signingX509Credentials;
private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
Saml2MessageBinding assertionConsumerServiceBinding, ProviderDetails providerDetails,
Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation,
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding,
ProviderDetails providerDetails,
Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials,
Collection<Saml2X509Credential> decryptionX509Credentials,
Collection<Saml2X509Credential> signingX509Credentials) {
@ -118,6 +126,9 @@ public final class RelyingPartyRegistration {
this.entityId = entityId;
this.assertionConsumerServiceLocation = assertionConsumerServiceLocation;
this.assertionConsumerServiceBinding = assertionConsumerServiceBinding;
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
this.providerDetails = providerDetails;
this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials));
this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials));
@ -177,6 +188,52 @@ public final class RelyingPartyRegistration {
return this.assertionConsumerServiceBinding;
}
/**
* Get the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Binding</a>
*
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in the
* relying party's &lt;SPSSODescriptor&gt;.
* @return the SingleLogoutService Binding
* @since 5.6
*/
public Saml2MessageBinding getSingleLogoutServiceBinding() {
return this.singleLogoutServiceBinding;
}
/**
* Get the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in the
* relying party's &lt;SPSSODescriptor&gt;.
* @return the SingleLogoutService Location
* @since 5.6
*/
public String getSingleLogoutServiceLocation() {
return this.singleLogoutServiceLocation;
}
/**
* Get the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Response Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService
* ResponseLocation="..."/&gt; in the relying party's &lt;SPSSODescriptor&gt;.
* @return the SingleLogoutService Response Location
* @since 5.6
*/
public String getSingleLogoutServiceResponseLocation() {
return this.singleLogoutServiceResponseLocation;
}
/**
* Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated
* with this relying party
@ -364,6 +421,9 @@ public final class RelyingPartyRegistration {
.decryptionX509Credentials((c) -> c.addAll(registration.getDecryptionX509Credentials()))
.assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation())
.assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding())
.singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation())
.singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation())
.singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding())
.assertingPartyDetails((assertingParty) -> assertingParty
.entityId(registration.getAssertingPartyDetails().getEntityId())
.wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned())
@ -376,7 +436,13 @@ public final class RelyingPartyRegistration {
.singleSignOnServiceLocation(
registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
.singleSignOnServiceBinding(
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()));
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
.singleLogoutServiceLocation(
registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
.singleLogoutServiceResponseLocation(
registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
.singleLogoutServiceBinding(
registration.getAssertingPartyDetails().getSingleLogoutServiceBinding()));
}
private static Saml2X509Credential fromDeprecated(
@ -445,10 +511,17 @@ public final class RelyingPartyRegistration {
private final Saml2MessageBinding singleSignOnServiceBinding;
private final String singleLogoutServiceLocation;
private final String singleLogoutServiceResponseLocation;
private final Saml2MessageBinding singleLogoutServiceBinding;
private AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, List<String> signingAlgorithms,
Collection<Saml2X509Credential> verificationX509Credentials,
Collection<Saml2X509Credential> encryptionX509Credentials, String singleSignOnServiceLocation,
Saml2MessageBinding singleSignOnServiceBinding) {
Saml2MessageBinding singleSignOnServiceBinding, String singleLogoutServiceLocation,
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding) {
Assert.hasText(entityId, "entityId cannot be null or empty");
Assert.notEmpty(signingAlgorithms, "signingAlgorithms cannot be empty");
Assert.notNull(verificationX509Credentials, "verificationX509Credentials cannot be null");
@ -472,6 +545,9 @@ public final class RelyingPartyRegistration {
this.encryptionX509Credentials = encryptionX509Credentials;
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
}
/**
@ -565,6 +641,51 @@ public final class RelyingPartyRegistration {
return this.singleSignOnServiceBinding;
}
/**
* Get the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in
* the asserting party's &lt;IDPSSODescriptor&gt;.
* @return the SingleLogoutService Location
* @since 5.6
*/
public String getSingleLogoutServiceLocation() {
return this.singleLogoutServiceLocation;
}
/**
* Get the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Response Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in
* the asserting party's &lt;IDPSSODescriptor&gt;.
* @return the SingleLogoutService Response Location
* @since 5.6
*/
public String getSingleLogoutServiceResponseLocation() {
return this.singleLogoutServiceResponseLocation;
}
/**
* Get the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Binding</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in
* the asserting party's &lt;IDPSSODescriptor&gt;.
* @return the SingleLogoutService Binding
* @since 5.6
*/
public Saml2MessageBinding getSingleLogoutServiceBinding() {
return this.singleLogoutServiceBinding;
}
public static final class Builder {
private String entityId;
@ -581,6 +702,12 @@ public final class RelyingPartyRegistration {
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
private String singleLogoutServiceLocation;
private String singleLogoutServiceResponseLocation;
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.REDIRECT;
/**
* Set the asserting party's <a href=
* "https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor">EntityID</a>.
@ -677,6 +804,59 @@ public final class RelyingPartyRegistration {
return this;
}
/**
* Set the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService
* Location="..."/&gt; in the asserting party's &lt;IDPSSODescriptor&gt;.
* @param singleLogoutServiceLocation the SingleLogoutService Location
* @return the {@link AssertingPartyDetails.Builder} for further configuration
* @since 5.6
*/
public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
return this;
}
/**
* Set the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Response Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService
* ResponseLocation="..."/&gt; in the asserting party's
* &lt;IDPSSODescriptor&gt;.
* @param singleLogoutServiceResponseLocation the SingleLogoutService Response
* Location
* @return the {@link AssertingPartyDetails.Builder} for further configuration
* @since 5.6
*/
public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) {
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
return this;
}
/**
* Set the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Binding</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt;
* in the asserting party's &lt;IDPSSODescriptor&gt;.
* @param singleLogoutServiceBinding the SingleLogoutService Binding
* @return the {@link AssertingPartyDetails.Builder} for further configuration
* @since 5.6
*/
public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
return this;
}
/**
* Creates an immutable ProviderDetails object representing the configuration
* for an Identity Provider, IDP
@ -689,7 +869,9 @@ public final class RelyingPartyRegistration {
return new AssertingPartyDetails(this.entityId, this.wantAuthnRequestsSigned, signingAlgorithms,
this.verificationX509Credentials, this.encryptionX509Credentials,
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding);
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding,
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
this.singleLogoutServiceBinding);
}
}
@ -830,6 +1012,12 @@ public final class RelyingPartyRegistration {
private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST;
private String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
private String singleLogoutServiceResponseLocation;
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.POST;
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new HashSet<>();
@ -933,6 +1121,58 @@ public final class RelyingPartyRegistration {
return this;
}
/**
* Set the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Binding</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in
* the relying party's &lt;SPSSODescriptor&gt;.
* @param singleLogoutServiceBinding the SingleLogoutService Binding
* @return the {@link Builder} for further configuration
* @since 5.6
*/
public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
return this;
}
/**
* Set the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in
* the relying party's &lt;SPSSODescriptor&gt;.
* @param singleLogoutServiceLocation the SingleLogoutService Location
* @return the {@link Builder} for further configuration
* @since 5.6
*/
public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
return this;
}
/**
* Set the <a href=
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
* Response Location</a>
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService
* ResponseLocation="..."/&gt; in the relying party's &lt;SPSSODescriptor&gt;.
* @param singleLogoutServiceResponseLocation the SingleLogoutService Response
* Location
* @return the {@link Builder} for further configuration
* @since 5.6
*/
public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) {
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
return this;
}
/**
* Apply this {@link Consumer} to further configure the Asserting Party details
* @param assertingPartyDetails The {@link Consumer} to apply
@ -1075,10 +1315,14 @@ public final class RelyingPartyRegistration {
for (Saml2X509Credential credential : this.providerDetails.assertingPartyDetailsBuilder.encryptionX509Credentials) {
this.credentials.add(toDeprecated(credential));
}
if (this.singleLogoutServiceResponseLocation == null) {
this.singleLogoutServiceResponseLocation = this.singleLogoutServiceLocation;
}
return new RelyingPartyRegistration(this.registrationId, this.entityId,
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
this.providerDetails.build(), this.credentials, this.decryptionX509Credentials,
this.signingX509Credentials);
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials,
this.decryptionX509Credentials, this.signingX509Credentials);
}
}

View File

@ -45,7 +45,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* @since 5.4
*/
public final class DefaultRelyingPartyRegistrationResolver
implements RelyingPartyRegistrationResolver, Converter<HttpServletRequest, RelyingPartyRegistration> {
implements Converter<HttpServletRequest, RelyingPartyRegistration>, RelyingPartyRegistrationResolver {
private Log logger = LogFactory.getLog(getClass());
@ -98,9 +98,14 @@ public final class DefaultRelyingPartyRegistrationResolver
String relyingPartyEntityId = templateResolver.apply(relyingPartyRegistration.getEntityId());
String assertionConsumerServiceLocation = templateResolver
.apply(relyingPartyRegistration.getAssertionConsumerServiceLocation());
String singleLogoutServiceLocation = templateResolver
.apply(relyingPartyRegistration.getSingleLogoutServiceLocation());
String singleLogoutServiceResponseLocation = templateResolver
.apply(relyingPartyRegistration.getSingleLogoutServiceResponseLocation());
return RelyingPartyRegistration.withRelyingPartyRegistration(relyingPartyRegistration)
.entityId(relyingPartyEntityId).assertionConsumerServiceLocation(assertionConsumerServiceLocation)
.build();
.singleLogoutServiceLocation(singleLogoutServiceLocation)
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation).build();
}
private Function<String, String> templateResolver(String applicationUri, RelyingPartyRegistration relyingParty) {
@ -108,6 +113,9 @@ public final class DefaultRelyingPartyRegistrationResolver
}
private static String resolveUrlTemplate(String template, String baseUrl, RelyingPartyRegistration relyingParty) {
if (template == null) {
return null;
}
String entityId = relyingParty.getAssertingPartyDetails().getEntityId();
String registrationId = relyingParty.getRegistrationId();
Map<String, String> uriVariables = new HashMap<>();

View File

@ -0,0 +1,105 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.security.MessageDigest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.util.Assert;
/**
* An implementation of an {@link Saml2LogoutRequestRepository} that stores
* {@link Saml2LogoutRequest} in the {@code HttpSession}.
*
* @author Josh Cummings
* @since 5.6
* @see Saml2LogoutRequestRepository
* @see Saml2LogoutRequest
*/
public final class HttpSessionLogoutRequestRepository implements Saml2LogoutRequestRepository {
private static final String DEFAULT_LOGOUT_REQUEST_ATTR_NAME = HttpSessionLogoutRequestRepository.class.getName()
+ ".LOGOUT_REQUEST";
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutRequest loadLogoutRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
Saml2LogoutRequest logoutRequest = (Saml2LogoutRequest) session.getAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME);
if (stateParameterEquals(request, logoutRequest)) {
return logoutRequest;
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void saveLogoutRequest(Saml2LogoutRequest logoutRequest, HttpServletRequest request,
HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
if (logoutRequest == null) {
request.getSession().removeAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME);
return;
}
String state = logoutRequest.getRelayState();
Assert.hasText(state, "logoutRequest.state cannot be empty");
request.getSession().setAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME, logoutRequest);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutRequest removeLogoutRequest(HttpServletRequest request, HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
Saml2LogoutRequest logoutRequest = loadLogoutRequest(request);
if (logoutRequest == null) {
return null;
}
request.getSession().removeAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME);
return logoutRequest;
}
private String getStateParameter(HttpServletRequest request) {
return request.getParameter("RelayState");
}
private boolean stateParameterEquals(HttpServletRequest request, Saml2LogoutRequest logoutRequest) {
String stateParameter = getStateParameter(request);
if (stateParameter == null || logoutRequest == null) {
return false;
}
String relayState = logoutRequest.getRelayState();
return MessageDigest.isEqual(Utf8.encode(stateParameter), Utf8.encode(relayState));
}
}

View File

@ -0,0 +1,167 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.BiConsumer;
import javax.servlet.http.HttpServletRequest;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutRequestMarshaller;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
import org.w3c.dom.Element;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
import org.springframework.util.Assert;
/**
* For internal use only. Intended for consolidating common behavior related to minting a
* SAML 2.0 Logout Request.
*/
final class OpenSamlLogoutRequestResolver {
static {
OpenSamlInitializationService.initialize();
}
private final Log logger = LogFactory.getLog(getClass());
private final LogoutRequestMarshaller marshaller;
private final IssuerBuilder issuerBuilder;
private final NameIDBuilder nameIdBuilder;
private final LogoutRequestBuilder logoutRequestBuilder;
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
/**
* Construct a {@link OpenSamlLogoutRequestResolver}
*/
OpenSamlLogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.marshaller = (LogoutRequestMarshaller) registry.getMarshallerFactory()
.getMarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.marshaller, "logoutRequestMarshaller must be configured in OpenSAML");
this.logoutRequestBuilder = (LogoutRequestBuilder) registry.getBuilderFactory()
.getBuilder(LogoutRequest.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.logoutRequestBuilder, "logoutRequestBuilder must be configured in OpenSAML");
this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
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");
}
/**
* Prepare to create, sign, and serialize a SAML 2.0 Logout Request.
*
* By default, includes a {@code NameID} based on the {@link Authentication} instance
* as well as the {@code Destination} and {@code Issuer} based on the
* {@link RelyingPartyRegistration} derived from the {@link Authentication}.
* @param request the HTTP request
* @param authentication the current user
* @return a signed and serialized SAML 2.0 Logout Request
*/
Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
return resolve(request, authentication, (registration, logoutRequest) -> {
});
}
Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication,
BiConsumer<RelyingPartyRegistration, LogoutRequest> logoutRequestConsumer) {
String registrationId = getRegistrationId(authentication);
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId);
if (registration == null) {
return null;
}
LogoutRequest logoutRequest = this.logoutRequestBuilder.buildObject();
logoutRequest.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
Issuer issuer = this.issuerBuilder.buildObject();
issuer.setValue(registration.getEntityId());
logoutRequest.setIssuer(issuer);
NameID nameId = this.nameIdBuilder.buildObject();
nameId.setValue(authentication.getName());
logoutRequest.setNameID(nameId);
logoutRequestConsumer.accept(registration, logoutRequest);
if (logoutRequest.getID() == null) {
logoutRequest.setID("LR" + UUID.randomUUID());
}
String relayState = UUID.randomUUID().toString();
Saml2LogoutRequest.Builder result = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.id(logoutRequest.getID());
if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) {
String xml = serialize(OpenSamlSigningUtils.sign(logoutRequest, registration));
String samlRequest = Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8));
return result.samlRequest(samlRequest).relayState(relayState).build();
}
else {
String xml = serialize(logoutRequest);
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
result.samlRequest(deflatedAndEncoded);
QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration)
.param("SAMLRequest", deflatedAndEncoded).param("RelayState", relayState);
return result.parameters((params) -> params.putAll(partial.parameters())).build();
}
}
private String getRegistrationId(Authentication authentication) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Attempting to resolve registrationId from " + authentication);
}
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof Saml2AuthenticatedPrincipal) {
return ((Saml2AuthenticatedPrincipal) principal).getRelyingPartyRegistrationId();
}
return null;
}
private String serialize(LogoutRequest logoutRequest) {
try {
Element element = this.marshaller.marshall(logoutRequest);
return SerializeSupport.nodeToString(element);
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}

View File

@ -0,0 +1,218 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.BiConsumer;
import javax.servlet.http.HttpServletRequest;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.Status;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller;
import org.opensaml.saml.saml2.core.impl.LogoutResponseBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutResponseMarshaller;
import org.opensaml.saml.saml2.core.impl.StatusBuilder;
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
import org.springframework.util.Assert;
/**
* For internal use only. Intended for consolidating common behavior related to minting a
* SAML 2.0 Logout Response.
*/
final class OpenSamlLogoutResponseResolver {
static {
OpenSamlInitializationService.initialize();
}
private final Log logger = LogFactory.getLog(getClass());
private final ParserPool parserPool;
private final LogoutRequestUnmarshaller unmarshaller;
private final LogoutResponseMarshaller marshaller;
private final LogoutResponseBuilder logoutResponseBuilder;
private final IssuerBuilder issuerBuilder;
private final StatusBuilder statusBuilder;
private final StatusCodeBuilder statusCodeBuilder;
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
/**
* Construct a {@link OpenSamlLogoutResponseResolver}
*/
OpenSamlLogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.parserPool = registry.getParserPool();
this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME);
this.marshaller = (LogoutResponseMarshaller) registry.getMarshallerFactory()
.getMarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.marshaller, "logoutResponseMarshaller must be configured in OpenSAML");
this.logoutResponseBuilder = (LogoutResponseBuilder) registry.getBuilderFactory()
.getBuilder(LogoutResponse.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.logoutResponseBuilder, "logoutResponseBuilder must be configured in OpenSAML");
this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML");
this.statusBuilder = (StatusBuilder) registry.getBuilderFactory().getBuilder(Status.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.statusBuilder, "statusBuilder must be configured in OpenSAML");
this.statusCodeBuilder = (StatusCodeBuilder) registry.getBuilderFactory()
.getBuilder(StatusCode.DEFAULT_ELEMENT_NAME);
Assert.notNull(this.statusCodeBuilder, "statusCodeBuilder must be configured in OpenSAML");
}
/**
* Prepare to create, sign, and serialize a SAML 2.0 Logout Response.
*
* By default, includes a {@code RelayState} based on the {@link HttpServletRequest}
* as well as the {@code Destination} and {@code Issuer} based on the
* {@link RelyingPartyRegistration} derived from the {@link Authentication}. The
* logout response is also marked as {@code SUCCESS}.
* @param request the HTTP request
* @param authentication the current user
* @return a signed and serialized SAML 2.0 Logout Response
*/
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
return resolve(request, authentication, (registration, logoutResponse) -> {
});
}
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
BiConsumer<RelyingPartyRegistration, LogoutResponse> logoutResponseConsumer) {
String registrationId = getRegistrationId(authentication);
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId);
if (registration == null) {
return null;
}
String serialized = request.getParameter("SAMLRequest");
byte[] b = Saml2Utils.samlDecode(serialized);
LogoutRequest logoutRequest = parse(inflateIfRequired(registration, b));
LogoutResponse logoutResponse = this.logoutResponseBuilder.buildObject();
logoutResponse.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
Issuer issuer = this.issuerBuilder.buildObject();
issuer.setValue(registration.getEntityId());
logoutResponse.setIssuer(issuer);
StatusCode code = this.statusCodeBuilder.buildObject();
code.setValue(StatusCode.SUCCESS);
Status status = this.statusBuilder.buildObject();
status.setStatusCode(code);
logoutResponse.setStatus(status);
logoutResponse.setInResponseTo(logoutRequest.getID());
if (logoutResponse.getID() == null) {
logoutResponse.setID("LR" + UUID.randomUUID());
}
logoutResponseConsumer.accept(registration, logoutResponse);
Saml2LogoutResponse.Builder result = Saml2LogoutResponse.withRelyingPartyRegistration(registration);
if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) {
String xml = serialize(OpenSamlSigningUtils.sign(logoutResponse, registration));
String samlResponse = Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8));
result.samlResponse(samlResponse);
if (request.getParameter("RelayState") != null) {
result.relayState(request.getParameter("RelayState"));
}
return result.build();
}
else {
String xml = serialize(logoutResponse);
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
result.samlResponse(deflatedAndEncoded);
QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration).param("SAMLResponse",
deflatedAndEncoded);
if (request.getParameter("RelayState") != null) {
partial.param("RelayState", request.getParameter("RelayState"));
}
return result.parameters((params) -> params.putAll(partial.parameters())).build();
}
}
private String getRegistrationId(Authentication authentication) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Attempting to resolve registrationId from " + authentication);
}
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof Saml2AuthenticatedPrincipal) {
return ((Saml2AuthenticatedPrincipal) principal).getRelyingPartyRegistrationId();
}
return null;
}
private String inflateIfRequired(RelyingPartyRegistration registration, byte[] b) {
if (registration.getSingleLogoutServiceBinding() == Saml2MessageBinding.REDIRECT) {
return Saml2Utils.samlInflate(b);
}
return new String(b, StandardCharsets.UTF_8);
}
private LogoutRequest parse(String request) throws Saml2Exception {
try {
Document document = this.parserPool
.parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8)));
Element element = document.getDocumentElement();
return (LogoutRequest) this.unmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception("Failed to deserialize LogoutRequest", ex);
}
}
private String serialize(LogoutResponse logoutResponse) {
try {
Element element = this.marshaller.marshall(logoutResponse);
return SerializeSupport.nodeToString(element);
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.w3c.dom.Element;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* Utility methods for signing SAML components with OpenSAML
*
* For internal use only.
*
* @author Josh Cummings
*/
final class OpenSamlSigningUtils {
static String serialize(XMLObject object) {
try {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
Element element = marshaller.marshall(object);
return SerializeSupport.nodeToString(element);
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
static <O extends SignableXMLObject> O sign(O object, RelyingPartyRegistration relyingPartyRegistration) {
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
try {
SignatureSupport.signObject(object, parameters);
return object;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
static QueryParametersPartial sign(RelyingPartyRegistration registration) {
return new QueryParametersPartial(registration);
}
private static SignatureSigningParameters resolveSigningParameters(
RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
CriteriaSet criteria = new CriteriaSet();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(algorithms);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private static List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setEntityId(relyingPartyRegistration.getEntityId());
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
static class QueryParametersPartial {
final RelyingPartyRegistration registration;
final Map<String, String> components = new LinkedHashMap<>();
QueryParametersPartial(RelyingPartyRegistration registration) {
this.registration = registration;
}
QueryParametersPartial param(String key, String value) {
this.components.put(key, value);
return this;
}
Map<String, String> parameters() {
SignatureSigningParameters parameters = resolveSigningParameters(this.registration);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put("SigAlg", algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put("Signature", b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
}
private OpenSamlSigningUtils() {
}
}

View File

@ -0,0 +1,250 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* A filter for handling logout requests in the form of a &lt;saml2:LogoutRequest&gt; sent
* from the asserting party.
*
* @author Josh Cummings
* @since 5.6
* @see Saml2LogoutRequestValidator
* @see Saml2AssertingPartyInitiatedLogoutSuccessHandler
*/
public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
private final Log logger = LogFactory.getLog(getClass());
private final Saml2LogoutRequestValidator logoutRequestValidator;
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
private final Saml2LogoutResponseResolver logoutResponseResolver;
private final LogoutHandler handler;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout/saml2/slo");
/**
* Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout
* Requests from the asserting party
* @param relyingPartyRegistrationResolver the strategy for resolving a
* {@link RelyingPartyRegistration}
* @param logoutRequestValidator the SAML 2.0 Logout Request authenticator
* @param logoutResponseResolver the strategy for creating a SAML 2.0 Logout Response
* @param handlers the actions that perform logout
*/
public Saml2LogoutRequestFilter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver,
Saml2LogoutRequestValidator logoutRequestValidator, Saml2LogoutResponseResolver logoutResponseResolver,
LogoutHandler... handlers) {
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
this.logoutRequestValidator = logoutRequestValidator;
this.logoutResponseResolver = logoutResponseResolver;
this.handler = new CompositeLogoutHandler(handlers);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (!this.logoutRequestMatcher.matches(request)) {
chain.doFilter(request, response);
return;
}
if (request.getParameter("SAMLRequest") == null) {
chain.doFilter(request, response);
return;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
getRegistrationId(authentication));
if (registration == null) {
this.logger
.trace("Did not process logout request since failed to find associated RelyingPartyRegistration");
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (!isCorrectBinding(request, registration)) {
this.logger.trace("Did not process logout request since used incorrect binding");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String serialized = request.getParameter("SAMLRequest");
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(serialized).relayState(request.getParameter("RelayState"))
.binding(registration.getSingleLogoutServiceBinding())
.location(registration.getSingleLogoutServiceLocation())
.parameters((params) -> params.put("SigAlg", request.getParameter("SigAlg")))
.parameters((params) -> params.put("Signature", request.getParameter("Signature"))).build();
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(logoutRequest,
registration, authentication);
Saml2LogoutValidatorResult result = this.logoutRequestValidator.validate(parameters);
if (result.hasErrors()) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, result.getErrors().iterator().next().toString());
this.logger.debug(LogMessage.format("Failed to validate LogoutRequest: %s", result.getErrors()));
return;
}
this.handler.logout(request, response, authentication);
Saml2LogoutResponse logoutResponse = this.logoutResponseResolver.resolve(request, authentication);
if (logoutResponse == null) {
this.logger.trace("Returning 401 since no logout response generated");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
if (logoutResponse.getBinding() == Saml2MessageBinding.REDIRECT) {
doRedirect(request, response, logoutResponse);
}
else {
doPost(response, logoutResponse);
}
}
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
this.logoutRequestMatcher = logoutRequestMatcher;
}
private String getRegistrationId(Authentication authentication) {
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof Saml2AuthenticatedPrincipal) {
return ((Saml2AuthenticatedPrincipal) principal).getRelyingPartyRegistrationId();
}
return null;
}
private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) {
Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding();
if (requiredBinding == Saml2MessageBinding.POST) {
return "POST".equals(request.getMethod());
}
return "GET".equals(request.getMethod());
}
private void doRedirect(HttpServletRequest request, HttpServletResponse response,
Saml2LogoutResponse logoutResponse) throws IOException {
String location = logoutResponse.getResponseLocation();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
addParameter("SAMLResponse", logoutResponse::getParameter, uriBuilder);
addParameter("RelayState", logoutResponse::getParameter, uriBuilder);
addParameter("SigAlg", logoutResponse::getParameter, uriBuilder);
addParameter("Signature", logoutResponse::getParameter, uriBuilder);
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void addParameter(String name, Function<String, String> parameters, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(parameters.apply(name))) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1));
}
}
private void doPost(HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException {
String location = logoutResponse.getResponseLocation();
String saml = logoutResponse.getSamlResponse();
String relayState = logoutResponse.getRelayState();
String html = createSamlPostRequestFormData(location, saml, relayState);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
}
private String createSamlPostRequestFormData(String location, String saml, String relayState) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n").append(" <head>\n");
html.append(" <meta charset=\"utf-8\" />\n");
html.append(" </head>\n");
html.append(" <body onload=\"document.forms[0].submit()\">\n");
html.append(" <noscript>\n");
html.append(" <p>\n");
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
html.append(" you must press the Continue button once to proceed.\n");
html.append(" </p>\n");
html.append(" </noscript>\n");
html.append(" \n");
html.append(" <form action=\"");
html.append(location);
html.append("\" method=\"post\">\n");
html.append(" <div>\n");
html.append(" <input type=\"hidden\" name=\"SAMLResponse\" value=\"");
html.append(HtmlUtils.htmlEscape(saml));
html.append("\"/>\n");
if (StringUtils.hasText(relayState)) {
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
html.append(HtmlUtils.htmlEscape(relayState));
html.append("\"/>\n");
}
html.append(" </div>\n");
html.append(" <noscript>\n");
html.append(" <div>\n");
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
html.append(" </div>\n");
html.append(" </noscript>\n");
html.append(" </form>\n");
html.append(" \n");
html.append(" </body>\n");
html.append("</html>");
return html.toString();
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
/**
* Implementations of this interface are responsible for the persistence of
* {@link Saml2LogoutRequest} between requests.
*
* <p>
* Used by the {@link Saml2RelyingPartyInitiatedLogoutSuccessHandler} for persisting the
* Logout Request before it initiates the SAML 2.0 SLO flow. As well, used by
* {@code OpenSamlLogoutResponseHandler} for resolving the Logout Request associated with
* that Logout Response.
*
* @author Josh Cummings
* @since 5.6
* @see Saml2LogoutRequest
* @see HttpSessionLogoutRequestRepository
*/
public interface Saml2LogoutRequestRepository {
/**
* Returns the {@link Saml2LogoutRequest} associated to the provided
* {@code HttpServletRequest} or {@code null} if not available.
* @param request the {@code HttpServletRequest}
* @return the {@link Saml2LogoutRequest} or {@code null} if not available
*/
Saml2LogoutRequest loadLogoutRequest(HttpServletRequest request);
/**
* Persists the {@link Saml2LogoutRequest} associating it to the provided
* {@code HttpServletRequest} and/or {@code HttpServletResponse}.
* @param logoutRequest the {@link Saml2LogoutRequest}
* @param request the {@code HttpServletRequest}
* @param response the {@code HttpServletResponse}
*/
void saveLogoutRequest(Saml2LogoutRequest logoutRequest, HttpServletRequest request, HttpServletResponse response);
/**
* Removes and returns the {@link Saml2LogoutRequest} associated to the provided
* {@code HttpServletRequest} and {@code HttpServletResponse} or if not available
* returns {@code null}.
* @param request the {@code HttpServletRequest}
* @param response the {@code HttpServletResponse}
* @return the {@link Saml2LogoutRequest} or {@code null} if not available
*/
Saml2LogoutRequest removeLogoutRequest(HttpServletRequest request, HttpServletResponse response);
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
/**
* Creates a signed SAML 2.0 Logout Request based on information from the
* {@link HttpServletRequest} and current {@link Authentication}.
*
* The returned logout request is suitable for sending to the asserting party based on,
* for example, the location and binding specified in
* {@link RelyingPartyRegistration#getAssertingPartyDetails()}.
*
* @author Josh Cummings
* @since 5.6
* @see RelyingPartyRegistration
*/
public interface Saml2LogoutRequestResolver {
/**
* Prepare to create, sign, and serialize a SAML 2.0 Logout Request.
*
* By default, includes a {@code NameID} based on the {@link Authentication} instance.
* @param request the HTTP request
* @param authentication the current user
* @return a signed and serialized SAML 2.0 Logout Request
*/
Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication);
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidatorParameters;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A filter for handling a &lt;saml2:LogoutResponse&gt; sent from the asserting party. A
* &lt;saml2:LogoutResponse&gt; is sent in response to a &lt;saml2:LogoutRequest&gt;
* already sent by the relying party.
*
* Note that before a &lt;saml2:LogoutRequest&gt; is sent, the user is logged out. Given
* that, this implementation should not use any {@link LogoutSuccessHandler} that relies
* on the user being logged in.
*
* @author Josh Cummings
* @since 5.6
* @see Saml2LogoutRequestRepository
* @see Saml2LogoutResponseValidator
*/
public final class Saml2LogoutResponseFilter extends OncePerRequestFilter {
private final Log logger = LogFactory.getLog(getClass());
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
private final Saml2LogoutResponseValidator logoutResponseValidator;
private final LogoutSuccessHandler logoutSuccessHandler;
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout/saml2/slo");
/**
* Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout
* Responses from the asserting party
* @param relyingPartyRegistrationResolver the strategy for resolving a
* {@link RelyingPartyRegistration}
* @param logoutResponseValidator authenticates the SAML 2.0 Logout Response
* @param logoutSuccessHandler the action to perform now that logout has succeeded
*/
public Saml2LogoutResponseFilter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver,
Saml2LogoutResponseValidator logoutResponseValidator, LogoutSuccessHandler logoutSuccessHandler) {
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
this.logoutResponseValidator = logoutResponseValidator;
this.logoutSuccessHandler = logoutSuccessHandler;
}
/**
* {@inheritDoc}
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (!this.logoutRequestMatcher.matches(request)) {
chain.doFilter(request, response);
return;
}
if (request.getParameter("SAMLResponse") == null) {
chain.doFilter(request, response);
return;
}
Saml2LogoutRequest logoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
if (logoutRequest == null) {
this.logger.trace("Did not process logout response since could not find associated LogoutRequest");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Failed to find associated LogoutRequest");
return;
}
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
logoutRequest.getRelyingPartyRegistrationId());
if (registration == null) {
this.logger
.trace("Did not process logout request since failed to find associated RelyingPartyRegistration");
Saml2Error error = new Saml2Error(Saml2ErrorCodes.RELYING_PARTY_REGISTRATION_NOT_FOUND,
"Failed to find associated RelyingPartyRegistration");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, error.toString());
return;
}
if (!isCorrectBinding(request, registration)) {
this.logger.trace("Did not process logout request since used incorrect binding");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String serialized = request.getParameter("SAMLResponse");
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(serialized).relayState(request.getParameter("RelayState"))
.binding(registration.getSingleLogoutServiceBinding())
.location(registration.getSingleLogoutServiceResponseLocation())
.parameters((params) -> params.put("SigAlg", request.getParameter("SigAlg")))
.parameters((params) -> params.put("Signature", request.getParameter("Signature"))).build();
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(logoutResponse,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.logoutResponseValidator.validate(parameters);
if (result.hasErrors()) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, result.getErrors().iterator().next().toString());
this.logger.debug(LogMessage.format("Failed to validate LogoutResponse: %s", result.getErrors()));
return;
}
this.logoutSuccessHandler.onLogoutSuccess(request, response, null);
}
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
this.logoutRequestMatcher = logoutRequestMatcher;
}
/**
* Use this {@link Saml2LogoutRequestRepository} for retrieving the SAML 2.0 Logout
* Request associated with the request's {@code RelayState}
* @param logoutRequestRepository the {@link Saml2LogoutRequestRepository} to use
*/
public void setLogoutRequestRepository(Saml2LogoutRequestRepository logoutRequestRepository) {
Assert.notNull(logoutRequestRepository, "logoutRequestRepository cannot be null");
this.logoutRequestRepository = logoutRequestRepository;
}
private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) {
Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding();
if (requiredBinding == Saml2MessageBinding.POST) {
return "POST".equals(request.getMethod());
}
return "GET".equals(request.getMethod());
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
/**
* Creates a signed SAML 2.0 Logout Response based on information from the
* {@link HttpServletRequest} and current {@link Authentication}.
*
* The returned logout response is suitable for sending to the asserting party based on,
* for example, the location and binding specified in
* {@link RelyingPartyRegistration#getAssertingPartyDetails()}.
*
* @author Josh Cummings
* @since 5.6
* @see RelyingPartyRegistration
*/
public interface Saml2LogoutResponseResolver {
/**
* Prepare to create, sign, and serialize a SAML 2.0 Logout Response.
* @param request the HTTP request
* @param authentication the current user
* @return a signed and serialized SAML 2.0 Logout Response
*/
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication);
}

View File

@ -0,0 +1,171 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* A success handler for issuing a SAML 2.0 Logout Request to the the SAML 2.0 Asserting
* Party
*
* @author Josh Cummings
* @since 5.6
*/
public final class Saml2RelyingPartyInitiatedLogoutSuccessHandler implements LogoutSuccessHandler {
private final Log logger = LogFactory.getLog(getClass());
private final Saml2LogoutRequestResolver logoutRequestResolver;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
/**
* Constructs a {@link Saml2RelyingPartyInitiatedLogoutSuccessHandler} using the
* provided parameters
* @param logoutRequestResolver the {@link Saml2LogoutRequestResolver} to use
*/
public Saml2RelyingPartyInitiatedLogoutSuccessHandler(Saml2LogoutRequestResolver logoutRequestResolver) {
this.logoutRequestResolver = logoutRequestResolver;
}
/**
* Produce and send a SAML 2.0 Logout Response based on the SAML 2.0 Logout Request
* received from the asserting party
* @param request the HTTP request
* @param response the HTTP response
* @param authentication the current principal details
* @throws IOException when failing to write to the response
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
Saml2LogoutRequest logoutRequest = this.logoutRequestResolver.resolve(request, authentication);
if (logoutRequest == null) {
this.logger.trace("Returning 401 since no logout request generated");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
if (logoutRequest.getBinding() == Saml2MessageBinding.REDIRECT) {
doRedirect(request, response, logoutRequest);
}
else {
doPost(response, logoutRequest);
}
}
/**
* Use this {@link Saml2LogoutRequestRepository} for saving the SAML 2.0 Logout
* Request
* @param logoutRequestRepository the {@link Saml2LogoutRequestRepository} to use
*/
public void setLogoutRequestRepository(Saml2LogoutRequestRepository logoutRequestRepository) {
Assert.notNull(logoutRequestRepository, "logoutRequestRepository cannot be null");
this.logoutRequestRepository = logoutRequestRepository;
}
private void doRedirect(HttpServletRequest request, HttpServletResponse response, Saml2LogoutRequest logoutRequest)
throws IOException {
String location = logoutRequest.getLocation();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
addParameter("SAMLRequest", logoutRequest::getParameter, uriBuilder);
addParameter("RelayState", logoutRequest::getParameter, uriBuilder);
addParameter("SigAlg", logoutRequest::getParameter, uriBuilder);
addParameter("Signature", logoutRequest::getParameter, uriBuilder);
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void addParameter(String name, Function<String, String> parameters, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(parameters.apply(name))) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1));
}
}
private void doPost(HttpServletResponse response, Saml2LogoutRequest logoutRequest) throws IOException {
String location = logoutRequest.getLocation();
String saml = logoutRequest.getSamlRequest();
String relayState = logoutRequest.getRelayState();
String html = createSamlPostRequestFormData(location, saml, relayState);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
}
private String createSamlPostRequestFormData(String location, String saml, String relayState) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n").append(" <head>\n");
html.append(" <meta charset=\"utf-8\" />\n");
html.append(" </head>\n");
html.append(" <body onload=\"document.forms[0].submit()\">\n");
html.append(" <noscript>\n");
html.append(" <p>\n");
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
html.append(" you must press the Continue button once to proceed.\n");
html.append(" </p>\n");
html.append(" </noscript>\n");
html.append(" \n");
html.append(" <form action=\"");
html.append(location);
html.append("\" method=\"post\">\n");
html.append(" <div>\n");
html.append(" <input type=\"hidden\" name=\"SAMLRequest\" value=\"");
html.append(HtmlUtils.htmlEscape(saml));
html.append("\"/>\n");
if (StringUtils.hasText(relayState)) {
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
html.append(HtmlUtils.htmlEscape(relayState));
html.append("\"/>\n");
}
html.append(" </div>\n");
html.append(" <noscript>\n");
html.append(" <div>\n");
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
html.append(" </div>\n");
html.append(" </noscript>\n");
html.append(" </form>\n");
html.append(" \n");
html.append(" </body>\n");
html.append("</html>");
return html.toString();
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;
import org.springframework.security.saml2.Saml2Exception;
/**
* Utility methods for working with serialized SAML messages.
*
* For internal use only.
*
* @author Josh Cummings
*/
final class Saml2Utils {
private Saml2Utils() {
}
static String samlEncode(byte[] b) {
return Base64.getEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
return Base64.getDecoder().decode(s);
}
static byte[] samlDeflate(String s) {
try {
ByteArrayOutputStream b = new ByteArrayOutputStream();
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true));
deflater.write(s.getBytes(StandardCharsets.UTF_8));
deflater.finish();
return b.toByteArray();
}
catch (IOException ex) {
throw new Saml2Exception("Unable to deflate string", ex);
}
}
static String samlInflate(byte[] b) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
iout.write(b);
iout.finish();
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
catch (IOException ex) {
throw new Saml2Exception("Unable to inflate string", ex);
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.time.Clock;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
* OpenSAML 3
*
* @author Josh Cummings
* @since 5.6
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
* {@code OpenSaml4LogoutRequestResolver}
*/
public final class OpenSaml3LogoutRequestResolver implements Saml2LogoutRequestResolver {
private final OpenSamlLogoutRequestResolver logoutRequestResolver;
private Consumer<LogoutRequestParameters> parametersConsumer = (parameters) -> {
};
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml3LogoutRequestResolver}
*/
public OpenSaml3LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutRequestResolver = new OpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
return this.logoutRequestResolver.resolve(request, authentication, (registration, logoutRequest) -> {
logoutRequest.setIssueInstant(new DateTime(this.clock.millis()));
this.parametersConsumer
.accept(new LogoutRequestParameters(request, registration, authentication, logoutRequest));
});
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
* @param parametersConsumer a consumer that accepts an
* {@link LogoutRequestParameters}
*/
public void setParametersConsumer(Consumer<LogoutRequestParameters> parametersConsumer) {
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
this.parametersConsumer = parametersConsumer;
}
/**
* Use this {@link Clock} for generating the issued {@link DateTime}
* @param clock the {@link Clock} to use
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.clock = clock;
}
public static final class LogoutRequestParameters {
private final HttpServletRequest request;
private final RelyingPartyRegistration registration;
private final Authentication authentication;
private final LogoutRequest logoutRequest;
public LogoutRequestParameters(HttpServletRequest request, RelyingPartyRegistration registration,
Authentication authentication, LogoutRequest logoutRequest) {
this.request = request;
this.registration = registration;
this.authentication = authentication;
this.logoutRequest = logoutRequest;
}
public HttpServletRequest getRequest() {
return this.request;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
public Authentication getAuthentication() {
return this.authentication;
}
public LogoutRequest getLogoutRequest() {
return this.logoutRequest;
}
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.time.Clock;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutResponseResolver} for resolving SAML 2.0 Logout Responses with
* OpenSAML 3
*
* @author Josh Cummings
* @since 5.6
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
* {@code OpenSaml4LogoutResponseResolver}
*/
public final class OpenSaml3LogoutResponseResolver implements Saml2LogoutResponseResolver {
private final OpenSamlLogoutResponseResolver logoutResponseResolver;
private Consumer<LogoutResponseParameters> parametersConsumer = (parameters) -> {
};
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml3LogoutResponseResolver}
*/
public OpenSaml3LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
return this.logoutResponseResolver.resolve(request, authentication, (registration, logoutResponse) -> {
logoutResponse.setIssueInstant(new DateTime(this.clock.millis()));
this.parametersConsumer
.accept(new LogoutResponseParameters(request, registration, authentication, logoutResponse));
});
}
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.clock = clock;
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutResponse}
* @param parametersConsumer a consumer that accepts an
* {@link LogoutResponseParameters}
*/
public void setParametersConsumer(Consumer<LogoutResponseParameters> parametersConsumer) {
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
this.parametersConsumer = parametersConsumer;
}
public static final class LogoutResponseParameters {
private final HttpServletRequest request;
private final RelyingPartyRegistration registration;
private final Authentication authentication;
private final LogoutResponse logoutResponse;
public LogoutResponseParameters(HttpServletRequest request, RelyingPartyRegistration registration,
Authentication authentication, LogoutResponse logoutResponse) {
this.request = request;
this.registration = registration;
this.authentication = authentication;
this.logoutResponse = logoutResponse;
}
public HttpServletRequest getRequest() {
return this.request;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
public Authentication getAuthentication() {
return this.authentication;
}
public LogoutResponse getLogoutResponse() {
return this.logoutResponse;
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenSaml3LogoutRequestResolver}
*/
public class OpenSaml3LogoutRequestResolverTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
@Test
public void resolveWhenCustomParametersConsumerThenUses() {
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
this.relyingPartyRegistrationResolver);
logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
HttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication);
assertThat(logoutRequest.getId()).isEqualTo("myid");
}
@Test
public void setParametersConsumerWhenNullThenIllegalArgument() {
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
this.relyingPartyRegistrationResolver);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver.LogoutResponseParameters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OpenSaml3LogoutResponseResolver}
*/
public class OpenSaml3LogoutResponseResolverTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
@Test
public void resolveWhenCustomParametersConsumerThenUses() {
OpenSaml3LogoutResponseResolver logoutResponseResolver = new OpenSaml3LogoutResponseResolver(
this.relyingPartyRegistrationResolver);
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
logoutResponseResolver.setParametersConsumer(parametersConsumer);
MockHttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setParameter("SAMLRequest",
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication);
assertThat(logoutResponse).isNotNull();
verify(parametersConsumer).accept(any());
}
@Test
public void setParametersConsumerWhenNullThenIllegalArgument() {
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
this.relyingPartyRegistrationResolver);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.time.Clock;
import java.time.Instant;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
* OpenSAML 4
*
* @author Josh Cummings
* @since 5.6
*/
public final class OpenSaml4LogoutRequestResolver implements Saml2LogoutRequestResolver {
private final OpenSamlLogoutRequestResolver logoutRequestResolver;
private Consumer<LogoutRequestParameters> parametersConsumer = (parameters) -> {
};
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml4LogoutRequestResolver}
*/
public OpenSaml4LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutRequestResolver = new OpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
return this.logoutRequestResolver.resolve(request, authentication, (registration, logoutRequest) -> {
logoutRequest.setIssueInstant(Instant.now(this.clock));
this.parametersConsumer
.accept(new LogoutRequestParameters(request, registration, authentication, logoutRequest));
});
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
* @param parametersConsumer a consumer that accepts an
* {@link LogoutRequestParameters}
*/
public void setParametersConsumer(Consumer<LogoutRequestParameters> parametersConsumer) {
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
this.parametersConsumer = parametersConsumer;
}
/**
* Use this {@link Clock} for determining the issued {@link Instant}
* @param clock the {@link Clock} to use
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.clock = clock;
}
public static final class LogoutRequestParameters {
private final HttpServletRequest request;
private final RelyingPartyRegistration registration;
private final Authentication authentication;
private final LogoutRequest logoutRequest;
public LogoutRequestParameters(HttpServletRequest request, RelyingPartyRegistration registration,
Authentication authentication, LogoutRequest logoutRequest) {
this.request = request;
this.registration = registration;
this.authentication = authentication;
this.logoutRequest = logoutRequest;
}
public HttpServletRequest getRequest() {
return this.request;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
public Authentication getAuthentication() {
return this.authentication;
}
public LogoutRequest getLogoutRequest() {
return this.logoutRequest;
}
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.time.Clock;
import java.time.Instant;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutResponseResolver} for resolving SAML 2.0 Logout Responses with
* OpenSAML 4
*
* @author Josh Cummings
* @since 5.6
*/
public final class OpenSaml4LogoutResponseResolver implements Saml2LogoutResponseResolver {
private final OpenSamlLogoutResponseResolver logoutResponseResolver;
private Consumer<LogoutResponseParameters> parametersConsumer = (parameters) -> {
};
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml4LogoutResponseResolver}
*/
public OpenSaml4LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
return this.logoutResponseResolver.resolve(request, authentication, (registration, logoutResponse) -> {
logoutResponse.setIssueInstant(Instant.now(this.clock));
this.parametersConsumer
.accept(new LogoutResponseParameters(request, registration, authentication, logoutResponse));
});
}
/**
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutResponse}
* @param parametersConsumer a consumer that accepts an
* {@link LogoutResponseParameters}
*/
public void setParametersConsumer(Consumer<LogoutResponseParameters> parametersConsumer) {
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
this.parametersConsumer = parametersConsumer;
}
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.clock = clock;
}
public static final class LogoutResponseParameters {
private final HttpServletRequest request;
private final RelyingPartyRegistration registration;
private final Authentication authentication;
private final LogoutResponse logoutResponse;
public LogoutResponseParameters(HttpServletRequest request, RelyingPartyRegistration registration,
Authentication authentication, LogoutResponse logoutResponse) {
this.request = request;
this.registration = registration;
this.authentication = authentication;
this.logoutResponse = logoutResponse;
}
public HttpServletRequest getRequest() {
return this.request;
}
public RelyingPartyRegistration getRelyingPartyRegistration() {
return this.registration;
}
public Authentication getAuthentication() {
return this.authentication;
}
public LogoutResponse getLogoutResponse() {
return this.logoutResponse;
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenSaml4LogoutRequestResolver}
*/
public class OpenSaml4LogoutRequestResolverTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
@Test
public void resolveWhenCustomParametersConsumerThenUses() {
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
this.relyingPartyRegistrationResolver);
logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
HttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication);
assertThat(logoutRequest.getId()).isEqualTo("myid");
}
@Test
public void setParametersConsumerWhenNullThenIllegalArgument() {
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
this.relyingPartyRegistrationResolver);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver.LogoutResponseParameters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OpenSaml4LogoutResponseResolver}
*/
public class OpenSaml4LogoutResponseResolverTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
@Test
public void resolveWhenCustomParametersConsumerThenUses() {
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(
this.relyingPartyRegistrationResolver);
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
logoutResponseResolver.setParametersConsumer(parametersConsumer);
MockHttpServletRequest request = new MockHttpServletRequest();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setParameter("SAMLRequest",
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication);
assertThat(logoutResponse).isNotNull();
verify(parametersConsumer).accept(any());
}
@Test
public void setParametersConsumerWhenNullThenIllegalArgument() {
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
this.relyingPartyRegistrationResolver);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
}
}

View File

@ -54,6 +54,8 @@ import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.EncryptedAttribute;
import org.opensaml.saml.saml2.core.EncryptedID;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.Status;
@ -63,6 +65,10 @@ import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
import org.opensaml.saml.saml2.core.impl.AttributeStatementBuilder;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutResponseBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
import org.opensaml.saml.saml2.core.impl.StatusBuilder;
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
import org.opensaml.saml.saml2.encryption.Encrypter;
@ -83,6 +89,7 @@ import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
public final class TestOpenSamlObjects {
@ -93,7 +100,7 @@ public final class TestOpenSamlObjects {
private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias";
private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
public static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
@ -221,7 +228,7 @@ public final class TestOpenSamlObjects {
return signable;
}
static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
public static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
return signed(signable, credential, entityId, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
}
@ -342,6 +349,41 @@ public final class TestOpenSamlObjects {
return status;
}
public static LogoutRequest assertingPartyLogoutRequest(RelyingPartyRegistration registration) {
LogoutRequestBuilder logoutRequestBuilder = new LogoutRequestBuilder();
LogoutRequest logoutRequest = logoutRequestBuilder.buildObject();
logoutRequest.setID("id");
NameIDBuilder nameIdBuilder = new NameIDBuilder();
NameID nameId = nameIdBuilder.buildObject();
nameId.setValue("user");
logoutRequest.setNameID(nameId);
IssuerBuilder issuerBuilder = new IssuerBuilder();
Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(registration.getAssertingPartyDetails().getEntityId());
logoutRequest.setIssuer(issuer);
logoutRequest.setDestination(registration.getSingleLogoutServiceLocation());
return logoutRequest;
}
public static LogoutResponse assertingPartyLogoutResponse(RelyingPartyRegistration registration) {
LogoutResponseBuilder logoutResponseBuilder = new LogoutResponseBuilder();
LogoutResponse logoutResponse = logoutResponseBuilder.buildObject();
logoutResponse.setID("id");
StatusBuilder statusBuilder = new StatusBuilder();
StatusCodeBuilder statusCodeBuilder = new StatusCodeBuilder();
StatusCode code = statusCodeBuilder.buildObject();
code.setValue(StatusCode.SUCCESS);
Status status = statusBuilder.buildObject();
status.setStatusCode(code);
logoutResponse.setStatus(status);
IssuerBuilder issuerBuilder = new IssuerBuilder();
Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(registration.getAssertingPartyDetails().getEntityId());
logoutResponse.setIssuer(issuer);
logoutResponse.setDestination(registration.getSingleLogoutServiceResponseLocation());
return logoutResponse;
}
static <T extends XMLObject> T build(QName qName) {
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
}

View File

@ -0,0 +1,173 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OpenSamlLogoutRequestValidator}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutRequestValidatorTests {
private final OpenSamlLogoutRequestValidator manager = new OpenSamlLogoutRequestValidator();
@Test
public void handleWhenPostBindingThenValidates() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = registration()
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
.build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
Saml2LogoutRequest request = redirect(logoutRequest, registration, OpenSamlSigningUtils.sign(registration));
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isFalse();
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getIssuer().setValue("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedUserThenInvalidRequestError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getNameID().setValue("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_REQUEST);
}
@Test
public void handleWhenMissingUserThenSubjectNotFoundError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setNameID(null);
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.SUBJECT_NOT_FOUND);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setDestination("wrong");
sign(logoutRequest, registration);
Saml2LogoutRequest request = post(logoutRequest, registration);
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
registration, authentication(registration));
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
}
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyDetails((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
}
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
}
private Authentication authentication(RelyingPartyRegistration registration) {
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
return new Saml2Authentication(principal, "response", new ArrayList<>());
}
private Saml2LogoutRequest post(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
return Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8))).build();
}
private Saml2LogoutRequest redirect(LogoutRequest logoutRequest, RelyingPartyRegistration registration,
QueryParametersPartial partial) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest)));
Map<String, String> parameters = partial.param("SAMLRequest", serialized).parameters();
return Saml2LogoutRequest.withRelyingPartyRegistration(registration).samlRequest(serialized)
.parameters((params) -> params.putAll(parameters)).build();
}
private void sign(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
TestOpenSamlObjects.signed(logoutRequest, registration.getSigningX509Credentials().iterator().next(),
registration.getAssertingPartyDetails().getEntityId());
}
private String serialize(XMLObject object) {
return OpenSamlSigningUtils.serialize(object);
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OpenSamlLogoutResponseValidator}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutResponseValidatorTests {
private final OpenSamlLogoutResponseValidator manager = new OpenSamlLogoutResponseValidator();
@Test
public void handleWhenAuthenticatedThenHandles() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = signing(verifying(registration()))
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
.build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
Saml2LogoutResponse response = redirect(logoutResponse, registration, OpenSamlSigningUtils.sign(registration));
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
this.manager.validate(parameters);
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getIssuer().setValue("wrong");
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.setDestination("wrong");
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
}
@Test
public void handleWhenStatusNotSuccessThenInvalidResponseError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getStatus().getStatusCode().setValue(StatusCode.UNKNOWN_PRINCIPAL);
sign(logoutResponse, registration);
Saml2LogoutResponse response = post(logoutResponse, registration);
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE);
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
}
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
return builder.assertingPartyDetails((party) -> party
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
}
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
}
private Saml2LogoutResponse post(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
return Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse(Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8)))
.build();
}
private Saml2LogoutResponse redirect(LogoutResponse logoutResponse, RelyingPartyRegistration registration,
QueryParametersPartial partial) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse)));
Map<String, String> parameters = partial.param("SAMLResponse", serialized).parameters();
return Saml2LogoutResponse.withRelyingPartyRegistration(registration).samlResponse(serialized)
.parameters((params) -> params.putAll(parameters)).build();
}
private void sign(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
TestOpenSamlObjects.signed(logoutResponse, registration.getSigningX509Credentials().iterator().next(),
registration.getAssertingPartyDetails().getEntityId());
}
private String serialize(XMLObject object) {
return OpenSamlSigningUtils.serialize(object);
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.BasicCredential;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.w3c.dom.Element;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* Utility methods for signing SAML components with OpenSAML
*
* For internal use only.
*
* @author Josh Cummings
*/
final class OpenSamlSigningUtils {
static String serialize(XMLObject object) {
try {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
Element element = marshaller.marshall(object);
return SerializeSupport.nodeToString(element);
}
catch (MarshallingException ex) {
throw new Saml2Exception(ex);
}
}
static <O extends SignableXMLObject> O sign(O object, RelyingPartyRegistration relyingPartyRegistration) {
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
try {
SignatureSupport.signObject(object, parameters);
return object;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
static QueryParametersPartial sign(RelyingPartyRegistration registration) {
return new QueryParametersPartial(registration);
}
private static SignatureSigningParameters resolveSigningParameters(
RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
CriteriaSet criteria = new CriteriaSet();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(algorithms);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private static List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = new ArrayList<>();
for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
X509Certificate certificate = x509Credential.getCertificate();
PrivateKey privateKey = x509Credential.getPrivateKey();
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
credential.setEntityId(relyingPartyRegistration.getEntityId());
credential.setUsageType(UsageType.SIGNING);
credentials.add(credential);
}
return credentials;
}
static class QueryParametersPartial {
final RelyingPartyRegistration registration;
final Map<String, String> components = new LinkedHashMap<>();
QueryParametersPartial(RelyingPartyRegistration registration) {
this.registration = registration;
}
QueryParametersPartial param(String key, String value) {
this.components.put(key, value);
return this;
}
Map<String, String> parameters() {
SignatureSigningParameters parameters = resolveSigningParameters(this.registration);
Credential credential = parameters.getSigningCredential();
String algorithmUri = parameters.getSignatureAlgorithm();
this.components.put("SigAlg", algorithmUri);
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : this.components.entrySet()) {
builder.queryParam(component.getKey(),
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
String queryString = builder.build(true).toString().substring(1);
try {
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
queryString.getBytes(StandardCharsets.UTF_8));
String b64Signature = Saml2Utils.samlEncode(rawSignature);
this.components.put("Signature", b64Signature);
}
catch (SecurityException ex) {
throw new Saml2Exception(ex);
}
return this.components;
}
}
private OpenSamlSigningUtils() {
}
}

View File

@ -41,7 +41,8 @@ public class OpenSamlMetadataResolverTests {
.contains("<md:KeyDescriptor use=\"encryption\">")
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
@Test
@ -56,7 +57,8 @@ public class OpenSamlMetadataResolverTests {
.contains("WantAssertionsSigned=\"true\"").doesNotContain("<md:KeyDescriptor use=\"signing\">")
.doesNotContain("<md:KeyDescriptor use=\"encryption\">")
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"")
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
}
}

View File

@ -37,17 +37,23 @@ public final class TestRelyingPartyRegistrations {
String apEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
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)
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
.credentials((c) -> c.add(signingCredential))
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
.credentials((c) -> c.add(verificationCertificate));
}
public static RelyingPartyRegistration.Builder noCredentials() {
return RelyingPartyRegistration.withRegistrationId("registration-id").entityId("rp-entity-id")
.assertionConsumerServiceLocation("https://rp.example.org/acs").assertingPartyDetails((party) -> party
.entityId("ap-entity-id").singleSignOnServiceLocation("https://ap.example.org/sso"));
.singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request")
.singleLogoutServiceResponseLocation("https://rp.example.org/logout/saml2/response")
.assertionConsumerServiceLocation("https://rp.example.org/acs")
.assertingPartyDetails((party) -> party.entityId("ap-entity-id")
.singleSignOnServiceLocation("https://ap.example.org/sso")
.singleLogoutServiceLocation("https://ap.example.org/logout/saml2/request")
.singleLogoutServiceResponseLocation("https://ap.example.org/logout/saml2/response"));
}
public static RelyingPartyRegistration.Builder full() {

View File

@ -52,6 +52,9 @@ public class DefaultRelyingPartyRegistrationResolverTests {
.isEqualTo("http://localhost/saml2/service-provider-metadata/" + this.registration.getRegistrationId());
assertThat(registration.getAssertionConsumerServiceLocation())
.isEqualTo("http://localhost/login/saml2/sso/" + this.registration.getRegistrationId());
assertThat(registration.getSingleLogoutServiceLocation()).isEqualTo("http://localhost/logout/saml2/slo");
assertThat(registration.getSingleLogoutServiceResponseLocation())
.isEqualTo("http://localhost/logout/saml2/slo");
}
@Test

View File

@ -0,0 +1,229 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link HttpSessionLogoutRequestRepository}
*/
public class HttpSessionLogoutRequestRepositoryTests {
HttpSessionLogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
@Test
public void loadLogoutRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository.loadLogoutRequest(null));
}
@Test
public void loadLogoutRequestWhenNotSavedThenReturnNull() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("RelayState", "state-1234");
Saml2LogoutRequest logoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(logoutRequest).isNull();
}
@Test
public void loadLogoutRequestWhenSavedThenReturnLogoutRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
request.addParameter("RelayState", logoutRequest.getRelayState());
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest);
}
@Test
public void loadLogoutRequestWhenMultipleSavedThenReplacesLogoutRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutRequest one = createLogoutRequest().relayState("state-1122").build();
this.logoutRequestRepository.saveLogoutRequest(one, request, response);
Saml2LogoutRequest two = createLogoutRequest().relayState("state-3344").build();
this.logoutRequestRepository.saveLogoutRequest(two, request, response);
request.setParameter("RelayState", one.getRelayState());
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isNull();
request.setParameter("RelayState", two.getRelayState());
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isEqualTo(two);
}
@Test
public void loadLogoutRequestWhenSavedAndStateParameterNullThenReturnNull() {
MockHttpServletRequest request = new MockHttpServletRequest();
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, new MockHttpServletResponse());
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isNull();
}
@Test
public void saveLogoutRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository
.saveLogoutRequest(logoutRequest, null, new MockHttpServletResponse()));
}
@Test
public void saveLogoutRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() {
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository
.saveLogoutRequest(logoutRequest, new MockHttpServletRequest(), null));
}
@Test
public void saveLogoutRequestWhenStateNullThenThrowIllegalArgumentException() {
Saml2LogoutRequest logoutRequest = createLogoutRequest().relayState(null).build();
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository
.saveLogoutRequest(logoutRequest, new MockHttpServletRequest(), new MockHttpServletResponse()));
}
@Test
public void saveLogoutRequestWhenNotNullThenSaved() {
MockHttpServletRequest request = new MockHttpServletRequest();
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, new MockHttpServletResponse());
request.addParameter("RelayState", logoutRequest.getRelayState());
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest);
}
@Test
public void saveLogoutRequestWhenNoExistingSessionAndDistributedSessionThenSaved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setSession(new MockDistributedHttpSession());
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, new MockHttpServletResponse());
request.addParameter("RelayState", logoutRequest.getRelayState());
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest);
}
@Test
public void saveLogoutRequestWhenExistingSessionAndDistributedSessionThenSaved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setSession(new MockDistributedHttpSession());
Saml2LogoutRequest logoutRequest1 = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest1, request, new MockHttpServletResponse());
Saml2LogoutRequest logoutRequest2 = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest2, request, new MockHttpServletResponse());
request.addParameter("RelayState", logoutRequest2.getRelayState());
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest2);
}
@Test
public void saveLogoutRequestWhenNullThenRemoved() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
request.addParameter("RelayState", logoutRequest.getRelayState());
this.logoutRequestRepository.saveLogoutRequest(null, request, response);
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest).isNull();
}
@Test
public void removeLogoutRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(
() -> this.logoutRequestRepository.removeLogoutRequest(null, new MockHttpServletResponse()));
}
@Test
public void removeLogoutRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.logoutRequestRepository.removeLogoutRequest(new MockHttpServletRequest(), null));
}
@Test
public void removeLogoutRequestWhenSavedThenRemoved() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
request.addParameter("RelayState", logoutRequest.getRelayState());
Saml2LogoutRequest removedLogoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(removedLogoutRequest).isNotNull();
assertThat(loadedLogoutRequest).isNull();
}
// gh-5263
@Test
public void removeLogoutRequestWhenSavedThenRemovedFromSession() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
request.addParameter("RelayState", logoutRequest.getRelayState());
Saml2LogoutRequest removedLogoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
String sessionAttributeName = HttpSessionLogoutRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST";
assertThat(removedLogoutRequest).isNotNull();
assertThat(request.getSession().getAttribute(sessionAttributeName)).isNull();
}
@Test
public void removeLogoutRequestWhenNotSavedThenNotRemoved() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("RelayState", "state-1234");
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutRequest removedLogoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
assertThat(removedLogoutRequest).isNull();
}
private Saml2LogoutRequest.Builder createLogoutRequest() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
return Saml2LogoutRequest.withRelyingPartyRegistration(registration).samlRequest("request").id("id")
.parameters((params) -> params.put("RelayState", "state-1234"));
}
static class MockDistributedHttpSession extends MockHttpSession {
@Override
public Object getAttribute(String name) {
return wrap(super.getAttribute(name));
}
@Override
public void setAttribute(String name, Object value) {
super.setAttribute(name, wrap(value));
}
private Object wrap(Object object) {
if (object instanceof Map) {
object = new HashMap<>((Map<Object, Object>) object);
}
return object;
}
}
}

View File

@ -0,0 +1,115 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenSamlLogoutRequestResolver}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutRequestResolverTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
OpenSamlLogoutRequestResolver logoutRequestResolver = new OpenSamlLogoutRequestResolver(
this.relyingPartyRegistrationResolver);
@Test
public void resolveRedirectWhenAuthenticatedThenIncludesName() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
Saml2Authentication authentication = authentication(registration);
HttpServletRequest request = new MockHttpServletRequest();
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutRequest saml2LogoutRequest = this.logoutRequestResolver.resolve(request, authentication);
assertThat(saml2LogoutRequest.getParameter("SigAlg")).isNotNull();
assertThat(saml2LogoutRequest.getParameter("Signature")).isNotNull();
assertThat(saml2LogoutRequest.getParameter("RelayState")).isNotNull();
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding);
assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName());
}
@Test
public void resolvePostWhenAuthenticatedThenIncludesName() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
Saml2Authentication authentication = authentication(registration);
HttpServletRequest request = new MockHttpServletRequest();
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutRequest saml2LogoutRequest = this.logoutRequestResolver.resolve(request, authentication);
assertThat(saml2LogoutRequest.getParameter("SigAlg")).isNull();
assertThat(saml2LogoutRequest.getParameter("Signature")).isNull();
assertThat(saml2LogoutRequest.getParameter("RelayState")).isNotNull();
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding);
assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName());
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
return new Saml2Authentication(principal, "response", new ArrayList<>());
}
private LogoutRequest getLogoutRequest(String samlRequest, Saml2MessageBinding binding) {
if (binding == Saml2MessageBinding.REDIRECT) {
samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest));
}
else {
samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8);
}
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool()
.parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8)));
Element element = document.getDocumentElement();
return (LogoutRequest) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element)
.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import org.junit.jupiter.api.Test;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenSamlLogoutResponseResolver}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutResponseResolverTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
OpenSamlLogoutResponseResolver logoutResponseResolver = new OpenSamlLogoutResponseResolver(
this.relyingPartyRegistrationResolver);
@Test
public void resolveRedirectWhenAuthenticatedThenSuccess() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
MockHttpServletRequest request = new MockHttpServletRequest();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setParameter("SAMLRequest",
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
request.setParameter("RelayState", "abcd");
Authentication authentication = authentication(registration);
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutResponse saml2LogoutResponse = this.logoutResponseResolver.resolve(request, authentication);
assertThat(saml2LogoutResponse.getParameter("SigAlg")).isNotNull();
assertThat(saml2LogoutResponse.getParameter("Signature")).isNotNull();
assertThat(saml2LogoutResponse.getParameter("RelayState")).isSameAs("abcd");
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
LogoutResponse logoutResponse = getLogoutResponse(saml2LogoutResponse.getSamlResponse(), binding);
assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS);
}
@Test
public void resolvePostWhenAuthenticatedThenSuccess() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
MockHttpServletRequest request = new MockHttpServletRequest();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setParameter("SAMLRequest",
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
request.setParameter("RelayState", "abcd");
Authentication authentication = authentication(registration);
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
Saml2LogoutResponse saml2LogoutResponse = this.logoutResponseResolver.resolve(request, authentication);
assertThat(saml2LogoutResponse.getParameter("SigAlg")).isNull();
assertThat(saml2LogoutResponse.getParameter("Signature")).isNull();
assertThat(saml2LogoutResponse.getParameter("RelayState")).isSameAs("abcd");
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
LogoutResponse logoutResponse = getLogoutResponse(saml2LogoutResponse.getSamlResponse(), binding);
assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS);
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
return new Saml2Authentication(principal, "response", new ArrayList<>());
}
private LogoutResponse getLogoutResponse(String saml2Response, Saml2MessageBinding binding) {
if (binding == Saml2MessageBinding.REDIRECT) {
saml2Response = Saml2Utils.samlInflate(Saml2Utils.samlDecode(saml2Response));
}
else {
saml2Response = new String(Saml2Utils.samlDecode(saml2Response), StandardCharsets.UTF_8);
}
try {
Document document = XMLObjectProviderRegistrySupport.getParserPool()
.parse(new ByteArrayInputStream(saml2Response.getBytes(StandardCharsets.UTF_8)));
Element element = document.getDocumentElement();
return (LogoutResponse) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element)
.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link Saml2LogoutRequestFilter}
*/
public class Saml2LogoutRequestFilterTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
Saml2LogoutRequestValidator logoutRequestValidator = mock(Saml2LogoutRequestValidator.class);
LogoutHandler logoutHandler = mock(LogoutHandler.class);
Saml2LogoutResponseResolver logoutResponseResolver = mock(Saml2LogoutResponseResolver.class);
Saml2LogoutRequestFilter logoutRequestProcessingFilter = new Saml2LogoutRequestFilter(
this.relyingPartyRegistrationResolver, this.logoutRequestValidator, this.logoutResponseResolver,
this.logoutHandler);
@AfterEach
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void doFilterWhenSamlRequestThenRedirects() throws Exception {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter("SAMLRequest", "request");
MockHttpServletResponse response = new MockHttpServletResponse();
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
given(this.logoutRequestValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse("response").build();
given(this.logoutResponseResolver.resolve(any(), any())).willReturn(logoutResponse);
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verify(this.logoutRequestValidator).validate(any());
verify(this.logoutHandler).logout(any(), any(), any());
verify(this.logoutResponseResolver).resolve(any(), any());
String content = response.getHeader("Location");
assertThat(content).contains("SAMLResponse");
assertThat(content)
.startsWith(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
}
@Test
public void doFilterWhenSamlRequestThenPosts() throws Exception {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter("SAMLRequest", "request");
MockHttpServletResponse response = new MockHttpServletResponse();
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
given(this.logoutRequestValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse("response").build();
given(this.logoutResponseResolver.resolve(any(), any())).willReturn(logoutResponse);
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verify(this.logoutRequestValidator).validate(any());
verify(this.logoutHandler).logout(any(), any(), any());
verify(this.logoutResponseResolver).resolve(any(), any());
String content = response.getContentAsString();
assertThat(content).contains("SAMLResponse");
assertThat(content).contains(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
}
@Test
public void doFilterWhenRequestMismatchesThenNoLogout() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout");
request.setServletPath("/logout");
request.setParameter("SAMLResponse", "response");
MockHttpServletResponse response = new MockHttpServletResponse();
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.logoutRequestValidator, this.logoutHandler);
}
@Test
public void doFilterWhenNoSamlRequestOrResponseThenNoLogout() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
MockHttpServletResponse response = new MockHttpServletResponse();
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.logoutRequestValidator, this.logoutHandler);
}
@Test
public void doFilterWhenValidationFailsThen401() throws Exception {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter("SAMLRequest", "request");
MockHttpServletResponse response = new MockHttpServletResponse();
given(this.relyingPartyRegistrationResolver.resolve(request, null)).willReturn(registration);
given(this.logoutRequestValidator.validate(any()))
.willReturn(Saml2LogoutValidatorResult.withErrors(new Saml2Error("error", "description")).build());
this.logoutRequestProcessingFilter.doFilter(request, response, new MockFilterChain());
assertThat(response.getStatus()).isEqualTo(401);
verifyNoInteractions(this.logoutHandler);
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link Saml2LogoutResponseFilter}
*/
public class Saml2LogoutResponseFilterTests {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class);
Saml2LogoutResponseValidator logoutResponseValidator = mock(Saml2LogoutResponseValidator.class);
LogoutSuccessHandler logoutSuccessHandler = mock(LogoutSuccessHandler.class);
Saml2LogoutResponseFilter logoutResponseProcessingFilter = new Saml2LogoutResponseFilter(
this.relyingPartyRegistrationResolver, this.logoutResponseValidator, this.logoutSuccessHandler);
@BeforeEach
public void setUp() {
this.logoutResponseProcessingFilter.setLogoutRequestRepository(this.logoutRequestRepository);
}
@AfterEach
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void doFilterWhenSamlResponsePostThenLogout() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter("SAMLResponse", "response");
MockHttpServletResponse response = new MockHttpServletResponse();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
given(this.relyingPartyRegistrationResolver.resolve(request, "registration-id")).willReturn(registration);
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest("request").build();
given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest);
given(this.logoutResponseValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verify(this.logoutResponseValidator).validate(any());
verify(this.logoutSuccessHandler).onLogoutSuccess(any(), any(), any());
}
@Test
public void doFilterWhenSamlResponseRedirectThenLogout() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter("SAMLResponse", "response");
MockHttpServletResponse response = new MockHttpServletResponse();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT).build();
given(this.relyingPartyRegistrationResolver.resolve(request, "registration-id")).willReturn(registration);
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest("request").build();
given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest);
given(this.logoutResponseValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verify(this.logoutResponseValidator).validate(any());
verify(this.logoutSuccessHandler).onLogoutSuccess(any(), any(), any());
}
@Test
public void doFilterWhenRequestMismatchesThenNoLogout() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout");
request.setServletPath("/logout");
request.setParameter("SAMLRequest", "request");
MockHttpServletResponse response = new MockHttpServletResponse();
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.logoutResponseValidator, this.logoutSuccessHandler);
}
@Test
public void doFilterWhenNoSamlRequestOrResponseThenNoLogout() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
MockHttpServletResponse response = new MockHttpServletResponse();
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.logoutResponseValidator, this.logoutSuccessHandler);
}
@Test
public void doFilterWhenValidatorFailsThenStops() throws Exception {
Authentication authentication = new TestingAuthenticationToken("user", "password");
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
request.setServletPath("/logout/saml2/slo");
request.setParameter("SAMLResponse", "response");
MockHttpServletResponse response = new MockHttpServletResponse();
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
given(this.relyingPartyRegistrationResolver.resolve(request, "registration-id")).willReturn(registration);
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest("request").build();
given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest);
given(this.logoutResponseValidator.validate(any()))
.willReturn(Saml2LogoutValidatorResult.withErrors(new Saml2Error("error", "description")).build());
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
verify(this.logoutResponseValidator).validate(any());
verifyNoInteractions(this.logoutSuccessHandler);
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.util.ArrayList;
import java.util.HashMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link Saml2RelyingPartyInitiatedLogoutSuccessHandler}
*
* @author Josh Cummings
*/
public class Saml2RelyingPartyInitiatedLogoutSuccessHandlerTests {
Saml2LogoutRequestResolver logoutRequestResolver = mock(Saml2LogoutRequestResolver.class);
Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class);
Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = new Saml2RelyingPartyInitiatedLogoutSuccessHandler(
this.logoutRequestResolver);
@BeforeEach
public void setUp() {
this.logoutRequestSuccessHandler.setLogoutRequestRepository(this.logoutRequestRepository);
}
@AfterEach
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void onLogoutSuccessWhenRedirectThenRedirectsToAssertingParty() throws Exception {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
Authentication authentication = authentication(registration);
SecurityContextHolder.getContext().setAuthentication(authentication);
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest("request").build();
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/saml2/logout");
request.setServletPath("/saml2/logout");
MockHttpServletResponse response = new MockHttpServletResponse();
given(this.logoutRequestResolver.resolve(any(), any())).willReturn(logoutRequest);
this.logoutRequestSuccessHandler.onLogoutSuccess(request, response, authentication);
String content = response.getHeader("Location");
assertThat(content).contains("SAMLRequest");
assertThat(content).startsWith(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
}
@Test
public void onLogoutSuccessWhenPostThenPostsToAssertingParty() throws Exception {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
Authentication authentication = authentication(registration);
SecurityContextHolder.getContext().setAuthentication(authentication);
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest("request").build();
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/saml2/logout");
request.setServletPath("/saml2/logout");
MockHttpServletResponse response = new MockHttpServletResponse();
given(this.logoutRequestResolver.resolve(any(), any())).willReturn(logoutRequest);
this.logoutRequestSuccessHandler.onLogoutSuccess(request, response, authentication);
String content = response.getContentAsString();
assertThat(content).contains("SAMLRequest");
assertThat(content).contains(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
return new Saml2Authentication(principal, "response", new ArrayList<>());
}
}