Revert "Add Single Logout Support"

This reverts commit e807fae869.
This commit is contained in:
Josh Cummings 2021-04-12 14:44:04 -06:00
parent 404a6c5674
commit 37b40476e7
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
41 changed files with 35 additions and 4957 deletions

View File

@ -1053,279 +1053,20 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
[[servlet-saml2login-logout]]
=== Performing Single Logout
Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
Spring Security does not yet support single logout.
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:
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
[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:
[source,java]
----
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
http
// ...
.addFilterBefore(filter, CsrfFilter.class);
.logout(logout -> logout
.logoutSuccessHandler(myCustomSuccessHandler())
.logoutRequestMatcher(myRequestMatcher())
)
----
=== Customizing `<saml2:LogoutRequest>` Resolution
The success handler will send logout requests to the asserting party.
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]
----
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
}
}
----
=== Customizing `<saml2:LogoutResponse>` 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) {
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
}
}
----
The request matcher will detect logout requests from the asserting party.

View File

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

View File

@ -1,217 +0,0 @@
/*
* 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.5
*/
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} parameter
*
* 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
*/
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 Map<String, String> parameters = new HashMap<>();
private String id;
private Builder(RelyingPartyRegistration registration) {
this.registration = registration;
}
/**
* 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 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.registration.getAssertingPartyDetails().getSingleLogoutServiceLocation(),
this.registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(), this.parameters,
this.id, this.registration.getRegistrationId());
}
}
}

View File

@ -1,184 +0,0 @@
/*
* 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.5
*/
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
*/
public Map<String, String> getParameters() {
return this.parameters;
}
/**
* Create a {@link Saml2LogoutResponse.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 Saml2LogoutResponse.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 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

@ -32,7 +32,6 @@ 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;
@ -86,7 +85,6 @@ 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;
}
@ -125,14 +123,6 @@ 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,7 +34,6 @@ 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;
@ -106,10 +105,6 @@ 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())) {
@ -124,27 +119,10 @@ class OpenSamlAssertingPartyMetadataConverter {
builder.assertingPartyDetails(
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
.singleSignOnServiceBinding(binding));
break;
return builder;
}
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;
throw new Saml2Exception(
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
}
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {

View File

@ -81,12 +81,6 @@ 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;
@ -96,9 +90,7 @@ public final class RelyingPartyRegistration {
private final Collection<Saml2X509Credential> signingX509Credentials;
private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation,
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding,
ProviderDetails providerDetails,
Saml2MessageBinding assertionConsumerServiceBinding, ProviderDetails providerDetails,
Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials,
Collection<Saml2X509Credential> decryptionX509Credentials,
Collection<Saml2X509Credential> signingX509Credentials) {
@ -126,9 +118,6 @@ 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));
@ -188,51 +177,6 @@ public final class RelyingPartyRegistration {
return this.assertionConsumerServiceBinding;
}
/**
* Get the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Binding.
*
* <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.5
*/
public Saml2MessageBinding getSingleLogoutServiceBinding() {
return this.singleLogoutServiceBinding;
}
/**
* Get the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Location.
*
* <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.5
*/
public String getSingleLogoutServiceLocation() {
return this.singleLogoutServiceLocation;
}
/**
* Get the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Response Location.
*
* <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.5
*/
public String getSingleLogoutServiceResponseLocation() {
return this.singleLogoutServiceResponseLocation;
}
/**
* Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated
* with this relying party
@ -420,9 +364,6 @@ 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())
@ -435,13 +376,7 @@ public final class RelyingPartyRegistration {
.singleSignOnServiceLocation(
registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
.singleSignOnServiceBinding(
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
.singleLogoutServiceLocation(
registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
.singleLogoutServiceResponseLocation(
registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
.singleLogoutServiceBinding(
registration.getAssertingPartyDetails().getSingleLogoutServiceBinding()));
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()));
}
private static Saml2X509Credential fromDeprecated(
@ -510,17 +445,10 @@ 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, String singleLogoutServiceLocation,
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding) {
Saml2MessageBinding singleSignOnServiceBinding) {
Assert.hasText(entityId, "entityId cannot be null or empty");
Assert.notEmpty(signingAlgorithms, "signingAlgorithms cannot be empty");
Assert.notNull(verificationX509Credentials, "verificationX509Credentials cannot be null");
@ -544,9 +472,6 @@ public final class RelyingPartyRegistration {
this.encryptionX509Credentials = encryptionX509Credentials;
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
}
/**
@ -640,48 +565,6 @@ public final class RelyingPartyRegistration {
return this.singleSignOnServiceBinding;
}
/**
* Get the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Location.
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in
* the asserting party's &lt;IDPSSODescriptor&gt;.
* @return the SingleLogoutService Location
*/
public String getSingleLogoutServiceLocation() {
return this.singleLogoutServiceLocation;
}
/**
* Get the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* ResponseLocation.
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in
* the asserting party's &lt;IDPSSODescriptor&gt;.
* @return the SingleLogoutService Response Location
*/
public String getSingleLogoutServiceResponseLocation() {
return this.singleLogoutServiceResponseLocation;
}
/**
* Get the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Binding.
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in
* the asserting party's &lt;IDPSSODescriptor&gt;.
* @return the SingleLogoutService Binding
*/
public Saml2MessageBinding getSingleLogoutServiceBinding() {
return this.singleLogoutServiceBinding;
}
public static final class Builder {
private String entityId;
@ -698,12 +581,6 @@ 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://wiki.shibboleth.net/confluence/display/CONCEPT/EntityNaming">EntityID</a>.
@ -800,55 +677,6 @@ public final class RelyingPartyRegistration {
return this;
}
/**
* Set the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Location.
*
* <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.5
*/
public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
return this;
}
/**
* Set the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Response Location.
*
* <p>
* Equivalent to the value found in &lt;SingleLogoutService
* ResponseLocation="..."/&gt; in the asserting party's
* &lt;IDPSSODescriptor&gt;.
* @return the SingleLogoutService Response Location
* @since 5.5
*/
public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) {
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
return this;
}
/**
* Set the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Binding.
*
* <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.5
*/
public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
return this;
}
/**
* Creates an immutable ProviderDetails object representing the configuration
* for an Identity Provider, IDP
@ -861,9 +689,7 @@ public final class RelyingPartyRegistration {
return new AssertingPartyDetails(this.entityId, this.wantAuthnRequestsSigned, signingAlgorithms,
this.verificationX509Credentials, this.encryptionX509Credentials,
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding,
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
this.singleLogoutServiceBinding);
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding);
}
}
@ -1004,12 +830,6 @@ public final class RelyingPartyRegistration {
private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST;
private String singleLogoutServiceLocation;
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<>();
@ -1113,54 +933,6 @@ public final class RelyingPartyRegistration {
return this;
}
/**
* Set the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Binding.
*
* <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.5
*/
public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
return this;
}
/**
* Set the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Location.
*
* <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.5
*/
public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
return this;
}
/**
* Set the <a href=
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForIdP#MetadataForIdP-Logout">SingleLogoutService</a>
* Response Location.
*
* <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.5
*/
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
@ -1303,14 +1075,10 @@ 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.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials,
this.decryptionX509Credentials, this.signingX509Credentials);
this.providerDetails.build(), this.credentials, this.decryptionX509Credentials,
this.signingX509Credentials);
}
}

View File

@ -41,7 +41,8 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Josh Cummings
* @since 5.4
*/
public final class DefaultRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
public final class DefaultRelyingPartyRegistrationResolver
implements Converter<HttpServletRequest, RelyingPartyRegistration>, RelyingPartyRegistrationResolver {
private static final char PATH_DELIMITER = '/';
@ -55,6 +56,14 @@ public final class DefaultRelyingPartyRegistrationResolver implements RelyingPar
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
}
/**
* {@inheritDoc}
*/
@Override
public RelyingPartyRegistration convert(HttpServletRequest request) {
return resolve(request, null);
}
/**
* {@inheritDoc}
*/
@ -77,14 +86,9 @@ public final class DefaultRelyingPartyRegistrationResolver implements RelyingPar
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)
.singleLogoutServiceLocation(singleLogoutServiceLocation)
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation).build();
.build();
}
private Function<String, String> templateResolver(String applicationUri, RelyingPartyRegistration relyingParty) {
@ -92,9 +96,6 @@ public final class DefaultRelyingPartyRegistrationResolver implements RelyingPar
}
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

