parent
2f734a0975
commit
e807fae869
|
@ -1053,20 +1053,279 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
|||
[[servlet-saml2login-logout]]
|
||||
=== Performing Single Logout
|
||||
|
||||
Spring Security does not yet support single logout.
|
||||
Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
|
||||
|
||||
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
|
||||
Briefly, there are two use cases Spring Security supports:
|
||||
|
||||
* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party.
|
||||
Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond
|
||||
* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party.
|
||||
Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party.
|
||||
|
||||
[NOTE]
|
||||
In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot.
|
||||
Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser.
|
||||
|
||||
=== Minimal Configuration for Single Logout
|
||||
|
||||
To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things:
|
||||
|
||||
* First, the asserting party must support SAML 2.0 Single Logout
|
||||
* Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint
|
||||
* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
|
||||
|
||||
==== RP-Initiated Single Logout
|
||||
|
||||
Given those, then for RP-initiated Single Logout, you can begin from the initial minimal example and add the following configuration:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Value("${private.key}") RSAPrivateKey key;
|
||||
@Value("${public.certificate}") X509Certificate certificate;
|
||||
|
||||
@Bean
|
||||
RelyingPartyRegistrationRepository registrations() {
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
||||
.fromMetadataLocation("https://ap.example.org/metadata")
|
||||
.registrationId("id")
|
||||
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
|
||||
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
|
||||
.build();
|
||||
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
|
||||
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
LogoutHandler logoutResponseHandler = logoutResponseHandler(registrationResolver);
|
||||
LogoutSuccessHandler logoutRequestSuccessHandler = logoutRequestSuccessHandler(registrationResolver);
|
||||
|
||||
http
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.saml2Login(withDefaults())
|
||||
.logout((logout) -> logout
|
||||
.logoutUrl("/saml2/logout")
|
||||
.logoutSuccessHandler(successHandler))
|
||||
.addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private LogoutSuccessHandler logoutRequestSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
|
||||
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(registrationResolver);
|
||||
return new Saml2LogoutRequestSuccessHandler(logoutRequestResolver);
|
||||
}
|
||||
|
||||
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
|
||||
return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
|
||||
}
|
||||
----
|
||||
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
|
||||
<2> - Second, supply a `LogoutSuccessHandler` for initiating Single Logout, sending a `saml2:LogoutRequest` to the asserting party
|
||||
<3> - Third, supply the `LogoutHandler` s needed to handle the `saml2:LogoutResponse` s sent from the asserting party.
|
||||
|
||||
==== Runtime Expectations for RP-Initiated
|
||||
|
||||
Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
|
||||
Your application will then do the following:
|
||||
|
||||
1. Logout the user and invalidate the session
|
||||
2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the currently logged-in user.
|
||||
3. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
|
||||
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
|
||||
5. Redirect to any configured successful logout endpoint
|
||||
|
||||
[TIP]
|
||||
If your asserting party does not send `<saml2:LogoutResponse>` s when logout is complete, the asserting party can still send a `POST /saml2/logout` and then there is no need to configure the `Saml2LogoutResponseHandler`.
|
||||
|
||||
==== AP-Initiated Single Logout
|
||||
|
||||
Instead of RP-initiated Single Logout, you can again begin from the initial minimal example and add the following configuration to achieve AP-initiated Single Logout:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Value("${private.key}") RSAPrivateKey key;
|
||||
@Value("${public.certificate}") X509Certificate certificate;
|
||||
|
||||
@Bean
|
||||
RelyingPartyRegistrationRepository registrations() {
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
||||
.fromMetadataLocation("https://ap.example.org/metadata")
|
||||
.registrationId("id")
|
||||
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
|
||||
.build();
|
||||
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
|
||||
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
LogoutHandler logoutRequestHandler = logoutRequestHandler(registrationResolver);
|
||||
LogoutSuccessHandler logoutResponseSuccessHandler = logoutResponseSuccessHandler(registrationResolver);
|
||||
|
||||
http
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.saml2Login(withDefaults())
|
||||
.addFilterBefore(new Saml2LogoutRequestFilter(logoutResponseSuccessHandler, logoutRequestHandler), CsrfFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
|
||||
return new CompositeLogoutHandler(
|
||||
new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver),
|
||||
new SecurityContextLogoutHandler(),
|
||||
new LogoutSuccessEventPublishingLogoutHandler());
|
||||
}
|
||||
|
||||
private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
|
||||
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(registrationResolver);
|
||||
return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver);
|
||||
}
|
||||
----
|
||||
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
|
||||
<2> - Second, supply the `LogoutHandler` needed to handle the `saml2:LogoutRequest` s sent from the asserting party.
|
||||
<3> - Third, supply a `LogoutSuccessHandler` for completing Single Logout, sending a `saml2:LogoutResponse` to the asserting party
|
||||
|
||||
==== Runtime Expectations for AP-Initiated
|
||||
|
||||
Given the above configuration, an asserting party can send a `POST /logout/saml2` to your application that includes a `<saml2:LogoutRequest>`
|
||||
Also, your application can participate in an AP-initated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
|
||||
|
||||
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
|
||||
2. Logout the user and invalidate the session
|
||||
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
|
||||
4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
|
||||
|
||||
[TIP]
|
||||
If your asserting party does not expect you do send a `<saml2:LogoutResponse>` s when logout is complete, you may not need to configure a `LogoutSuccessHandler`
|
||||
|
||||
[NOTE]
|
||||
In the event that you need to support both logout flows, you can combine the above to configurations.
|
||||
|
||||
=== Configuring Logout Endpoints
|
||||
|
||||
There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
|
||||
* `/logout` - the endpoint for initiating single logout with an asserting party
|
||||
* `/logout/saml2/slo` - the endpoint for receiving logout requests or responses from an asserting party
|
||||
|
||||
Because the user is already logged in, the `registrationId` is already known.
|
||||
For this reason, `+{registrationId}+` is not part of these URLs by default.
|
||||
|
||||
These URLs are customizable in the DSL.
|
||||
|
||||
For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`.
|
||||
To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
|
||||
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
|
||||
http
|
||||
// ...
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessHandler(myCustomSuccessHandler())
|
||||
.logoutRequestMatcher(myRequestMatcher())
|
||||
)
|
||||
.addFilterBefore(filter, CsrfFilter.class);
|
||||
----
|
||||
|
||||
The success handler will send logout requests to the asserting party.
|
||||
=== Customizing `<saml2:LogoutRequest>` Resolution
|
||||
|
||||
The request matcher will detect logout requests from 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
|
||||
}
|
||||
}
|
||||
----
|
||||
|
|
|
@ -37,6 +37,13 @@ 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.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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 <saml2:LogoutRequest> payload
|
||||
* @return the signed and serialized <saml2:LogoutRequest> 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 <saml2:LogoutRequest>
|
||||
*
|
||||
* 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 <saml2:LogoutRequest> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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 <saml2:LogoutResponse> payload
|
||||
* @return the signed and serialized <saml2:LogoutResponse> 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 <saml2:LogoutResponse>
|
||||
*
|
||||
* 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 <saml2:LogoutResponse> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
|
|||
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
|
||||
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.xmlsec.signature.KeyInfo;
|
||||
|
@ -85,6 +86,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
|
|||
spSsoDescriptor.getKeyDescriptors()
|
||||
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
|
||||
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
|
||||
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
|
||||
return spSsoDescriptor;
|
||||
}
|
||||
|
||||
|
@ -123,6 +125,14 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
|
|||
return assertionConsumerService;
|
||||
}
|
||||
|
||||
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration) {
|
||||
SingleLogoutService singleLogoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
|
||||
singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation());
|
||||
singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
|
||||
singleLogoutService.setBinding(registration.getSingleLogoutServiceBinding().getUrn());
|
||||
return singleLogoutService;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T build(QName elementName) {
|
||||
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
|||
import org.opensaml.saml.saml2.metadata.Extensions;
|
||||
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
|
||||
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
|
||||
|
@ -105,6 +106,10 @@ class OpenSamlAssertingPartyMetadataConverter {
|
|||
builder.assertingPartyDetails(
|
||||
(party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())));
|
||||
}
|
||||
if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) {
|
||||
throw new Saml2Exception(
|
||||
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
|
||||
}
|
||||
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
|
||||
Saml2MessageBinding binding;
|
||||
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
|
||||
|
@ -119,10 +124,27 @@ class OpenSamlAssertingPartyMetadataConverter {
|
|||
builder.assertingPartyDetails(
|
||||
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
|
||||
.singleSignOnServiceBinding(binding));
|
||||
return builder;
|
||||
break;
|
||||
}
|
||||
throw new Saml2Exception(
|
||||
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
|
||||
for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
|
||||
Saml2MessageBinding binding;
|
||||
if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
|
||||
binding = Saml2MessageBinding.POST;
|
||||
}
|
||||
else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
|
||||
binding = Saml2MessageBinding.REDIRECT;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
String responseLocation = (singleLogoutService.getResponseLocation() == null)
|
||||
? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation();
|
||||
builder.assertingPartyDetails(
|
||||
(party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation())
|
||||
.singleLogoutServiceResponseLocation(responseLocation).singleLogoutServiceBinding(binding));
|
||||
break;
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
|
||||
|
|
|
@ -81,6 +81,12 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
private final Saml2MessageBinding assertionConsumerServiceBinding;
|
||||
|
||||
private final String singleLogoutServiceLocation;
|
||||
|
||||
private final String singleLogoutServiceResponseLocation;
|
||||
|
||||
private final Saml2MessageBinding singleLogoutServiceBinding;
|
||||
|
||||
private final ProviderDetails providerDetails;
|
||||
|
||||
private final List<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials;
|
||||
|
@ -90,7 +96,9 @@ public final class RelyingPartyRegistration {
|
|||
private final Collection<Saml2X509Credential> signingX509Credentials;
|
||||
|
||||
private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
|
||||
Saml2MessageBinding assertionConsumerServiceBinding, ProviderDetails providerDetails,
|
||||
Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation,
|
||||
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding,
|
||||
ProviderDetails providerDetails,
|
||||
Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials,
|
||||
Collection<Saml2X509Credential> decryptionX509Credentials,
|
||||
Collection<Saml2X509Credential> signingX509Credentials) {
|
||||
|
@ -118,6 +126,9 @@ public final class RelyingPartyRegistration {
|
|||
this.entityId = entityId;
|
||||
this.assertionConsumerServiceLocation = assertionConsumerServiceLocation;
|
||||
this.assertionConsumerServiceBinding = assertionConsumerServiceBinding;
|
||||
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||
this.providerDetails = providerDetails;
|
||||
this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials));
|
||||
this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials));
|
||||
|
@ -177,6 +188,51 @@ 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 <SingleLogoutService Binding="..."/> in the
|
||||
* relying party's <SPSSODescriptor>.
|
||||
* @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 <SingleLogoutService Location="..."/> in the
|
||||
* relying party's <SPSSODescriptor>.
|
||||
* @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 <SingleLogoutService
|
||||
* ResponseLocation="..."/> in the relying party's <SPSSODescriptor>.
|
||||
* @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
|
||||
|
@ -364,6 +420,9 @@ public final class RelyingPartyRegistration {
|
|||
.decryptionX509Credentials((c) -> c.addAll(registration.getDecryptionX509Credentials()))
|
||||
.assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation())
|
||||
.assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding())
|
||||
.singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation())
|
||||
.singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation())
|
||||
.singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding())
|
||||
.assertingPartyDetails((assertingParty) -> assertingParty
|
||||
.entityId(registration.getAssertingPartyDetails().getEntityId())
|
||||
.wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned())
|
||||
|
@ -376,7 +435,13 @@ public final class RelyingPartyRegistration {
|
|||
.singleSignOnServiceLocation(
|
||||
registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
|
||||
.singleSignOnServiceBinding(
|
||||
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()));
|
||||
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
|
||||
.singleLogoutServiceLocation(
|
||||
registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
|
||||
.singleLogoutServiceResponseLocation(
|
||||
registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
|
||||
.singleLogoutServiceBinding(
|
||||
registration.getAssertingPartyDetails().getSingleLogoutServiceBinding()));
|
||||
}
|
||||
|
||||
private static Saml2X509Credential fromDeprecated(
|
||||
|
@ -445,10 +510,17 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
private final Saml2MessageBinding singleSignOnServiceBinding;
|
||||
|
||||
private final String singleLogoutServiceLocation;
|
||||
|
||||
private final String singleLogoutServiceResponseLocation;
|
||||
|
||||
private final Saml2MessageBinding singleLogoutServiceBinding;
|
||||
|
||||
private AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, List<String> signingAlgorithms,
|
||||
Collection<Saml2X509Credential> verificationX509Credentials,
|
||||
Collection<Saml2X509Credential> encryptionX509Credentials, String singleSignOnServiceLocation,
|
||||
Saml2MessageBinding singleSignOnServiceBinding) {
|
||||
Saml2MessageBinding singleSignOnServiceBinding, String singleLogoutServiceLocation,
|
||||
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding) {
|
||||
Assert.hasText(entityId, "entityId cannot be null or empty");
|
||||
Assert.notEmpty(signingAlgorithms, "signingAlgorithms cannot be empty");
|
||||
Assert.notNull(verificationX509Credentials, "verificationX509Credentials cannot be null");
|
||||
|
@ -472,6 +544,9 @@ public final class RelyingPartyRegistration {
|
|||
this.encryptionX509Credentials = encryptionX509Credentials;
|
||||
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
|
||||
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
|
||||
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -565,6 +640,48 @@ 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 <SingleLogoutService Location="..."/> in
|
||||
* the asserting party's <IDPSSODescriptor>.
|
||||
* @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 <SingleLogoutService Location="..."/> in
|
||||
* the asserting party's <IDPSSODescriptor>.
|
||||
* @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 <SingleLogoutService Binding="..."/> in
|
||||
* the asserting party's <IDPSSODescriptor>.
|
||||
* @return the SingleLogoutService Binding
|
||||
*/
|
||||
public Saml2MessageBinding getSingleLogoutServiceBinding() {
|
||||
return this.singleLogoutServiceBinding;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String entityId;
|
||||
|
@ -581,6 +698,12 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
|
||||
|
||||
private String singleLogoutServiceLocation;
|
||||
|
||||
private String singleLogoutServiceResponseLocation;
|
||||
|
||||
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.REDIRECT;
|
||||
|
||||
/**
|
||||
* Set the asserting party's <a href=
|
||||
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/EntityNaming">EntityID</a>.
|
||||
|
@ -677,6 +800,55 @@ 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 <SingleLogoutService
|
||||
* Location="..."/> in the asserting party's <IDPSSODescriptor>.
|
||||
* @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 <SingleLogoutService
|
||||
* ResponseLocation="..."/> in the asserting party's
|
||||
* <IDPSSODescriptor>.
|
||||
* @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 <SingleLogoutService Binding="..."/>
|
||||
* in the asserting party's <IDPSSODescriptor>.
|
||||
* @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
|
||||
|
@ -689,7 +861,9 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
return new AssertingPartyDetails(this.entityId, this.wantAuthnRequestsSigned, signingAlgorithms,
|
||||
this.verificationX509Credentials, this.encryptionX509Credentials,
|
||||
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding);
|
||||
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding,
|
||||
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||
this.singleLogoutServiceBinding);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -830,6 +1004,12 @@ 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<>();
|
||||
|
@ -933,6 +1113,54 @@ 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 <SingleLogoutService Binding="..."/> in
|
||||
* the relying party's <SPSSODescriptor>.
|
||||
* @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 <SingleLogoutService Location="..."/> in
|
||||
* the relying party's <SPSSODescriptor>.
|
||||
* @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 <SingleLogoutService
|
||||
* ResponseLocation="..."/> in the relying party's <SPSSODescriptor>.
|
||||
* @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
|
||||
|
@ -1075,10 +1303,14 @@ public final class RelyingPartyRegistration {
|
|||
for (Saml2X509Credential credential : this.providerDetails.assertingPartyDetailsBuilder.encryptionX509Credentials) {
|
||||
this.credentials.add(toDeprecated(credential));
|
||||
}
|
||||
if (this.singleLogoutServiceResponseLocation == null) {
|
||||
this.singleLogoutServiceResponseLocation = this.singleLogoutServiceLocation;
|
||||
}
|
||||
return new RelyingPartyRegistration(this.registrationId, this.entityId,
|
||||
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
|
||||
this.providerDetails.build(), this.credentials, this.decryptionX509Credentials,
|
||||
this.signingX509Credentials);
|
||||
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||
this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials,
|
||||
this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,8 +41,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|||
* @author Josh Cummings
|
||||
* @since 5.4
|
||||
*/
|
||||
public final class DefaultRelyingPartyRegistrationResolver
|
||||
implements Converter<HttpServletRequest, RelyingPartyRegistration>, RelyingPartyRegistrationResolver {
|
||||
public final class DefaultRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||
|
||||
private static final char PATH_DELIMITER = '/';
|
||||
|
||||
|
@ -56,14 +55,6 @@ public final class DefaultRelyingPartyRegistrationResolver
|
|||
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public RelyingPartyRegistration convert(HttpServletRequest request) {
|
||||
return resolve(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
@ -86,9 +77,14 @@ public final class DefaultRelyingPartyRegistrationResolver
|
|||
String relyingPartyEntityId = templateResolver.apply(relyingPartyRegistration.getEntityId());
|
||||
String assertionConsumerServiceLocation = templateResolver
|
||||
.apply(relyingPartyRegistration.getAssertionConsumerServiceLocation());
|
||||
String singleLogoutServiceLocation = templateResolver
|
||||
.apply(relyingPartyRegistration.getSingleLogoutServiceLocation());
|
||||
String singleLogoutServiceResponseLocation = templateResolver
|
||||
.apply(relyingPartyRegistration.getSingleLogoutServiceResponseLocation());
|
||||
return RelyingPartyRegistration.withRelyingPartyRegistration(relyingPartyRegistration)
|
||||
.entityId(relyingPartyEntityId).assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.build();
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation).build();
|
||||
}
|
||||
|
||||
private Function<String, String> templateResolver(String applicationUri, RelyingPartyRegistration relyingParty) {
|
||||
|
@ -96,6 +92,9 @@ public final class DefaultRelyingPartyRegistrationResolver
|
|||
}
|
||||
|
||||
private static String resolveUrlTemplate(String template, String baseUrl, RelyingPartyRegistration relyingParty) {
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
String entityId = relyingParty.getAssertingPartyDetails().getEntityId();
|
||||
String registrationId = relyingParty.getRegistrationId();
|
||||
Map<String, String> uriVariables = new HashMap<>();
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.core.xml.io.Marshaller;
|
||||
import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
|
||||
import org.opensaml.security.SecurityException;
|
||||
import org.opensaml.security.credential.BasicCredential;
|
||||
import org.opensaml.security.credential.Credential;
|
||||
import org.opensaml.security.credential.CredentialSupport;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.xmlsec.SignatureSigningParameters;
|
||||
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
|
||||
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
|
||||
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
|
||||
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
|
||||
import org.opensaml.xmlsec.signature.SignableXMLObject;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureSupport;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for signing SAML components with OpenSAML
|
||||
*
|
||||
* For internal use only.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
final class OpenSamlSigningUtils {
|
||||
|
||||
static String serialize(XMLObject object) {
|
||||
try {
|
||||
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
|
||||
Element element = marshaller.marshall(object);
|
||||
return SerializeSupport.nodeToString(element);
|
||||
}
|
||||
catch (MarshallingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static <O extends SignableXMLObject> O sign(O object, RelyingPartyRegistration relyingPartyRegistration) {
|
||||
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
|
||||
try {
|
||||
SignatureSupport.signObject(object, parameters);
|
||||
return object;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static QueryParametersPartial sign(RelyingPartyRegistration registration) {
|
||||
return new QueryParametersPartial(registration);
|
||||
}
|
||||
|
||||
private static SignatureSigningParameters resolveSigningParameters(
|
||||
RelyingPartyRegistration relyingPartyRegistration) {
|
||||
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
|
||||
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
|
||||
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
|
||||
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
|
||||
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
|
||||
CriteriaSet criteria = new CriteriaSet();
|
||||
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
|
||||
signingConfiguration.setSigningCredentials(credentials);
|
||||
signingConfiguration.setSignatureAlgorithms(algorithms);
|
||||
signingConfiguration.setSignatureReferenceDigestMethods(digests);
|
||||
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
|
||||
criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration));
|
||||
try {
|
||||
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
|
||||
Assert.notNull(parameters, "Failed to resolve any signing credential");
|
||||
return parameters;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
|
||||
List<Credential> credentials = new ArrayList<>();
|
||||
for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
|
||||
X509Certificate certificate = x509Credential.getCertificate();
|
||||
PrivateKey privateKey = x509Credential.getPrivateKey();
|
||||
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
|
||||
credential.setEntityId(relyingPartyRegistration.getEntityId());
|
||||
credential.setUsageType(UsageType.SIGNING);
|
||||
credentials.add(credential);
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
static class QueryParametersPartial {
|
||||
|
||||
final RelyingPartyRegistration registration;
|
||||
|
||||
final Map<String, String> components = new LinkedHashMap<>();
|
||||
|
||||
QueryParametersPartial(RelyingPartyRegistration registration) {
|
||||
this.registration = registration;
|
||||
}
|
||||
|
||||
QueryParametersPartial param(String key, String value) {
|
||||
this.components.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
Map<String, String> parameters() {
|
||||
SignatureSigningParameters parameters = resolveSigningParameters(this.registration);
|
||||
Credential credential = parameters.getSigningCredential();
|
||||
String algorithmUri = parameters.getSignatureAlgorithm();
|
||||
this.components.put("SigAlg", algorithmUri);
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
|
||||
for (Map.Entry<String, String> component : this.components.entrySet()) {
|
||||
builder.queryParam(component.getKey(),
|
||||
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
String queryString = builder.build(true).toString().substring(1);
|
||||
try {
|
||||
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
|
||||
queryString.getBytes(StandardCharsets.UTF_8));
|
||||
String b64Signature = Saml2Utils.samlEncode(rawSignature);
|
||||
this.components.put("Signature", b64Signature);
|
||||
}
|
||||
catch (SecurityException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
return this.components;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private OpenSamlSigningUtils() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 <saml2:LogoutRequest> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for the persistence of
|
||||
* {@link Saml2LogoutRequest} between requests.
|
||||
*
|
||||
* <p>
|
||||
* Used by the {@link 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);
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 <saml2:LogoutResponse> sent from the asserting party. A
|
||||
* <saml2:LogoutResponse> is sent in response to a <saml2:LogoutRequest>
|
||||
* already sent by the relying party.
|
||||
*
|
||||
* Note that before a <saml2:LogoutRequest> 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 <saml2:LogoutResponse> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -54,6 +54,8 @@ import org.opensaml.saml.saml2.core.EncryptedAssertion;
|
|||
import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.Status;
|
||||
|
@ -63,6 +65,10 @@ import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
|||
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
|
||||
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.AttributeStatementBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutResponseBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.StatusBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
|
||||
import org.opensaml.saml.saml2.encryption.Encrypter;
|
||||
|
@ -83,6 +89,7 @@ import org.springframework.security.saml2.Saml2Exception;
|
|||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
public final class TestOpenSamlObjects {
|
||||
|
||||
|
@ -93,7 +100,7 @@ public final class TestOpenSamlObjects {
|
|||
|
||||
private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias";
|
||||
|
||||
private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
|
||||
public static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
|
||||
|
||||
private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
|
||||
|
||||
|
@ -221,7 +228,7 @@ public final class TestOpenSamlObjects {
|
|||
return signable;
|
||||
}
|
||||
|
||||
static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
|
||||
public static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
|
||||
return signed(signable, credential, entityId, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
|
||||
}
|
||||
|
||||
|
@ -342,6 +349,41 @@ public final class TestOpenSamlObjects {
|
|||
return status;
|
||||
}
|
||||
|
||||
public static LogoutRequest assertingPartyLogoutRequest(RelyingPartyRegistration registration) {
|
||||
LogoutRequestBuilder logoutRequestBuilder = new LogoutRequestBuilder();
|
||||
LogoutRequest logoutRequest = logoutRequestBuilder.buildObject();
|
||||
logoutRequest.setID("id");
|
||||
NameIDBuilder nameIdBuilder = new NameIDBuilder();
|
||||
NameID nameId = nameIdBuilder.buildObject();
|
||||
nameId.setValue("user");
|
||||
logoutRequest.setNameID(nameId);
|
||||
IssuerBuilder issuerBuilder = new IssuerBuilder();
|
||||
Issuer issuer = issuerBuilder.buildObject();
|
||||
issuer.setValue(registration.getAssertingPartyDetails().getEntityId());
|
||||
logoutRequest.setIssuer(issuer);
|
||||
logoutRequest.setDestination(registration.getSingleLogoutServiceLocation());
|
||||
return logoutRequest;
|
||||
}
|
||||
|
||||
public static LogoutResponse assertingPartyLogoutResponse(RelyingPartyRegistration registration) {
|
||||
LogoutResponseBuilder logoutResponseBuilder = new LogoutResponseBuilder();
|
||||
LogoutResponse logoutResponse = logoutResponseBuilder.buildObject();
|
||||
logoutResponse.setID("id");
|
||||
StatusBuilder statusBuilder = new StatusBuilder();
|
||||
StatusCodeBuilder statusCodeBuilder = new StatusCodeBuilder();
|
||||
StatusCode code = statusCodeBuilder.buildObject();
|
||||
code.setValue(StatusCode.SUCCESS);
|
||||
Status status = statusBuilder.buildObject();
|
||||
status.setStatusCode(code);
|
||||
logoutResponse.setStatus(status);
|
||||
IssuerBuilder issuerBuilder = new IssuerBuilder();
|
||||
Issuer issuer = issuerBuilder.buildObject();
|
||||
issuer.setValue(registration.getAssertingPartyDetails().getEntityId());
|
||||
logoutResponse.setIssuer(issuer);
|
||||
logoutResponse.setDestination(registration.getSingleLogoutServiceResponseLocation());
|
||||
return logoutResponse;
|
||||
}
|
||||
|
||||
static <T extends XMLObject> T build(QName qName) {
|
||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||
}
|
||||
|
|
|
@ -41,7 +41,8 @@ public class OpenSamlMetadataResolverTests {
|
|||
.contains("<md:KeyDescriptor use=\"encryption\">")
|
||||
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
|
||||
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
|
||||
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -56,7 +57,8 @@ public class OpenSamlMetadataResolverTests {
|
|||
.contains("WantAssertionsSigned=\"true\"").doesNotContain("<md:KeyDescriptor use=\"signing\">")
|
||||
.doesNotContain("<md:KeyDescriptor use=\"encryption\">")
|
||||
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"")
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
|
||||
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,17 +37,23 @@ public final class TestRelyingPartyRegistrations {
|
|||
String apEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
|
||||
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
||||
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.credentials((c) -> c.add(signingCredential))
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
|
||||
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
|
||||
.credentials((c) -> c.add(verificationCertificate));
|
||||
}
|
||||
|
||||
public static RelyingPartyRegistration.Builder noCredentials() {
|
||||
return RelyingPartyRegistration.withRegistrationId("registration-id").entityId("rp-entity-id")
|
||||
.assertionConsumerServiceLocation("https://rp.example.org/acs").assertingPartyDetails((party) -> party
|
||||
.entityId("ap-entity-id").singleSignOnServiceLocation("https://ap.example.org/sso"));
|
||||
.singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request")
|
||||
.singleLogoutServiceResponseLocation("https://rp.example.org/logout/saml2/response")
|
||||
.assertionConsumerServiceLocation("https://rp.example.org/acs")
|
||||
.assertingPartyDetails((party) -> party.entityId("ap-entity-id")
|
||||
.singleSignOnServiceLocation("https://ap.example.org/sso")
|
||||
.singleLogoutServiceLocation("https://ap.example.org/logout/saml2/request")
|
||||
.singleLogoutServiceResponseLocation("https://ap.example.org/logout/saml2/response"));
|
||||
}
|
||||
|
||||
public static RelyingPartyRegistration.Builder full() {
|
||||
|
|
|
@ -52,6 +52,9 @@ public class DefaultRelyingPartyRegistrationResolverTests {
|
|||
.isEqualTo("http://localhost/saml2/service-provider-metadata/" + this.registration.getRegistrationId());
|
||||
assertThat(registration.getAssertionConsumerServiceLocation())
|
||||
.isEqualTo("http://localhost/login/saml2/sso/" + this.registration.getRegistrationId());
|
||||
assertThat(registration.getSingleLogoutServiceLocation()).isEqualTo("http://localhost/logout/saml2/slo");
|
||||
assertThat(registration.getSingleLogoutServiceResponseLocation())
|
||||
.isEqualTo("http://localhost/logout/saml2/slo");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue