parent
2f734a0975
commit
e807fae869
|
@ -1053,20 +1053,279 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
||||||
[[servlet-saml2login-logout]]
|
[[servlet-saml2login-logout]]
|
||||||
=== Performing Single 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]
|
[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
|
http
|
||||||
// ...
|
// ...
|
||||||
.logout(logout -> logout
|
.addFilterBefore(filter, CsrfFilter.class);
|
||||||
.logoutSuccessHandler(myCustomSuccessHandler())
|
|
||||||
.logoutRequestMatcher(myRequestMatcher())
|
|
||||||
)
|
|
||||||
----
|
----
|
||||||
|
|
||||||
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";
|
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.
|
* 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.EntityDescriptor;
|
||||||
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
||||||
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
|
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.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
|
||||||
import org.opensaml.security.credential.UsageType;
|
import org.opensaml.security.credential.UsageType;
|
||||||
import org.opensaml.xmlsec.signature.KeyInfo;
|
import org.opensaml.xmlsec.signature.KeyInfo;
|
||||||
|
@ -85,6 +86,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
|
||||||
spSsoDescriptor.getKeyDescriptors()
|
spSsoDescriptor.getKeyDescriptors()
|
||||||
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
|
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
|
||||||
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
|
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
|
||||||
|
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
|
||||||
return spSsoDescriptor;
|
return spSsoDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +125,14 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
|
||||||
return assertionConsumerService;
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private <T> T build(QName elementName) {
|
private <T> T build(QName elementName) {
|
||||||
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(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.Extensions;
|
||||||
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
|
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
|
||||||
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
||||||
|
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
|
||||||
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
|
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
|
||||||
import org.opensaml.security.credential.UsageType;
|
import org.opensaml.security.credential.UsageType;
|
||||||
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
|
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
|
||||||
|
@ -105,6 +106,10 @@ class OpenSamlAssertingPartyMetadataConverter {
|
||||||
builder.assertingPartyDetails(
|
builder.assertingPartyDetails(
|
||||||
(party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())));
|
(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()) {
|
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
|
||||||
Saml2MessageBinding binding;
|
Saml2MessageBinding binding;
|
||||||
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
|
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
|
||||||
|
@ -119,10 +124,27 @@ class OpenSamlAssertingPartyMetadataConverter {
|
||||||
builder.assertingPartyDetails(
|
builder.assertingPartyDetails(
|
||||||
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
|
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
|
||||||
.singleSignOnServiceBinding(binding));
|
.singleSignOnServiceBinding(binding));
|
||||||
return builder;
|
break;
|
||||||
}
|
}
|
||||||
throw new Saml2Exception(
|
for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
|
||||||
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
|
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) {
|
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
|
||||||
|
|
|
@ -81,6 +81,12 @@ public final class RelyingPartyRegistration {
|
||||||
|
|
||||||
private final Saml2MessageBinding assertionConsumerServiceBinding;
|
private final Saml2MessageBinding assertionConsumerServiceBinding;
|
||||||
|
|
||||||
|
private final String singleLogoutServiceLocation;
|
||||||
|
|
||||||
|
private final String singleLogoutServiceResponseLocation;
|
||||||
|
|
||||||
|
private final Saml2MessageBinding singleLogoutServiceBinding;
|
||||||
|
|
||||||
private final ProviderDetails providerDetails;
|
private final ProviderDetails providerDetails;
|
||||||
|
|
||||||
private final List<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials;
|
private final List<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials;
|
||||||
|
@ -90,7 +96,9 @@ public final class RelyingPartyRegistration {
|
||||||
private final Collection<Saml2X509Credential> signingX509Credentials;
|
private final Collection<Saml2X509Credential> signingX509Credentials;
|
||||||
|
|
||||||
private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
|
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<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials,
|
||||||
Collection<Saml2X509Credential> decryptionX509Credentials,
|
Collection<Saml2X509Credential> decryptionX509Credentials,
|
||||||
Collection<Saml2X509Credential> signingX509Credentials) {
|
Collection<Saml2X509Credential> signingX509Credentials) {
|
||||||
|
@ -118,6 +126,9 @@ public final class RelyingPartyRegistration {
|
||||||
this.entityId = entityId;
|
this.entityId = entityId;
|
||||||
this.assertionConsumerServiceLocation = assertionConsumerServiceLocation;
|
this.assertionConsumerServiceLocation = assertionConsumerServiceLocation;
|
||||||
this.assertionConsumerServiceBinding = assertionConsumerServiceBinding;
|
this.assertionConsumerServiceBinding = assertionConsumerServiceBinding;
|
||||||
|
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||||
|
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||||
|
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||||
this.providerDetails = providerDetails;
|
this.providerDetails = providerDetails;
|
||||||
this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials));
|
this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials));
|
||||||
this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials));
|
this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials));
|
||||||
|
@ -177,6 +188,51 @@ public final class RelyingPartyRegistration {
|
||||||
return this.assertionConsumerServiceBinding;
|
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
|
* Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated
|
||||||
* with this relying party
|
* with this relying party
|
||||||
|
@ -364,6 +420,9 @@ public final class RelyingPartyRegistration {
|
||||||
.decryptionX509Credentials((c) -> c.addAll(registration.getDecryptionX509Credentials()))
|
.decryptionX509Credentials((c) -> c.addAll(registration.getDecryptionX509Credentials()))
|
||||||
.assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation())
|
.assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation())
|
||||||
.assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding())
|
.assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding())
|
||||||
|
.singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation())
|
||||||
|
.singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation())
|
||||||
|
.singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding())
|
||||||
.assertingPartyDetails((assertingParty) -> assertingParty
|
.assertingPartyDetails((assertingParty) -> assertingParty
|
||||||
.entityId(registration.getAssertingPartyDetails().getEntityId())
|
.entityId(registration.getAssertingPartyDetails().getEntityId())
|
||||||
.wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned())
|
.wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned())
|
||||||
|
@ -376,7 +435,13 @@ public final class RelyingPartyRegistration {
|
||||||
.singleSignOnServiceLocation(
|
.singleSignOnServiceLocation(
|
||||||
registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
|
registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
|
||||||
.singleSignOnServiceBinding(
|
.singleSignOnServiceBinding(
|
||||||
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()));
|
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
|
||||||
|
.singleLogoutServiceLocation(
|
||||||
|
registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
|
||||||
|
.singleLogoutServiceResponseLocation(
|
||||||
|
registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
|
||||||
|
.singleLogoutServiceBinding(
|
||||||
|
registration.getAssertingPartyDetails().getSingleLogoutServiceBinding()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Saml2X509Credential fromDeprecated(
|
private static Saml2X509Credential fromDeprecated(
|
||||||
|
@ -445,10 +510,17 @@ public final class RelyingPartyRegistration {
|
||||||
|
|
||||||
private final Saml2MessageBinding singleSignOnServiceBinding;
|
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,
|
private AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, List<String> signingAlgorithms,
|
||||||
Collection<Saml2X509Credential> verificationX509Credentials,
|
Collection<Saml2X509Credential> verificationX509Credentials,
|
||||||
Collection<Saml2X509Credential> encryptionX509Credentials, String singleSignOnServiceLocation,
|
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.hasText(entityId, "entityId cannot be null or empty");
|
||||||
Assert.notEmpty(signingAlgorithms, "signingAlgorithms cannot be empty");
|
Assert.notEmpty(signingAlgorithms, "signingAlgorithms cannot be empty");
|
||||||
Assert.notNull(verificationX509Credentials, "verificationX509Credentials cannot be null");
|
Assert.notNull(verificationX509Credentials, "verificationX509Credentials cannot be null");
|
||||||
|
@ -472,6 +544,9 @@ public final class RelyingPartyRegistration {
|
||||||
this.encryptionX509Credentials = encryptionX509Credentials;
|
this.encryptionX509Credentials = encryptionX509Credentials;
|
||||||
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
|
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
|
||||||
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
|
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
|
||||||
|
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||||
|
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||||
|
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -565,6 +640,48 @@ public final class RelyingPartyRegistration {
|
||||||
return this.singleSignOnServiceBinding;
|
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 {
|
public static final class Builder {
|
||||||
|
|
||||||
private String entityId;
|
private String entityId;
|
||||||
|
@ -581,6 +698,12 @@ public final class RelyingPartyRegistration {
|
||||||
|
|
||||||
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
|
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
|
||||||
|
|
||||||
|
private String singleLogoutServiceLocation;
|
||||||
|
|
||||||
|
private String singleLogoutServiceResponseLocation;
|
||||||
|
|
||||||
|
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.REDIRECT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the asserting party's <a href=
|
* Set the asserting party's <a href=
|
||||||
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/EntityNaming">EntityID</a>.
|
* "https://wiki.shibboleth.net/confluence/display/CONCEPT/EntityNaming">EntityID</a>.
|
||||||
|
@ -677,6 +800,55 @@ public final class RelyingPartyRegistration {
|
||||||
return this;
|
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
|
* Creates an immutable ProviderDetails object representing the configuration
|
||||||
* for an Identity Provider, IDP
|
* for an Identity Provider, IDP
|
||||||
|
@ -689,7 +861,9 @@ public final class RelyingPartyRegistration {
|
||||||
|
|
||||||
return new AssertingPartyDetails(this.entityId, this.wantAuthnRequestsSigned, signingAlgorithms,
|
return new AssertingPartyDetails(this.entityId, this.wantAuthnRequestsSigned, signingAlgorithms,
|
||||||
this.verificationX509Credentials, this.encryptionX509Credentials,
|
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 Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST;
|
||||||
|
|
||||||
|
private String singleLogoutServiceLocation;
|
||||||
|
|
||||||
|
private String singleLogoutServiceResponseLocation;
|
||||||
|
|
||||||
|
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.POST;
|
||||||
|
|
||||||
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
|
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
|
||||||
|
|
||||||
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new HashSet<>();
|
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new HashSet<>();
|
||||||
|
@ -933,6 +1113,54 @@ public final class RelyingPartyRegistration {
|
||||||
return this;
|
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
|
* Apply this {@link Consumer} to further configure the Asserting Party details
|
||||||
* @param assertingPartyDetails The {@link Consumer} to apply
|
* @param assertingPartyDetails The {@link Consumer} to apply
|
||||||
|
@ -1075,10 +1303,14 @@ public final class RelyingPartyRegistration {
|
||||||
for (Saml2X509Credential credential : this.providerDetails.assertingPartyDetailsBuilder.encryptionX509Credentials) {
|
for (Saml2X509Credential credential : this.providerDetails.assertingPartyDetailsBuilder.encryptionX509Credentials) {
|
||||||
this.credentials.add(toDeprecated(credential));
|
this.credentials.add(toDeprecated(credential));
|
||||||
}
|
}
|
||||||
|
if (this.singleLogoutServiceResponseLocation == null) {
|
||||||
|
this.singleLogoutServiceResponseLocation = this.singleLogoutServiceLocation;
|
||||||
|
}
|
||||||
return new RelyingPartyRegistration(this.registrationId, this.entityId,
|
return new RelyingPartyRegistration(this.registrationId, this.entityId,
|
||||||
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
|
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
|
||||||
this.providerDetails.build(), this.credentials, this.decryptionX509Credentials,
|
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||||
this.signingX509Credentials);
|
this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials,
|
||||||
|
this.decryptionX509Credentials, this.signingX509Credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||||
* @author Josh Cummings
|
* @author Josh Cummings
|
||||||
* @since 5.4
|
* @since 5.4
|
||||||
*/
|
*/
|
||||||
public final class DefaultRelyingPartyRegistrationResolver
|
public final class DefaultRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
|
||||||
implements Converter<HttpServletRequest, RelyingPartyRegistration>, RelyingPartyRegistrationResolver {
|
|
||||||
|
|
||||||
private static final char PATH_DELIMITER = '/';
|
private static final char PATH_DELIMITER = '/';
|
||||||
|
|
||||||
|
@ -56,14 +55,6 @@ public final class DefaultRelyingPartyRegistrationResolver
|
||||||
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
|
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public RelyingPartyRegistration convert(HttpServletRequest request) {
|
|
||||||
return resolve(request, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
@ -86,9 +77,14 @@ public final class DefaultRelyingPartyRegistrationResolver
|
||||||
String relyingPartyEntityId = templateResolver.apply(relyingPartyRegistration.getEntityId());
|
String relyingPartyEntityId = templateResolver.apply(relyingPartyRegistration.getEntityId());
|
||||||
String assertionConsumerServiceLocation = templateResolver
|
String assertionConsumerServiceLocation = templateResolver
|
||||||
.apply(relyingPartyRegistration.getAssertionConsumerServiceLocation());
|
.apply(relyingPartyRegistration.getAssertionConsumerServiceLocation());
|
||||||
|
String singleLogoutServiceLocation = templateResolver
|
||||||
|
.apply(relyingPartyRegistration.getSingleLogoutServiceLocation());
|
||||||
|
String singleLogoutServiceResponseLocation = templateResolver
|
||||||
|
.apply(relyingPartyRegistration.getSingleLogoutServiceResponseLocation());
|
||||||
return RelyingPartyRegistration.withRelyingPartyRegistration(relyingPartyRegistration)
|
return RelyingPartyRegistration.withRelyingPartyRegistration(relyingPartyRegistration)
|
||||||
.entityId(relyingPartyEntityId).assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
.entityId(relyingPartyEntityId).assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||||
.build();
|
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||||
|
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<String, String> templateResolver(String applicationUri, RelyingPartyRegistration relyingParty) {
|
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) {
|
private static String resolveUrlTemplate(String template, String baseUrl, RelyingPartyRegistration relyingParty) {
|
||||||
|
if (template == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
String entityId = relyingParty.getAssertingPartyDetails().getEntityId();
|
String entityId = relyingParty.getAssertingPartyDetails().getEntityId();
|
||||||
String registrationId = relyingParty.getRegistrationId();
|
String registrationId = relyingParty.getRegistrationId();
|
||||||
Map<String, String> uriVariables = new HashMap<>();
|
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.EncryptedAttribute;
|
||||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
import org.opensaml.saml.saml2.core.EncryptedID;
|
||||||
import org.opensaml.saml.saml2.core.Issuer;
|
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.NameID;
|
||||||
import org.opensaml.saml.saml2.core.Response;
|
import org.opensaml.saml.saml2.core.Response;
|
||||||
import org.opensaml.saml.saml2.core.Status;
|
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.SubjectConfirmationData;
|
||||||
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
||||||
import org.opensaml.saml.saml2.core.impl.AttributeStatementBuilder;
|
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.StatusBuilder;
|
||||||
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
|
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
|
||||||
import org.opensaml.saml.saml2.encryption.Encrypter;
|
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.OpenSamlInitializationService;
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||||
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||||
|
|
||||||
public final class TestOpenSamlObjects {
|
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 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";
|
private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
|
||||||
|
|
||||||
|
@ -221,7 +228,7 @@ public final class TestOpenSamlObjects {
|
||||||
return signable;
|
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);
|
return signed(signable, credential, entityId, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,6 +349,41 @@ public final class TestOpenSamlObjects {
|
||||||
return status;
|
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) {
|
static <T extends XMLObject> T build(QName qName) {
|
||||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,8 @@ public class OpenSamlMetadataResolverTests {
|
||||||
.contains("<md:KeyDescriptor use=\"encryption\">")
|
.contains("<md:KeyDescriptor use=\"encryption\">")
|
||||||
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
|
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
|
||||||
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
|
.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
|
@Test
|
||||||
|
@ -56,7 +57,8 @@ public class OpenSamlMetadataResolverTests {
|
||||||
.contains("WantAssertionsSigned=\"true\"").doesNotContain("<md:KeyDescriptor use=\"signing\">")
|
.contains("WantAssertionsSigned=\"true\"").doesNotContain("<md:KeyDescriptor use=\"signing\">")
|
||||||
.doesNotContain("<md:KeyDescriptor use=\"encryption\">")
|
.doesNotContain("<md:KeyDescriptor use=\"encryption\">")
|
||||||
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"")
|
.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";
|
String apEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
|
||||||
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
||||||
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||||
|
String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
|
||||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
|
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
|
||||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||||
.credentials((c) -> c.add(signingCredential))
|
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
|
||||||
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
|
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
|
||||||
.credentials((c) -> c.add(verificationCertificate));
|
.credentials((c) -> c.add(verificationCertificate));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RelyingPartyRegistration.Builder noCredentials() {
|
public static RelyingPartyRegistration.Builder noCredentials() {
|
||||||
return RelyingPartyRegistration.withRegistrationId("registration-id").entityId("rp-entity-id")
|
return RelyingPartyRegistration.withRegistrationId("registration-id").entityId("rp-entity-id")
|
||||||
.assertionConsumerServiceLocation("https://rp.example.org/acs").assertingPartyDetails((party) -> party
|
.singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request")
|
||||||
.entityId("ap-entity-id").singleSignOnServiceLocation("https://ap.example.org/sso"));
|
.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() {
|
public static RelyingPartyRegistration.Builder full() {
|
||||||
|
|
|
@ -52,6 +52,9 @@ public class DefaultRelyingPartyRegistrationResolverTests {
|
||||||
.isEqualTo("http://localhost/saml2/service-provider-metadata/" + this.registration.getRegistrationId());
|
.isEqualTo("http://localhost/saml2/service-provider-metadata/" + this.registration.getRegistrationId());
|
||||||
assertThat(registration.getAssertionConsumerServiceLocation())
|
assertThat(registration.getAssertionConsumerServiceLocation())
|
||||||
.isEqualTo("http://localhost/login/saml2/sso/" + this.registration.getRegistrationId());
|
.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
|
@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