@ -1,112 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
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.5
* @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");
String stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
Map<String, Saml2LogoutRequest> logoutRequests = this.getLogoutRequests(request);
return logoutRequests.get(stateParameter);
}
/**
* {@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) {
removeLogoutRequest(request, response);
return;
}
String state = logoutRequest.getRelayState();
Assert.hasText(state, "logoutRequest.state cannot be empty");
Map<String, Saml2LogoutRequest> logoutRequests = this.getLogoutRequests(request);
logoutRequests.put(state, logoutRequest);
request.getSession().setAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME, logoutRequests);
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutRequest removeLogoutRequest(HttpServletRequest request, HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
String stateParameter = getStateParameter(request);
if (stateParameter == null) {
return null;
}
Map<String, Saml2LogoutRequest> logoutRequests = getLogoutRequests(request);
Saml2LogoutRequest originalRequest = logoutRequests.remove(stateParameter);
if (!logoutRequests.isEmpty()) {
request.getSession().setAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME, logoutRequests);
}
else {
request.getSession().removeAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME);
}
return originalRequest;
}
private String getStateParameter(HttpServletRequest request) {
return request.getParameter("RelayState");
}
private Map<String, Saml2LogoutRequest> getLogoutRequests(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Map<String, Saml2LogoutRequest> logoutRequests = (session != null)
? (Map<String, Saml2LogoutRequest>) session.getAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME) : null;
if (logoutRequests == null) {
return new HashMap<>();
}
return logoutRequests;
}
}

View File

@ -1,198 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.http.HttpMethod;
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.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlVerificationUtils.VerifierPartial;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
/**
* A {@link LogoutHandler} that handles SAML 2.0 Logout Requests received from a SAML 2.0
* Asserting Party.
*
* @author Josh Cummings
* @since 5.5
*/
public final class OpenSamlLogoutRequestHandler implements LogoutHandler {
static {
OpenSamlInitializationService.initialize();
}
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
private final ParserPool parserPool;
private final LogoutRequestUnmarshaller unmarshaller;
/**
* Constructs a {@link OpenSamlLogoutRequestHandler} from the provided parameters
* @param relyingPartyRegistrationResolver the
* {@link RelyingPartyRegistrationResolver} from which to derive the
* {@link RelyingPartyRegistration}
*/
public OpenSamlLogoutRequestHandler(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);
}
/**
* Processes 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.
*
* If any processing step fails, a {@link Saml2Exception} is thrown, stopping the
* logout process
* @param request the HTTP request
* @param response the HTTP response
* @param authentication the current principal details
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String serialized = request.getParameter("SAMLRequest");
Assert.notNull(serialized, "SAMLRequest cannot be null");
byte[] b = Saml2Utils.samlDecode(serialized);
serialized = inflateIfRequired(request, b);
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
getRegistrationId(authentication));
Assert.notNull(registration, "Failed to lookup RelyingPartyRegistration for request");
LogoutRequest logoutRequest = parse(serialized);
Saml2ResponseValidatorResult result = verifySignature(request, logoutRequest, registration);
result = result.concat(validateRequest(logoutRequest, registration, authentication));
if (result.hasErrors()) {
throw new Saml2Exception("Failed to validate LogoutRequest: " + result.getErrors().iterator().next());
}
request.setAttribute(LogoutRequest.class.getName(), logoutRequest);
}
private String getRegistrationId(Authentication authentication) {
if (authentication instanceof Saml2Authentication) {
return ((Saml2Authentication) authentication).getRelyingPartyRegistrationId();
}
return null;
}
private String inflateIfRequired(HttpServletRequest request, byte[] b) {
if (HttpMethod.GET.matches(request.getMethod())) {
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 Saml2ResponseValidatorResult verifySignature(HttpServletRequest request, LogoutRequest logoutRequest,
RelyingPartyRegistration registration) {
VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(logoutRequest, registration);
if (logoutRequest.isSigned()) {
return partial.post(logoutRequest.getSignature());
}
return partial.redirect(request, "SAMLRequest");
}
private Saml2ResponseValidatorResult validateRequest(LogoutRequest request, RelyingPartyRegistration registration,
Authentication authentication) {
Saml2ResponseValidatorResult result = Saml2ResponseValidatorResult.success();
return result.concat(validateIssuer(request, registration)).concat(validateDestination(request, registration))
.concat(validateName(request, authentication));
}
private Saml2ResponseValidatorResult validateIssuer(LogoutRequest request, RelyingPartyRegistration registration) {
if (request.getIssuer() == null) {
return Saml2ResponseValidatorResult
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
}
String issuer = request.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) {
return Saml2ResponseValidatorResult.failure(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
return Saml2ResponseValidatorResult.success();
}
private Saml2ResponseValidatorResult validateDestination(LogoutRequest request,
RelyingPartyRegistration registration) {
if (request.getDestination() == null) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutResponse"));
}
String destination = request.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceLocation())) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
return Saml2ResponseValidatorResult.success();
}
private Saml2ResponseValidatorResult validateName(LogoutRequest request, Authentication authentication) {
if (authentication == null) {
return Saml2ResponseValidatorResult.success();
}
NameID nameId = request.getNameID();
if (nameId == null) {
return Saml2ResponseValidatorResult.failure(
new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest"));
}
String name = nameId.getValue();
if (!name.equals(authentication.getName())) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST,
"Failed to match subject in LogoutRequest with currently logged in user"));
}
return Saml2ResponseValidatorResult.success();
}
}

View File

@ -1,243 +0,0 @@
/*
* 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.Map;
import java.util.UUID;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
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.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.web.RelyingPartyRegistrationResolver;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
* OpenSAML
*
* Note that there are {@link Saml2LogoutRequestResolver} implements that are targeted for
* OpenSAML 3 and OpenSAML 4 via {@code OpenSaml3LogoutRequestResolver} and
* {@code OpenSaml4LogoutRequestResolver}
*
* @author Josh Cummings
* @since 5.5
*/
public final class OpenSamlLogoutRequestResolver implements Saml2LogoutRequestResolver {
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
/**
* Construct a {@link OpenSamlLogoutRequestResolver} using the provided parameters
* @param relyingPartyRegistrationResolver the
* {@link RelyingPartyRegistrationResolver} for selecting the
* {@link RelyingPartyRegistration}
*/
public OpenSamlLogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
}
/**
* 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}.
*
* The {@link Authentication} must be of type {@link Saml2Authentication} in order to
* look up the {@link RelyingPartyRegistration} that holds the signing key.
* @param request the HTTP request
* @param authentication the current principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout Request
* that the resolver supplied
*/
@Override
public OpenSamlLogoutRequestBuilder resolveLogoutRequest(HttpServletRequest request,
Authentication authentication) {
Assert.notNull(authentication, "Failed to lookup logged-in user for formulating LogoutRequest");
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
getRegistrationId(authentication));
Assert.notNull(registration, "Failed to lookup RelyingPartyRegistration for formulating LogoutRequest");
return new OpenSamlLogoutRequestBuilder(registration)
.destination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
.issuer(registration.getEntityId()).name(authentication.getName());
}
private String getRegistrationId(Authentication authentication) {
if (authentication instanceof Saml2Authentication) {
return ((Saml2Authentication) authentication).getRelyingPartyRegistrationId();
}
return null;
}
/**
* A builder, useful for overriding any aspects of the SAML 2.0 Logout Request that
* the resolver supplied.
*
* The request returned from the {@link #logoutRequest()} method is signed and
* serialized. It will at minimum include an {@code ID} and a {@code RelayState},
* though note that callers should also provide an {@code IssueInstant}. For your
* convenience, {@link OpenSamlLogoutRequestResolver} also sets some default values.
*
* This builder is specifically handy for getting access to the underlying
* {@link LogoutRequest} to make changes before it gets signed and serialized
*
* @see OpenSamlLogoutRequestResolver#resolveLogoutRequest
*/
public static final class OpenSamlLogoutRequestBuilder
implements Saml2LogoutRequestBuilder<OpenSamlLogoutRequestBuilder> {
static {
OpenSamlInitializationService.initialize();
}
private final LogoutRequestMarshaller marshaller;
private final IssuerBuilder issuerBuilder;
private final NameIDBuilder nameIdBuilder;
private final RelyingPartyRegistration registration;
private final LogoutRequest logoutRequest;
private String relayState;
/**
* Construct a {@link OpenSamlLogoutRequestBuilder} using the provided parameters
* @param registration the {@link RelyingPartyRegistration} to use
*/
public OpenSamlLogoutRequestBuilder(RelyingPartyRegistration registration) {
Assert.notNull(registration, "registration cannot be null");
this.registration = registration;
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");
LogoutRequestBuilder logoutRequestBuilder = (LogoutRequestBuilder) registry.getBuilderFactory()
.getBuilder(LogoutRequest.DEFAULT_ELEMENT_NAME);
Assert.notNull(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");
this.logoutRequest = logoutRequestBuilder.buildObject();
}
/**
* {@inheritDoc}
*/
@Override
public OpenSamlLogoutRequestBuilder name(String name) {
NameID nameId = this.nameIdBuilder.buildObject();
nameId.setValue(name);
this.logoutRequest.setNameID(nameId);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public OpenSamlLogoutRequestBuilder relayState(String relayState) {
this.relayState = relayState;
return this;
}
/**
* Mutate the {@link LogoutRequest} using the provided {@link Consumer}
* @param request the Logout Request {@link Consumer} to use
* @return the {@link OpenSamlLogoutRequestBuilder} for further customizations
*/
public OpenSamlLogoutRequestBuilder logoutRequest(Consumer<LogoutRequest> request) {
request.accept(this.logoutRequest);
return this;
}
private OpenSamlLogoutRequestBuilder destination(String destination) {
this.logoutRequest.setDestination(destination);
return this;
}
private OpenSamlLogoutRequestBuilder issuer(String issuer) {
Issuer iss = this.issuerBuilder.buildObject();
iss.setValue(issuer);
this.logoutRequest.setIssuer(iss);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutRequest logoutRequest() {
if (this.logoutRequest.getID() == null) {
this.logoutRequest.setID("LR" + UUID.randomUUID());
}
if (this.relayState == null) {
this.relayState = UUID.randomUUID().toString();
}
Saml2LogoutRequest.Builder result = Saml2LogoutRequest.withRelyingPartyRegistration(this.registration)
.id(this.logoutRequest.getID());
if (this.registration.getAssertingPartyDetails()
.getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) {
String xml = serialize(OpenSamlSigningUtils.sign(this.logoutRequest, this.registration));
return result.samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build();
}
else {
String xml = serialize(this.logoutRequest);
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
result.samlRequest(deflatedAndEncoded);
Map<String, String> parameters = OpenSamlSigningUtils.sign(this.registration)
.param("SAMLRequest", deflatedAndEncoded).param("RelayState", this.relayState).parameters();
return result.parameters((params) -> params.putAll(parameters)).build();
}
}
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

@ -1,217 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.http.HttpMethod;
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.core.Saml2ResponseValidatorResult;
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.security.saml2.provider.service.web.authentication.logout.OpenSamlVerificationUtils.VerifierPartial;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
/**
* A {@link LogoutHandler} that handles SAML 2.0 Logout Responses received from a SAML 2.0
* Asserting Party.
*
* @author Josh Cummings
* @since 5.5
*/
public final class OpenSamlLogoutResponseHandler implements LogoutHandler {
static {
OpenSamlInitializationService.initialize();
}
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
private final ParserPool parserPool;
private final LogoutResponseUnmarshaller unmarshaller;
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
/**
* Constructs a {@link OpenSamlLogoutResponseHandler} from the provided parameters
* @param relyingPartyRegistrationResolver the
* {@link RelyingPartyRegistrationResolver} from which to derive the
* {@link RelyingPartyRegistration}
*/
public OpenSamlLogoutResponseHandler(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.parserPool = registry.getParserPool();
this.unmarshaller = (LogoutResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
.getUnmarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME);
}
/**
* Processes 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.
*
* If any processing step fails, a {@link Saml2Exception} is thrown, stopping the
* logout process
* @param request the HTTP request
* @param response the HTTP response
* @param authentication the current principal details
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String serialized = request.getParameter("SAMLResponse");
Assert.notNull(serialized, "SAMLResponse cannot be null");
byte[] b = Saml2Utils.samlDecode(serialized);
serialized = inflateIfRequired(request, b);
Saml2LogoutRequest logoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
if (logoutRequest == null) {
throw new Saml2Exception("Failed to find associated LogoutRequest");
}
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
logoutRequest.getRelyingPartyRegistrationId());
LogoutResponse logoutResponse = parse(serialized);
Saml2ResponseValidatorResult result = verifySignature(request, logoutResponse, registration)
.concat(validateRequest(logoutResponse, registration))
.concat(validateLogoutRequest(logoutResponse, logoutRequest.getId()));
if (result.hasErrors()) {
throw new Saml2Exception("Failed to validate LogoutResponse: " + result.getErrors().iterator().next());
}
}
/**
* Use this {@link Saml2LogoutRequestRepository} for looking up the associated 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 String inflateIfRequired(HttpServletRequest request, byte[] b) {
if (HttpMethod.GET.matches(request.getMethod())) {
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 Saml2ResponseValidatorResult verifySignature(HttpServletRequest request, LogoutResponse response,
RelyingPartyRegistration registration) {
VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(response, registration);
if (response.isSigned()) {
return partial.post(response.getSignature());
}
return partial.redirect(request, "SAMLResponse");
}
private Saml2ResponseValidatorResult validateRequest(LogoutResponse response,
RelyingPartyRegistration registration) {
Saml2ResponseValidatorResult result = Saml2ResponseValidatorResult.success();
return result.concat(validateIssuer(response, registration)).concat(validateDestination(response, registration))
.concat(validateStatus(response));
}
private Saml2ResponseValidatorResult validateIssuer(LogoutResponse response,
RelyingPartyRegistration registration) {
if (response.getIssuer() == null) {
return Saml2ResponseValidatorResult
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
}
String issuer = response.getIssuer().getValue();
if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) {
return Saml2ResponseValidatorResult.failure(
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
}
return Saml2ResponseValidatorResult.success();
}
private Saml2ResponseValidatorResult validateDestination(LogoutResponse response,
RelyingPartyRegistration registration) {
if (response.getDestination() == null) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to find destination in LogoutResponse"));
}
String destination = response.getDestination();
if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
"Failed to match destination to configured destination"));
}
return Saml2ResponseValidatorResult.success();
}
private Saml2ResponseValidatorResult validateStatus(LogoutResponse response) {
if (response.getStatus() == null) {
return Saml2ResponseValidatorResult.success();
}
if (response.getStatus().getStatusCode() == null) {
return Saml2ResponseValidatorResult.success();
}
if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) {
return Saml2ResponseValidatorResult.success();
}
if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) {
return Saml2ResponseValidatorResult.success();
}
return Saml2ResponseValidatorResult
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed"));
}
private Saml2ResponseValidatorResult validateLogoutRequest(LogoutResponse response, String id) {
if (response.getInResponseTo() == null) {
return Saml2ResponseValidatorResult.success();
}
if (response.getInResponseTo().equals(id)) {
return Saml2ResponseValidatorResult.success();
}
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE,
"LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest"));
}
}

View File

@ -1,268 +0,0 @@
/*
* 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.Consumer;
import javax.servlet.http.HttpServletRequest;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
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.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.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.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.Saml2Authentication;
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;
/**
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Responses with
* OpenSAML
*
* Note that there are {@link Saml2LogoutRequestResolver} implements that are targeted for
* OpenSAML 3 and OpenSAML 4 via {@code OpenSaml3LogoutResponseResolver} and
* {@code OpenSaml4LogoutResponseResolver}
*
* @author Josh Cummings
* @since 5.5
*/
public final class OpenSamlLogoutResponseResolver implements Saml2LogoutResponseResolver {
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
/**
* Construct a {@link OpenSamlLogoutResponseResolver} using the provided parameters
* @param relyingPartyRegistrationResolver the
* {@link RelyingPartyRegistrationResolver} for selecting the
* {@link RelyingPartyRegistration}
*/
public OpenSamlLogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
}
/**
* 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}.
*
* The {@link Authentication} must be of type {@link Saml2Authentication} in order to
* look up the {@link RelyingPartyRegistration} that holds the signing key.
* @param request the HTTP request
* @param authentication the current principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout Request
* that the resolver supplied
*/
@Override
public OpenSamlLogoutResponseBuilder resolveLogoutResponse(HttpServletRequest request,
Authentication authentication) {
LogoutRequest logoutRequest = (LogoutRequest) request.getAttribute(LogoutRequest.class.getName());
if (logoutRequest == null) {
throw new Saml2Exception("Failed to find associated LogoutRequest");
}
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
getRegistrationId(authentication));
Assert.notNull(registration, "Failed to lookup RelyingPartyRegistration for request");
return new OpenSamlLogoutResponseBuilder(registration)
.destination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
.issuer(registration.getEntityId()).status(StatusCode.SUCCESS)
.relayState(request.getParameter("RelayState")).inResponseTo(logoutRequest.getID());
}
private String getRegistrationId(Authentication authentication) {
if (authentication instanceof Saml2Authentication) {
return ((Saml2Authentication) authentication).getRelyingPartyRegistrationId();
}
return null;
}
/**
* A builder, useful for overriding any aspects of the SAML 2.0 Logout Response that
* the resolver supplied.
*
* The request returned from the {@link #logoutResponse()} method is signed and
* serialized. It will at minimum include an {@code ID}, though note that callers
* should include an {@code InResponseTo} and {@code IssueInstant}. For your
* convenience, {@link OpenSamlLogoutResponseResolver} also sets some default values.
*
* This builder is specifically handy for getting access to the underlying
* {@link LogoutResponse} to make changes before it gets signed and serialized
*
* @see OpenSamlLogoutResponseResolver#resolveLogoutResponse
*/
public static final class OpenSamlLogoutResponseBuilder
implements Saml2LogoutResponseBuilder<OpenSamlLogoutResponseBuilder> {
static {
OpenSamlInitializationService.initialize();
}
private final LogoutResponseMarshaller marshaller;
private final LogoutResponseBuilder logoutResponseBuilder;
private final IssuerBuilder issuerBuilder;
private final StatusBuilder statusBuilder;
private final StatusCodeBuilder statusCodeBuilder;
private final RelyingPartyRegistration registration;
private final LogoutResponse logoutResponse;
private String relayState;
/**
* Construct a {@link OpenSamlLogoutResponseBuilder} using the provided parameters
* @param registration the {@link RelyingPartyRegistration} to use
*/
public OpenSamlLogoutResponseBuilder(RelyingPartyRegistration registration) {
Assert.notNull(registration, "registration cannot be null");
this.registration = registration;
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
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");
this.logoutResponse = this.logoutResponseBuilder.buildObject();
}
/**
* {@inheritDoc}
*/
@Override
public OpenSamlLogoutResponseBuilder inResponseTo(String inResponseTo) {
this.logoutResponse.setInResponseTo(inResponseTo);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public OpenSamlLogoutResponseBuilder status(String status) {
StatusCode code = this.statusCodeBuilder.buildObject();
code.setValue(status);
Status s = this.statusBuilder.buildObject();
s.setStatusCode(code);
this.logoutResponse.setStatus(s);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public OpenSamlLogoutResponseBuilder relayState(String relayState) {
this.relayState = relayState;
return this;
}
/**
* Mutate the {@link LogoutResponse} using the provided {@link Consumer}
* @param response the Logout Response {@link Consumer} to use
* @return the {@link OpenSamlLogoutResponseBuilder} for further customizations
*/
public OpenSamlLogoutResponseBuilder logoutResponse(Consumer<LogoutResponse> response) {
response.accept(this.logoutResponse);
return this;
}
private OpenSamlLogoutResponseBuilder destination(String destination) {
this.logoutResponse.setDestination(destination);
return this;
}
private OpenSamlLogoutResponseBuilder issuer(String issuer) {
Issuer iss = this.issuerBuilder.buildObject();
iss.setValue(issuer);
this.logoutResponse.setIssuer(iss);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LogoutResponse logoutResponse() {
Saml2LogoutResponse.Builder result = Saml2LogoutResponse.withRelyingPartyRegistration(this.registration);
if (this.logoutResponse.getID() == null) {
this.logoutResponse.setID("LR" + UUID.randomUUID());
}
if (this.registration.getAssertingPartyDetails()
.getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) {
String xml = serialize(OpenSamlSigningUtils.sign(this.logoutResponse, this.registration));
return result.samlResponse(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build();
}
else {
String xml = serialize(this.logoutResponse);
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
result.samlResponse(deflatedAndEncoded);
QueryParametersPartial partial = OpenSamlSigningUtils.sign(this.registration).param("SAMLResponse",
deflatedAndEncoded);
if (this.relayState != null) {
partial.param("RelayState", this.relayState);
}
return result.parameters((params) -> params.putAll(partial.parameters())).build();
}
}
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

@ -1,173 +0,0 @@
/*
* 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

@ -1,218 +0,0 @@
/*
* 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.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
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.Saml2ResponseValidatorResult;
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);
}
Saml2ResponseValidatorResult redirect(HttpServletRequest request, String objectParameterName) {
RedirectSignature signature = new RedirectSignature(request, objectParameterName);
if (signature.getAlgorithm() == null) {
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Missing signature algorithm for object [" + this.id + "]"));
}
if (!signature.hasSignature()) {
return Saml2ResponseValidatorResult.failure(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 Saml2ResponseValidatorResult.failure(errors);
}
Saml2ResponseValidatorResult 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 Saml2ResponseValidatorResult.failure(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 HttpServletRequest request;
private final String objectParameterName;
RedirectSignature(HttpServletRequest request, String objectParameterName) {
this.request = request;
this.objectParameterName = objectParameterName;
}
String getAlgorithm() {
return this.request.getParameter("SigAlg");
}
byte[] getContent() {
if (this.request.getParameter("RelayState") != null) {
return String.format("%s=%s&RelayState=%s&SigAlg=%s", this.objectParameterName,
UriUtils.encode(this.request.getParameter(this.objectParameterName),
StandardCharsets.ISO_8859_1),
UriUtils.encode(this.request.getParameter("RelayState"), StandardCharsets.ISO_8859_1),
UriUtils.encode(getAlgorithm(), StandardCharsets.ISO_8859_1))
.getBytes(StandardCharsets.UTF_8);
}
else {
return String
.format("%s=%s&SigAlg=%s", this.objectParameterName,
UriUtils.encode(this.request.getParameter(this.objectParameterName),
StandardCharsets.ISO_8859_1),
UriUtils.encode(getAlgorithm(), StandardCharsets.ISO_8859_1))
.getBytes(StandardCharsets.UTF_8);
}
}
byte[] getSignature() {
return Saml2Utils.samlDecode(this.request.getParameter("Signature"));
}
boolean hasSignature() {
return this.request.getParameter("Signature") != null;
}
}
}
private OpenSamlVerificationUtils() {
}
}

View File

@ -1,92 +0,0 @@
/*
* 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.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
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 logout requests in the form of a &lt;saml2:LogoutRequest&gt; sent
* from the asserting party.
*
* @author Josh Cummings
* @since 5.5
*/
public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
private static final String DEFAULT_LOGOUT_ENDPOINT = "/logout/saml2/slo";
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher(DEFAULT_LOGOUT_ENDPOINT);
private final LogoutHandler logoutHandler;
private final LogoutSuccessHandler logoutSuccessHandler;
/**
* Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout
* Requests from the asserting party
* @param logoutSuccessHandler the success handler to be run after the logout request
* passes validation and other logout operations succeed. This success handler will
* typically be one that issues a SAML 2.0 Logout Response to the asserting party,
* like {@link Saml2LogoutResponseSuccessHandler}
* @param logoutHandler the handler for handling the logout request, may be a
* {@link org.springframework.security.web.authentication.logout.CompositeLogoutHandler}
* that handles other logout concerns
*/
public Saml2LogoutRequestFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler logoutHandler) {
this.logoutSuccessHandler = logoutSuccessHandler;
this.logoutHandler = logoutHandler;
}
@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();
this.logoutHandler.logout(request, response, authentication);
this.logoutSuccessHandler.onLogoutSuccess(request, response, authentication);
}
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
this.logoutRequestMatcher = logoutRequestMatcher;
}
}

View File

@ -1,68 +0,0 @@
/*
* 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 Saml2LogoutRequestSuccessHandler} for persisting the Logout Request
* before it initiates the SAML 2.0 SLO flow. As well, used by
* {@link OpenSamlLogoutResponseHandler} for resolving the Logout Request associated with
* that Logout Response.
*
* @author Josh Cummings
* @since 5.5
* @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

@ -1,82 +0,0 @@
/*
* 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.5
* @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 principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout Request
* that the resolver supplied
*/
Saml2LogoutRequestBuilder<?> resolveLogoutRequest(HttpServletRequest request, Authentication authentication);
/**
* A partial application, useful for overriding any aspects of the SAML 2.0 Logout
* Request that the resolver supplied.
*
* The request returned from the {@link #logoutRequest()} method is signed and
* serialized
*/
interface Saml2LogoutRequestBuilder<P extends Saml2LogoutRequestBuilder<P>> {
/**
* Use the given name in the SAML 2.0 Logout Request
* @param name the name to use
* @return the {@link Saml2LogoutRequestBuilder} for further customizations
*/
P name(String name);
/**
* Use this relay state when sending the logout response
* @param relayState the relay state to use
* @return the {@link Saml2LogoutRequestBuilder} for further customizations
*/
P relayState(String relayState);
/**
* Return a signed and serialized SAML 2.0 Logout Request and associated signed
* request parameters
* @return a signed and serialized SAML 2.0 Logout Request
*/
Saml2LogoutRequest logoutRequest();
}
}

View File

@ -1,159 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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 Response in response to the SAML 2.0
* Logout Request that the SAML 2.0 Asserting Party sent
*
* @author Josh Cummings
* @since 5.5
*/
public final class Saml2LogoutRequestSuccessHandler implements LogoutSuccessHandler {
private final Saml2LogoutRequestResolver logoutRequestResolver;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
/**
* Constructs a {@link Saml2LogoutRequestSuccessHandler} using the provided parameters
* @param logoutRequestResolver the {@link Saml2LogoutRequestResolver} to use
*/
public Saml2LogoutRequestSuccessHandler(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 {
Saml2LogoutRequestResolver.Saml2LogoutRequestBuilder<?> builder = this.logoutRequestResolver
.resolveLogoutRequest(request, authentication);
if (builder == null) {
return;
}
Saml2LogoutRequest logoutRequest = builder.logoutRequest();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
if (logoutRequest.getBinding() == Saml2MessageBinding.REDIRECT) {
doRedirect(request, response, logoutRequest);
}
else {
doPost(response, logoutRequest);
}
}
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, uriBuilder);
addParameter("RelayState", logoutRequest, uriBuilder);
addParameter("SigAlg", logoutRequest, uriBuilder);
addParameter("Signature", logoutRequest, uriBuilder);
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void addParameter(String name, Saml2LogoutRequest logoutRequest, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(logoutRequest.getParameter(name))) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(logoutRequest.getParameter(name), StandardCharsets.ISO_8859_1));
}
}
private void doPost(HttpServletResponse response, Saml2LogoutRequest logoutRequest) throws IOException {
String html = createSamlPostRequestFormData(logoutRequest);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
}
private String createSamlPostRequestFormData(Saml2LogoutRequest logoutRequest) {
String location = logoutRequest.getLocation();
String samlRequest = logoutRequest.getSamlRequest();
String relayState = logoutRequest.getRelayState();
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(samlRequest));
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

@ -1,117 +0,0 @@
/*
* 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.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
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 LogoutHandler} or
* {@link LogoutSuccessHandler} that rely on the user being logged in.
*
* @author Josh Cummings
* @since 5.5
*/
public final class Saml2LogoutResponseFilter extends OncePerRequestFilter {
private static final String DEFAULT_LOGOUT_ENDPOINT = "/logout/saml2/slo";
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher(DEFAULT_LOGOUT_ENDPOINT);
private final LogoutHandler logoutHandler;
private LogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
/**
* Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout
* Responses from the asserting party
* @param logoutHandler the handlers for handling the logout response
*/
public Saml2LogoutResponseFilter(LogoutHandler logoutHandler) {
this.logoutHandler = new CompositeLogoutHandler(logoutHandler);
}
/**
* {@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;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
this.logoutHandler.logout(request, response, authentication);
this.logoutSuccessHandler.onLogoutSuccess(request, response, authentication);
}
/**
* Use this {@link RequestMatcher} for requests
*
* This is handy when your asserting party needs it to be a specific endpoint instead
* of the default.
* @param logoutRequestMatcher the {@link RequestMatcher} to use
*/
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
this.logoutRequestMatcher = logoutRequestMatcher;
}
/**
* Use this {@link LogoutSuccessHandler} when complete
*
* Note that when a &lt;saml2:LogoutResponse&gt; is received, the end user is already
* logged out. Any {@link LogoutSuccessHandler} used here should not rely on the
* {@link Authentication}. {@link SimpleUrlLogoutSuccessHandler} is an example of
* this.
* @param logoutSuccessHandler the {@link LogoutSuccessHandler} to use
* @see SimpleUrlLogoutSuccessHandler
*/
public void setLogoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
this.logoutSuccessHandler = logoutSuccessHandler;
}
}

View File

@ -1,90 +0,0 @@
/*
* 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.5
* @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 principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout
* Response that the resolver supplied
*/
Saml2LogoutResponseBuilder<?> resolveLogoutResponse(HttpServletRequest request, Authentication authentication);
/**
* A partial application, useful for overriding any aspects of the SAML 2.0 Logout
* Response that the resolver supplied.
*
* The response returned from the {@link #logoutResponse()} method is signed and
* serialized
*/
interface Saml2LogoutResponseBuilder<P extends Saml2LogoutResponseBuilder<P>> {
/**
* Use this value as the {@code InResponseTo} identifier for the associated SAML
* 2.0 Logout Request
* @param name the logout request identifier
* @return the {@link Saml2LogoutResponseBuilder} for further customizations
*/
P inResponseTo(String name);
/**
* Use this status code in the logout response.
*
* The default is {@code SUCCESS}.
* @param status the status code to use
* @return the {@link Saml2LogoutResponseBuilder} for further customizations
*/
P status(String status);
/**
* Use this relay state when sending the logout response
* @param relayState the relay state to use
* @return the {@link Saml2LogoutResponseBuilder} for further customizations
*/
P relayState(String relayState);
/**
* Return a signed and serialized SAML 2.0 Logout Response and associated signed
* request parameters
* @return a signed and serialized SAML 2.0 Logout Response
*/
Saml2LogoutResponse logoutResponse();
}
}

View File

@ -1,148 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
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 Response in response to the SAML 2.0
* Logout Request that the SAML 2.0 Asserting Party sent
*
* @author Josh Cummings
* @since 5.5
*/
public final class Saml2LogoutResponseSuccessHandler implements LogoutSuccessHandler {
private final Saml2LogoutResponseResolver logoutResponseResolver;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* Constructs a {@link Saml2LogoutResponseSuccessHandler} using the provided
* parameters
* @param logoutResponseResolver the {@link Saml2LogoutResponseResolver} to use
*/
public Saml2LogoutResponseSuccessHandler(Saml2LogoutResponseResolver logoutResponseResolver) {
this.logoutResponseResolver = logoutResponseResolver;
}
/**
* 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 {
Saml2LogoutResponse logoutResponse = this.logoutResponseResolver.resolveLogoutResponse(request, authentication)
.logoutResponse();
if (logoutResponse.getBinding() == Saml2MessageBinding.REDIRECT) {
doRedirect(request, response, logoutResponse);
}
else {
doPost(response, logoutResponse);
}
}
private void doRedirect(HttpServletRequest request, HttpServletResponse response,
Saml2LogoutResponse logoutResponse) throws IOException {
String location = logoutResponse.getResponseLocation();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
addParameter("SAMLResponse", logoutResponse, uriBuilder);
addParameter("RelayState", logoutResponse, uriBuilder);
addParameter("SigAlg", logoutResponse, uriBuilder);
addParameter("Signature", logoutResponse, uriBuilder);
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void addParameter(String name, Saml2LogoutResponse logoutResponse, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(logoutResponse.getParameter(name))) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(logoutResponse.getParameter(name), StandardCharsets.ISO_8859_1));
}
}
private void doPost(HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException {
String html = createSamlPostRequestFormData(logoutResponse);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
}
private String createSamlPostRequestFormData(Saml2LogoutResponse logoutResponse) {
String location = logoutResponse.getResponseLocation();
String samlRequest = logoutResponse.getSamlResponse();
String relayState = logoutResponse.getRelayState();
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(samlRequest));
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

@ -1,35 +0,0 @@
/*
* 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;
/**
* Attribute names for coordinating between SAML 2.0 Logout components.
*
* For internal use only.
*
* @author Josh Cummings
*/
final class Saml2RequestAttributeNames {
static final String LOGOUT_REQUEST_ID = Saml2RequestAttributeNames.class.getName() + "_LOGOUT_REQUEST_ID";
private Saml2RequestAttributeNames() {
}
}

View File

@ -1,79 +0,0 @@
/*
* 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.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;
import org.apache.commons.codec.binary.Base64;
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 static Base64 BASE64 = new Base64(0, new byte[] { '\n' });
private Saml2Utils() {
}
static String samlEncode(byte[] b) {
return BASE64.encodeAsString(b);
}
static byte[] samlDecode(String s) {
return BASE64.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

@ -54,8 +54,6 @@ 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;
@ -65,10 +63,6 @@ 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;
@ -89,7 +83,6 @@ 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 {
@ -100,7 +93,7 @@ public final class TestOpenSamlObjects {
private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias";
public static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
private 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";
@ -228,7 +221,7 @@ public final class TestOpenSamlObjects {
return signable;
}
public static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
return signed(signable, credential, entityId, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
}
@ -349,41 +342,6 @@ 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

@ -41,8 +41,7 @@ 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("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
}
@Test
@ -57,8 +56,7 @@ 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("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
}
}

View File

@ -37,23 +37,17 @@ 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)
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
.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")
.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"));
.assertionConsumerServiceLocation("https://rp.example.org/acs").assertingPartyDetails((party) -> party
.entityId("ap-entity-id").singleSignOnServiceLocation("https://ap.example.org/sso"));
}
public static RelyingPartyRegistration.Builder full() {

View File

@ -52,9 +52,6 @@ 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

@ -1,242 +0,0 @@
/*
* 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.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 {
private 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);
}
// gh-5110
@Test
public void loadLogoutRequestWhenMultipleSavedThenReturnMatchingLogoutRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
String state1 = "state-1122";
Saml2LogoutRequest logoutRequest1 = createLogoutRequest().relayState(state1).build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest1, request, response);
String state2 = "state-3344";
Saml2LogoutRequest logoutRequest2 = createLogoutRequest().relayState(state2).build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest2, request, response);
String state3 = "state-5566";
Saml2LogoutRequest logoutRequest3 = createLogoutRequest().relayState(state3).build();
this.logoutRequestRepository.saveLogoutRequest(logoutRequest3, request, response);
request.addParameter("RelayState", state1);
Saml2LogoutRequest loadedLogoutRequest1 = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest1).isEqualTo(logoutRequest1);
request.removeParameter("RelayState");
request.addParameter("RelayState", state2);
Saml2LogoutRequest loadedLogoutRequest2 = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest2).isEqualTo(logoutRequest2);
request.removeParameter("RelayState");
request.addParameter("RelayState", state3);
Saml2LogoutRequest loadedLogoutRequest3 = this.logoutRequestRepository.loadLogoutRequest(request);
assertThat(loadedLogoutRequest3).isEqualTo(logoutRequest3);
}
@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

@ -1,182 +0,0 @@
/*
* 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.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.Saml2Exception;
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.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.OpenSamlSigningUtils.QueryParametersPartial;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link OpenSamlLogoutRequestHandler}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutRequestHandlerTests {
private final RelyingPartyRegistrationResolver resolver = mock(RelyingPartyRegistrationResolver.class);
private final OpenSamlLogoutRequestHandler handler = new OpenSamlLogoutRequestHandler(this.resolver);
@Test
public void handleWhenAuthenticatedThenSavesRequestId() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
sign(logoutRequest, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutRequest);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
this.handler.logout(request, null, authentication);
String id = ((LogoutRequest) request.getAttribute(LogoutRequest.class.getName())).getID();
assertThat(id).isEqualTo(logoutRequest.getID());
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = redirect(logoutRequest, OpenSamlSigningUtils.sign(registration));
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
this.handler.logout(request, null, authentication);
String id = ((LogoutRequest) request.getAttribute(LogoutRequest.class.getName())).getID();
assertThat(id).isEqualTo(logoutRequest.getID());
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getIssuer().setValue("wrong");
sign(logoutRequest, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutRequest);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.handler.logout(request, null, authentication))
.withMessageContaining(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedUserThenInvalidRequestError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.getNameID().setValue("wrong");
sign(logoutRequest, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutRequest);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.handler.logout(request, null, authentication))
.withMessageContaining(Saml2ErrorCodes.INVALID_REQUEST);
}
@Test
public void handleWhenMissingUserThenSubjectNotFoundError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setNameID(null);
sign(logoutRequest, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutRequest);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.handler.logout(request, null, authentication))
.withMessageContaining(Saml2ErrorCodes.SUBJECT_NOT_FOUND);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
logoutRequest.setDestination("wrong");
sign(logoutRequest, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutRequest);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.handler.logout(request, null, authentication))
.withMessageContaining(Saml2ErrorCodes.INVALID_DESTINATION);
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()));
}
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) {
return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response",
new ArrayList<>(), registration.getRegistrationId());
}
private MockHttpServletRequest post(LogoutRequest logoutRequest) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setParameter("SAMLRequest",
Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8)));
return request;
}
private MockHttpServletRequest redirect(LogoutRequest logoutRequest, QueryParametersPartial partial) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest)));
Map<String, String> parameters = partial.param("SAMLRequest", serialized).parameters();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameters(parameters);
request.setMethod("GET");
return request;
}
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

@ -1,112 +0,0 @@
/*
* 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.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.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link OpenSamlLogoutRequestResolver}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutRequestResolverTests {
private final RelyingPartyRegistrationResolver resolver = mock(RelyingPartyRegistrationResolver.class);
private final OpenSamlLogoutRequestResolver logoutResolver = new OpenSamlLogoutRequestResolver(this.resolver);
@Test
public void resolveRedirectWhenAuthenticatedThenIncludesName() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
Saml2Authentication authentication = authentication(registration);
HttpServletRequest request = new MockHttpServletRequest();
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
Saml2LogoutRequest saml2LogoutRequest = this.logoutResolver.resolveLogoutRequest(request, authentication)
.logoutRequest();
assertThat(saml2LogoutRequest.getParameter("SigAlg")).isNotNull();
assertThat(saml2LogoutRequest.getParameter("Signature")).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.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
Saml2LogoutRequest saml2LogoutRequest = this.logoutResolver.resolveLogoutRequest(request, authentication)
.logoutRequest();
assertThat(saml2LogoutRequest.getParameter("SigAlg")).isNull();
assertThat(saml2LogoutRequest.getParameter("Signature")).isNull();
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding);
assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName());
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response",
new ArrayList<>(), registration.getRegistrationId());
}
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

@ -1,189 +0,0 @@
/*
* 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.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.Saml2Exception;
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.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 org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link OpenSamlLogoutResponseHandler}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutResponseHandlerTests {
private final RelyingPartyRegistrationResolver resolver = mock(RelyingPartyRegistrationResolver.class);
private final Saml2LogoutRequestRepository repository = mock(Saml2LogoutRequestRepository.class);
private final OpenSamlLogoutResponseHandler handler = new OpenSamlLogoutResponseHandler(this.resolver);
@Before
public void setUp() {
this.handler.setLogoutRequestRepository(this.repository);
}
@Test
public void handleWhenAuthenticatedThenHandles() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
given(this.repository.removeLogoutRequest(any(), any())).willReturn(logoutRequest);
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
sign(logoutResponse, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutResponse);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
this.handler.logout(request, null, authentication);
}
@Test
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
RelyingPartyRegistration registration = signing(verifying(registration())).build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
given(this.repository.removeLogoutRequest(any(), any())).willReturn(logoutRequest);
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = redirect(logoutResponse, OpenSamlSigningUtils.sign(registration));
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
this.handler.logout(request, null, authentication);
}
@Test
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
given(this.repository.removeLogoutRequest(any(), any())).willReturn(logoutRequest);
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getIssuer().setValue("wrong");
sign(logoutResponse, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutResponse);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.handler.logout(request, null, authentication))
.withMessageContaining(Saml2ErrorCodes.INVALID_SIGNATURE);
}
@Test
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
given(this.repository.removeLogoutRequest(any(), any())).willReturn(logoutRequest);
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.setDestination("wrong");
sign(logoutResponse, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutResponse);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.handler.logout(request, null, authentication))
.withMessageContaining(Saml2ErrorCodes.INVALID_DESTINATION);
}
@Test
public void handleWhenStatusNotSuccessThenInvalidResponseError() {
RelyingPartyRegistration registration = registration().build();
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
.build();
given(this.repository.removeLogoutRequest(any(), any())).willReturn(logoutRequest);
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
logoutResponse.getStatus().getStatusCode().setValue(StatusCode.UNKNOWN_PRINCIPAL);
sign(logoutResponse, registration);
Authentication authentication = authentication(registration);
MockHttpServletRequest request = post(logoutResponse);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> this.handler.logout(request, null, authentication))
.withMessageContaining(Saml2ErrorCodes.INVALID_RESPONSE);
}
private RelyingPartyRegistration.Builder registration() {
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()));
}
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) {
return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response",
new ArrayList<>(), registration.getRegistrationId());
}
private MockHttpServletRequest post(LogoutResponse logoutResponse) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setParameter("SAMLResponse",
Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8)));
return request;
}
private MockHttpServletRequest redirect(LogoutResponse logoutResponse, QueryParametersPartial partial) {
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse)));
Map<String, String> parameters = partial.param("SAMLResponse", serialized).parameters();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameters(parameters);
request.setMethod("GET");
return request;
}
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

@ -1,119 +0,0 @@
/*
* 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.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.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.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* Tests for {@link OpenSamlLogoutResponseResolver}
*
* @author Josh Cummings
*/
public class OpenSamlLogoutResponseResolverTests {
private final RelyingPartyRegistrationResolver resolver = mock(RelyingPartyRegistrationResolver.class);
private final OpenSamlLogoutResponseResolver logoutResolver = new OpenSamlLogoutResponseResolver(this.resolver);
@Test
public void resolveRedirectWhenAuthenticatedThenSuccess() {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
Saml2Authentication authentication = authentication(registration);
HttpServletRequest request = new MockHttpServletRequest();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setAttribute(LogoutRequest.class.getName(), logoutRequest);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
Saml2LogoutResponse saml2LogoutResponse = this.logoutResolver.resolveLogoutResponse(request, authentication)
.logoutResponse();
assertThat(saml2LogoutResponse.getParameter("SigAlg")).isNotNull();
assertThat(saml2LogoutResponse.getParameter("Signature")).isNotNull();
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();
Saml2Authentication authentication = authentication(registration);
HttpServletRequest request = new MockHttpServletRequest();
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
request.setAttribute(LogoutRequest.class.getName(), logoutRequest);
given(this.resolver.resolve(request, registration.getRegistrationId())).willReturn(registration);
Saml2LogoutResponse saml2LogoutResponse = this.logoutResolver.resolveLogoutResponse(request, authentication)
.logoutResponse();
assertThat(saml2LogoutResponse.getParameter("SigAlg")).isNull();
assertThat(saml2LogoutResponse.getParameter("Signature")).isNull();
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
LogoutResponse logoutResponse = getLogoutResponse(saml2LogoutResponse.getSamlResponse(), binding);
assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS);
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response",
new ArrayList<>(), registration.getRegistrationId());
}
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

@ -1,116 +0,0 @@
/*
* 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.After;
import org.junit.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.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.verifyNoInteractions;
public class Saml2LogoutRequestFilterTests {
private final LogoutHandler handler = mock(LogoutHandler.class);
private final LogoutSuccessHandler successHandler = mock(LogoutSuccessHandler.class);
private final Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(this.successHandler, this.handler);
@After
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void doFilterWhenSamlRequestMatchesThenLogout() 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("SAMLRequest", "request");
MockHttpServletResponse response = new MockHttpServletResponse();
this.filter.doFilterInternal(request, response, new MockFilterChain());
verify(this.handler).logout(request, response, authentication);
verify(this.successHandler).onLogoutSuccess(request, response, authentication);
}
@Test
public void doFilterWhenSamlResponseMatchesThenLogout() 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("SAMLRequest", "request");
MockHttpServletResponse response = new MockHttpServletResponse();
this.filter.doFilterInternal(request, response, new MockFilterChain());
verify(this.handler).logout(request, response, authentication);
verify(this.successHandler).onLogoutSuccess(request, response, authentication);
}
@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.filter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.handler);
verifyNoInteractions(this.successHandler);
}
@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.filter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.handler);
verifyNoInteractions(this.successHandler);
}
@Test
public void doFilterWhenLogoutHandlerFailsThenNoSuccessHandler() {
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();
willThrow(RuntimeException.class).given(this.handler).logout(request, response, authentication);
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> this.filter.doFilterInternal(request, response, new MockFilterChain()));
verify(this.handler).logout(request, response, authentication);
verifyNoInteractions(this.successHandler);
}
}

View File

@ -1,111 +0,0 @@
/*
* 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.After;
import org.junit.Before;
import org.junit.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 org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver.Saml2LogoutRequestBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.RETURNS_SELF;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.willReturn;
/**
* Tests for {@link Saml2LogoutRequestSuccessHandler}
*
* @author Josh Cummings
*/
public class Saml2LogoutRequestSuccessHandlerTests {
private final Saml2LogoutRequestResolver resolver = mock(Saml2LogoutRequestResolver.class);
private final Saml2LogoutRequestRepository repository = mock(Saml2LogoutRequestRepository.class);
private final Saml2LogoutRequestSuccessHandler handler = new Saml2LogoutRequestSuccessHandler(this.resolver);
@Before
public void setUp() {
this.handler.setLogoutRequestRepository(this.repository);
}
@After
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void doFilterWhenRedirectThenRedirectsToAssertingParty() 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();
Saml2LogoutRequestBuilder<?> partial = mock(Saml2LogoutRequestBuilder.class, RETURNS_SELF);
given(partial.logoutRequest()).willReturn(logoutRequest);
willReturn(partial).given(this.resolver).resolveLogoutRequest(request, authentication);
this.handler.onLogoutSuccess(request, response, authentication);
String content = response.getHeader("Location");
assertThat(content).contains("SAMLRequest");
assertThat(content).startsWith(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
}
@Test
public void doFilterWhenPostThenPostsToAssertingParty() 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();
Saml2LogoutRequestBuilder<?> partial = mock(Saml2LogoutRequestBuilder.class, RETURNS_SELF);
given(partial.logoutRequest()).willReturn(logoutRequest);
willReturn(partial).given(this.resolver).resolveLogoutRequest(request, authentication);
this.handler.onLogoutSuccess(request, response, authentication);
String content = response.getContentAsString();
assertThat(content).contains("SAMLRequest");
assertThat(content).contains(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response",
new ArrayList<>(), registration.getRegistrationId());
}
}

View File

@ -1,122 +0,0 @@
/*
* 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.After;
import org.junit.Before;
import org.junit.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.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.verifyNoInteractions;
public class Saml2LogoutResponseFilterTests {
private final LogoutHandler handler = mock(LogoutHandler.class);
private final LogoutSuccessHandler successHandler = mock(LogoutSuccessHandler.class);
private final Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(this.handler);
@Before
public void setUp() {
this.filter.setLogoutSuccessHandler(this.successHandler);
}
@After
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void doFilterWhenSamlRequestMatchesThenLogout() 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();
this.filter.doFilterInternal(request, response, new MockFilterChain());
verify(this.handler).logout(request, response, authentication);
verify(this.successHandler).onLogoutSuccess(request, response, authentication);
}
@Test
public void doFilterWhenSamlResponseMatchesThenLogout() 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();
this.filter.doFilterInternal(request, response, new MockFilterChain());
verify(this.handler).logout(request, response, authentication);
verify(this.successHandler).onLogoutSuccess(request, response, authentication);
}
@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.filter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.handler);
verifyNoInteractions(this.successHandler);
}
@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");
MockHttpServletResponse response = new MockHttpServletResponse();
this.filter.doFilterInternal(request, response, new MockFilterChain());
verifyNoInteractions(this.handler);
verifyNoInteractions(this.successHandler);
}
@Test
public void doFilterWhenLogoutHandlerFailsThenNoSuccessHandler() {
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();
willThrow(RuntimeException.class).given(this.handler).logout(request, response, authentication);
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> this.filter.doFilterInternal(request, response, new MockFilterChain()));
verify(this.handler).logout(request, response, authentication);
verifyNoInteractions(this.successHandler);
}
}

View File

@ -1,95 +0,0 @@
/*
* 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.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.Authentication;
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.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.authentication.logout.Saml2LogoutResponseResolver.Saml2LogoutResponseBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.RETURNS_SELF;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.willReturn;
/**
* Tests for {@link Saml2LogoutResponseSuccessHandler}
*
* @author Josh Cummings
*/
public class Saml2LogoutResponseSuccessHandlerTests {
private final Saml2LogoutResponseResolver resolver = mock(Saml2LogoutResponseResolver.class);
private final Saml2LogoutResponseSuccessHandler handler = new Saml2LogoutResponseSuccessHandler(this.resolver);
@Test
public void doFilterWhenRedirectThenRedirectsToAssertingParty() throws Exception {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
Authentication authentication = authentication(registration);
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse("response").build();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(Saml2RequestAttributeNames.LOGOUT_REQUEST_ID, "id");
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutResponseBuilder<?> partial = mock(Saml2LogoutResponseBuilder.class, RETURNS_SELF);
given(partial.logoutResponse()).willReturn(logoutResponse);
willReturn(partial).given(this.resolver).resolveLogoutResponse(request, authentication);
this.handler.onLogoutSuccess(request, response, authentication);
String content = response.getHeader("Location");
assertThat(content).contains("SAMLResponse");
assertThat(content)
.startsWith(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
}
@Test
public void doFilterWhenPostThenPostsToAssertingParty() throws Exception {
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
Authentication authentication = authentication(registration);
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
.samlResponse("response").build();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(Saml2RequestAttributeNames.LOGOUT_REQUEST_ID, "id");
MockHttpServletResponse response = new MockHttpServletResponse();
Saml2LogoutResponseBuilder<?> partial = mock(Saml2LogoutResponseBuilder.class, RETURNS_SELF);
given(partial.logoutResponse()).willReturn(logoutResponse);
willReturn(partial).given(this.resolver).resolveLogoutResponse(request, authentication);
this.handler.onLogoutSuccess(request, response, authentication);
String content = response.getContentAsString();
assertThat(content).contains("SAMLResponse");
assertThat(content).contains(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()), "response",
new ArrayList<>(), registration.getRegistrationId());
}
}

View File

@ -1,91 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestResolver.OpenSamlLogoutRequestBuilder;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
* OpenSAML 3
*
* @author Josh Cummings
* @since 5.5
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
* {@code OpenSaml4LogoutRequestResolver}
*/
public class OpenSaml3LogoutRequestResolver implements Saml2LogoutRequestResolver {
private final OpenSamlLogoutRequestResolver logoutRequestResolver;
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml3LogoutRequestResolver} with the provided parameters
* @param relyingPartyRegistrationResolver a strategy for resolving a
* {@link RelyingPartyRegistration}
*/
public OpenSaml3LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutRequestResolver = new OpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver);
}
/**
* 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}. The
* request also contains its issued {@link DateTime}.
*
* The {@link Authentication} must be of type {@link Saml2Authentication} in order to
* look up the {@link RelyingPartyRegistration} that holds the signing key.
* @param request the HTTP request
* @param authentication the current principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout Request
* that the resolver supplied
*/
@Override
public Saml2LogoutRequestBuilder<?> resolveLogoutRequest(HttpServletRequest request,
Authentication authentication) {
OpenSamlLogoutRequestBuilder builder = this.logoutRequestResolver.resolveLogoutRequest(request, authentication);
if (builder == null) {
return null;
}
return builder
.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(new DateTime(this.clock.millis())));
}
/**
* 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;
}
}

View File

@ -1,89 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseResolver.OpenSamlLogoutResponseBuilder;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutResponseResolver} for resolving SAML 2.0 Logout Responses with
* OpenSAML 3
*
* @author Josh Cummings
* @since 5.5
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
* {@code OpenSaml4LogoutResponseResolver}
*/
public class OpenSaml3LogoutResponseResolver implements Saml2LogoutResponseResolver {
private final OpenSamlLogoutResponseResolver logoutResponseResolver;
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml3LogoutResponseResolver} with the provided parameters
* @param relyingPartyRegistrationResolver a strategy for resolving a
* {@link RelyingPartyRegistration}
*/
public OpenSaml3LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver);
}
/**
* 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 also includes an issued {@link DateTime} and is marked as
* {@code SUCCESS}.
*
* The {@link Authentication} must be of type {@link Saml2Authentication} in order to
* look up the {@link RelyingPartyRegistration} that holds the signing key.
* @param request the HTTP request
* @param authentication the current principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout Request
* that the resolver supplied
*/
@Override
public Saml2LogoutResponseBuilder<?> resolveLogoutResponse(HttpServletRequest request,
Authentication authentication) {
OpenSamlLogoutResponseBuilder builder = this.logoutResponseResolver.resolveLogoutResponse(request,
authentication);
if (builder == null) {
return null;
}
return builder
.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(new DateTime(this.clock.millis())));
}
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.clock = clock;
}
}

View File

@ -1,87 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestResolver.OpenSamlLogoutRequestBuilder;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
* OpenSAML 4
*
* @author Josh Cummings
* @since 5.5
*/
public class OpenSaml4LogoutRequestResolver implements Saml2LogoutRequestResolver {
private final OpenSamlLogoutRequestResolver logoutRequestResolver;
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml4LogoutRequestResolver} with the provided parameters
* @param relyingPartyRegistrationResolver a strategy for resolving a
* {@link RelyingPartyRegistration}
*/
public OpenSaml4LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutRequestResolver = new OpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver);
}
/**
* 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}. The
* request also contains its issued {@link Instant}.
*
* The {@link Authentication} must be of type {@link Saml2Authentication} in order to
* look up the {@link RelyingPartyRegistration} that holds the signing key.
* @param request the HTTP request
* @param authentication the current principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout Request
* that the resolver supplied
*/
@Override
public Saml2LogoutRequestBuilder<?> resolveLogoutRequest(HttpServletRequest request,
Authentication authentication) {
OpenSamlLogoutRequestBuilder builder = this.logoutRequestResolver.resolveLogoutRequest(request, authentication);
if (builder == null) {
return null;
}
return builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(Instant.now(this.clock)));
}
/**
* 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;
}
}

View File

@ -1,85 +0,0 @@
/*
* 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 javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseResolver.OpenSamlLogoutResponseBuilder;
import org.springframework.util.Assert;
/**
* A {@link Saml2LogoutResponseResolver} for resolving SAML 2.0 Logout Responses with
* OpenSAML 4
*
* @author Josh Cummings
* @since 5.5
*/
public class OpenSaml4LogoutResponseResolver implements Saml2LogoutResponseResolver {
private final OpenSamlLogoutResponseResolver logoutResponseResolver;
private Clock clock = Clock.systemUTC();
/**
* Construct a {@link OpenSaml4LogoutResponseResolver} with the provided parameters
* @param relyingPartyRegistrationResolver the strategy for resolving a
* {@link RelyingPartyRegistration}
*/
public OpenSaml4LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver);
}
/**
* 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 also includes an issued {@link Instant} and is marked as
* {@code SUCCESS}.
*
* The {@link Authentication} must be of type {@link Saml2Authentication} in order to
* look up the {@link RelyingPartyRegistration} that holds the signing key.
* @param request the HTTP request
* @param authentication the current principal details
* @return a builder, useful for overriding any aspects of the SAML 2.0 Logout Request
* that the resolver supplied
*/
@Override
public Saml2LogoutResponseBuilder<?> resolveLogoutResponse(HttpServletRequest request,
Authentication authentication) {
OpenSamlLogoutResponseBuilder builder = this.logoutResponseResolver.resolveLogoutResponse(request,
authentication);
if (builder == null) {
return null;
}
return builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(Instant.now(this.clock)));
}
public void setClock(Clock clock) {
Assert.notNull(clock, "clock must not be null");
this.clock = clock;
}
}