parent
6488295cad
commit
c63d618b26
|
@ -1618,35 +1618,281 @@ filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
|
|||
[[servlet-saml2login-logout]]
|
||||
=== Performing Single Logout
|
||||
|
||||
Spring Security does not yet support single logout.
|
||||
Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
|
||||
|
||||
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
|
||||
Briefly, there are two use cases Spring Security supports:
|
||||
|
||||
* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party.
|
||||
Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond
|
||||
* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party.
|
||||
Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party.
|
||||
|
||||
[NOTE]
|
||||
In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot.
|
||||
Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser.
|
||||
|
||||
=== Minimal Configuration for Single Logout
|
||||
|
||||
To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things:
|
||||
|
||||
* First, the asserting party must support SAML 2.0 Single Logout
|
||||
* Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint
|
||||
* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
|
||||
|
||||
==== RP-Initiated Single Logout
|
||||
|
||||
Given those, then for RP-initiated Single Logout, you can begin from the initial minimal example and add the following configuration:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Value("${private.key}") RSAPrivateKey key;
|
||||
@Value("${public.certificate}") X509Certificate certificate;
|
||||
|
||||
@Bean
|
||||
RelyingPartyRegistrationRepository registrations() {
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
||||
.fromMetadataLocation("https://ap.example.org/metadata")
|
||||
.registrationId("id")
|
||||
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
|
||||
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
|
||||
.build();
|
||||
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
|
||||
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
LogoutHandler logoutResponseHandler = logoutResponseHandler(registrationResolver);
|
||||
LogoutSuccessHandler logoutRequestSuccessHandler = logoutRequestSuccessHandler(registrationResolver);
|
||||
|
||||
http
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.saml2Login(withDefaults())
|
||||
.logout((logout) -> logout
|
||||
.logoutUrl("/saml2/logout")
|
||||
.logoutSuccessHandler(successHandler))
|
||||
.addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private LogoutSuccessHandler logoutRequestSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
|
||||
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(registrationResolver);
|
||||
return new Saml2LogoutRequestSuccessHandler(logoutRequestResolver);
|
||||
}
|
||||
|
||||
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
|
||||
return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
|
||||
}
|
||||
----
|
||||
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
|
||||
<2> - Second, supply a `LogoutSuccessHandler` for initiating Single Logout, sending a `saml2:LogoutRequest` to the asserting party
|
||||
<3> - Third, supply the `LogoutHandler` s needed to handle the `saml2:LogoutResponse` s sent from the asserting party.
|
||||
|
||||
==== Runtime Expectations for RP-Initiated
|
||||
|
||||
Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
|
||||
Your application will then do the following:
|
||||
|
||||
1. Logout the user and invalidate the session
|
||||
2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the currently logged-in user.
|
||||
3. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
|
||||
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
|
||||
5. Redirect to any configured successful logout endpoint
|
||||
|
||||
[TIP]
|
||||
If your asserting party does not send `<saml2:LogoutResponse>` s when logout is complete, the asserting party can still send a `POST /saml2/logout` and then there is no need to configure the `Saml2LogoutResponseHandler`.
|
||||
|
||||
==== AP-Initiated Single Logout
|
||||
|
||||
Instead of RP-initiated Single Logout, you can again begin from the initial minimal example and add the following configuration to achieve AP-initiated Single Logout:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Value("${private.key}") RSAPrivateKey key;
|
||||
@Value("${public.certificate}") X509Certificate certificate;
|
||||
|
||||
@Bean
|
||||
RelyingPartyRegistrationRepository registrations() {
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
||||
.fromMetadataLocation("https://ap.example.org/metadata")
|
||||
.registrationId("id")
|
||||
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
|
||||
.build();
|
||||
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
|
||||
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
|
||||
LogoutHandler logoutRequestHandler = logoutRequestHandler(registrationResolver);
|
||||
LogoutSuccessHandler logoutResponseSuccessHandler = logoutResponseSuccessHandler(registrationResolver);
|
||||
|
||||
http
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.saml2Login(withDefaults())
|
||||
.addFilterBefore(new Saml2LogoutRequestFilter(logoutResponseSuccessHandler, logoutRequestHandler), CsrfFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
|
||||
return new CompositeLogoutHandler(
|
||||
new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver),
|
||||
new SecurityContextLogoutHandler(),
|
||||
new LogoutSuccessEventPublishingLogoutHandler());
|
||||
}
|
||||
|
||||
private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
|
||||
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(registrationResolver);
|
||||
return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver);
|
||||
}
|
||||
----
|
||||
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
|
||||
<2> - Second, supply the `LogoutHandler` needed to handle the `saml2:LogoutRequest` s sent from the asserting party.
|
||||
<3> - Third, supply a `LogoutSuccessHandler` for completing Single Logout, sending a `saml2:LogoutResponse` to the asserting party
|
||||
|
||||
==== Runtime Expectations for AP-Initiated
|
||||
|
||||
Given the above configuration, an asserting party can send a `POST /logout/saml2` to your application that includes a `<saml2:LogoutRequest>`
|
||||
Also, your application can participate in an AP-initated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
|
||||
|
||||
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
|
||||
2. Logout the user and invalidate the session
|
||||
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
|
||||
4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
|
||||
|
||||
[TIP]
|
||||
If your asserting party does not expect you do send a `<saml2:LogoutResponse>` s when logout is complete, you may not need to configure a `LogoutSuccessHandler`
|
||||
|
||||
[NOTE]
|
||||
In the event that you need to support both logout flows, you can combine the above to configurations.
|
||||
|
||||
=== Configuring Logout Endpoints
|
||||
|
||||
There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
|
||||
* `/logout` - the endpoint for initiating single logout with an asserting party
|
||||
* `/logout/saml2/slo` - the endpoint for receiving logout requests or responses from an asserting party
|
||||
|
||||
Because the user is already logged in, the `registrationId` is already known.
|
||||
For this reason, `+{registrationId}+` is not part of these URLs by default.
|
||||
|
||||
These URLs are customizable in the DSL.
|
||||
|
||||
For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`.
|
||||
To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
|
||||
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
|
||||
http
|
||||
// ...
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessHandler(myCustomSuccessHandler())
|
||||
.logoutRequestMatcher(myRequestMatcher())
|
||||
)
|
||||
.addFilterBefore(filter, CsrfFilter.class);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
=== Customizing `<saml2:LogoutRequest>` Resolution
|
||||
|
||||
It's common to need to set other values in the `<saml2:LogoutRequest>` than the defaults that Spring Security provides.
|
||||
|
||||
By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
|
||||
|
||||
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation`
|
||||
* The `ID` attribute - a GUID
|
||||
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
|
||||
* The `<NameID>` element - from `Authentication#getName`
|
||||
|
||||
To add other values, you can use delegation, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
http {
|
||||
logout {
|
||||
// ...
|
||||
logoutSuccessHandler = myCustomSuccessHandler()
|
||||
logoutRequestMatcher = myRequestMatcher()
|
||||
OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver);
|
||||
return (request, response, authentication) -> {
|
||||
OpenSamlLogoutRequestBuilder builder = delegate.resolveLogoutRequest(request, response, authentication); <1>
|
||||
builder.name(((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute")); <2>
|
||||
builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now()));
|
||||
return builder.logoutRequest(); <3>
|
||||
};
|
||||
----
|
||||
<1> - Spring Security applies default values to a `<saml2:LogoutRequest>`
|
||||
<2> - Your application specifies customizations
|
||||
<3> - You complete the invocation by calling `request()`
|
||||
|
||||
[NOTE]
|
||||
Support for OpenSAML 4 is coming.
|
||||
In anticipation of that, `OpenSamlLogoutRequestResolver` does not add an `IssueInstant`.
|
||||
Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it.
|
||||
|
||||
=== Customizing `<saml2:LogoutResponse>` Resolution
|
||||
|
||||
It's common to need to set other values in the `<saml2:LogoutResponse>` than the defaults that Spring Security provides.
|
||||
|
||||
By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
|
||||
|
||||
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation`
|
||||
* The `ID` attribute - a GUID
|
||||
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
|
||||
* The `<Status>` element - `SUCCESS`
|
||||
|
||||
To add other values, you can use delegation, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver);
|
||||
return (request, response, authentication) -> {
|
||||
OpenSamlLogoutResponseBuilder builder = delegate.resolveLogoutResponse(request, response, authentication); <1>
|
||||
if (checkOtherPrevailingConditions()) {
|
||||
builder.status(StatusCode.PARTIAL_LOGOUT); <2>
|
||||
}
|
||||
builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now()));
|
||||
return builder.logoutResponse(); <3>
|
||||
};
|
||||
----
|
||||
<1> - Spring Security applies default values to a `<saml2:LogoutResponse>`
|
||||
<2> - Your application specifies customizations
|
||||
<3> - You complete the invocation by calling `response()`
|
||||
|
||||
[NOTE]
|
||||
Support for OpenSAML 4 is coming.
|
||||
In anticipation of that, `OpenSamlLogoutResponseResolver` does not add an `IssueInstant`.
|
||||
Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it.
|
||||
|
||||
=== Customizing `<saml2:LogoutRequest>` Validation
|
||||
|
||||
To customize validation, you can implement your own `LogoutHandler`.
|
||||
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
|
||||
OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver);
|
||||
return (request, response, authentication) -> {
|
||||
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name
|
||||
LogoutRequest logoutRequest = // ... parse using OpenSAML
|
||||
// perform custom validation
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The success handler will send logout requests to the asserting party.
|
||||
=== Customizing `<saml2:LogoutResponse>` Validation
|
||||
|
||||
The request matcher will detect logout requests from the asserting party.
|
||||
To customize validation, you can implement your own `LogoutHandler`.
|
||||
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
|
||||
OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver);
|
||||
return (request, response, authentication) -> {
|
||||
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status
|
||||
LogoutResponse logoutResponse = // ... parse using OpenSAML
|
||||
// perform custom validation
|
||||
}
|
||||
}
|
||||
----
|
||||
|
|
|
@ -37,6 +37,13 @@ public interface Saml2ErrorCodes {
|
|||
*/
|
||||
String MALFORMED_RESPONSE_DATA = "malformed_response_data";
|
||||
|
||||
/**
|
||||
* Request is invalid in a general way.
|
||||
*
|
||||
* @since 5.6
|
||||
*/
|
||||
String INVALID_REQUEST = "invalid_request";
|
||||
|
||||
/**
|
||||
* Response is invalid in a general way.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import net.shibboleth.utilities.java.support.xml.ParserPool;
|
||||
import org.opensaml.core.config.ConfigurationService;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlVerificationUtils.VerifierPartial;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutRequestValidator} that authenticates a SAML 2.0 Logout Requests
|
||||
* received from a SAML 2.0 Asserting Party using OpenSAML.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
|
||||
|
||||
static {
|
||||
OpenSamlInitializationService.initialize();
|
||||
}
|
||||
|
||||
private final ParserPool parserPool;
|
||||
|
||||
private final LogoutRequestUnmarshaller unmarshaller;
|
||||
|
||||
/**
|
||||
* Constructs a {@link OpenSamlLogoutRequestValidator}
|
||||
*/
|
||||
public OpenSamlLogoutRequestValidator() {
|
||||
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
|
||||
this.parserPool = registry.getParserPool();
|
||||
this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
|
||||
.getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) {
|
||||
Saml2LogoutRequest request = parameters.getLogoutRequest();
|
||||
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
|
||||
Authentication authentication = parameters.getAuthentication();
|
||||
byte[] b = Saml2Utils.samlDecode(request.getSamlRequest());
|
||||
LogoutRequest logoutRequest = parse(inflateIfRequired(request, b));
|
||||
return Saml2LogoutValidatorResult.withErrors().errors(verifySignature(request, logoutRequest, registration))
|
||||
.errors(validateRequest(logoutRequest, registration, authentication)).build();
|
||||
}
|
||||
|
||||
private String inflateIfRequired(Saml2LogoutRequest request, byte[] b) {
|
||||
if (request.getBinding() == Saml2MessageBinding.REDIRECT) {
|
||||
return Saml2Utils.samlInflate(b);
|
||||
}
|
||||
return new String(b, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LogoutRequest parse(String request) throws Saml2Exception {
|
||||
try {
|
||||
Document document = this.parserPool
|
||||
.parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8)));
|
||||
Element element = document.getDocumentElement();
|
||||
return (LogoutRequest) this.unmarshaller.unmarshall(element);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception("Failed to deserialize LogoutRequest", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest,
|
||||
RelyingPartyRegistration registration) {
|
||||
return (errors) -> {
|
||||
VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(logoutRequest, registration);
|
||||
if (logoutRequest.isSigned()) {
|
||||
errors.addAll(partial.post(logoutRequest.getSignature()));
|
||||
}
|
||||
else {
|
||||
errors.addAll(partial.redirect(request));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateRequest(LogoutRequest request,
|
||||
RelyingPartyRegistration registration, Authentication authentication) {
|
||||
return (errors) -> {
|
||||
validateIssuer(request, registration).accept(errors);
|
||||
validateDestination(request, registration).accept(errors);
|
||||
validateName(request, authentication).accept(errors);
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutRequest request,
|
||||
RelyingPartyRegistration registration) {
|
||||
return (errors) -> {
|
||||
if (request.getIssuer() == null) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
|
||||
return;
|
||||
}
|
||||
String issuer = request.getIssuer().getValue();
|
||||
if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) {
|
||||
errors.add(
|
||||
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateDestination(LogoutRequest request,
|
||||
RelyingPartyRegistration registration) {
|
||||
return (errors) -> {
|
||||
if (request.getDestination() == null) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
|
||||
"Failed to find destination in LogoutResponse"));
|
||||
return;
|
||||
}
|
||||
String destination = request.getDestination();
|
||||
if (!destination.equals(registration.getSingleLogoutServiceLocation())) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
|
||||
"Failed to match destination to configured destination"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateName(LogoutRequest request, Authentication authentication) {
|
||||
return (errors) -> {
|
||||
if (authentication == null) {
|
||||
return;
|
||||
}
|
||||
NameID nameId = request.getNameID();
|
||||
if (nameId == null) {
|
||||
errors.add(
|
||||
new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest"));
|
||||
return;
|
||||
}
|
||||
String name = nameId.getValue();
|
||||
if (!name.equals(authentication.getName())) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST,
|
||||
"Failed to match subject in LogoutRequest with currently logged in user"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import net.shibboleth.utilities.java.support.xml.ParserPool;
|
||||
import org.opensaml.core.config.ConfigurationService;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutResponseUnmarshaller;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlVerificationUtils.VerifierPartial;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutResponseValidator} that authenticates a SAML 2.0 Logout Responses
|
||||
* received from a SAML 2.0 Asserting Party using OpenSAML.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public class OpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
|
||||
|
||||
static {
|
||||
OpenSamlInitializationService.initialize();
|
||||
}
|
||||
|
||||
private final ParserPool parserPool;
|
||||
|
||||
private final LogoutResponseUnmarshaller unmarshaller;
|
||||
|
||||
/**
|
||||
* Constructs a {@link OpenSamlLogoutRequestValidator}
|
||||
*/
|
||||
public OpenSamlLogoutResponseValidator() {
|
||||
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
|
||||
this.parserPool = registry.getParserPool();
|
||||
this.unmarshaller = (LogoutResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
|
||||
.getUnmarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) {
|
||||
Saml2LogoutResponse response = parameters.getLogoutResponse();
|
||||
Saml2LogoutRequest request = parameters.getLogoutRequest();
|
||||
RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration();
|
||||
byte[] b = Saml2Utils.samlDecode(response.getSamlResponse());
|
||||
LogoutResponse logoutResponse = parse(inflateIfRequired(response, b));
|
||||
return Saml2LogoutValidatorResult.withErrors().errors(verifySignature(response, logoutResponse, registration))
|
||||
.errors(validateRequest(logoutResponse, registration))
|
||||
.errors(validateLogoutRequest(logoutResponse, request.getId())).build();
|
||||
}
|
||||
|
||||
private String inflateIfRequired(Saml2LogoutResponse response, byte[] b) {
|
||||
if (response.getBinding() == Saml2MessageBinding.REDIRECT) {
|
||||
return Saml2Utils.samlInflate(b);
|
||||
}
|
||||
return new String(b, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LogoutResponse parse(String response) throws Saml2Exception {
|
||||
try {
|
||||
Document document = this.parserPool
|
||||
.parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)));
|
||||
Element element = document.getDocumentElement();
|
||||
return (LogoutResponse) this.unmarshaller.unmarshall(element);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception("Failed to deserialize LogoutResponse", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> verifySignature(Saml2LogoutResponse response,
|
||||
LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
|
||||
return (errors) -> {
|
||||
VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(logoutResponse, registration);
|
||||
if (logoutResponse.isSigned()) {
|
||||
errors.addAll(partial.post(logoutResponse.getSignature()));
|
||||
}
|
||||
else {
|
||||
errors.addAll(partial.redirect(response));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateRequest(LogoutResponse response,
|
||||
RelyingPartyRegistration registration) {
|
||||
return (errors) -> {
|
||||
validateIssuer(response, registration).accept(errors);
|
||||
validateDestination(response, registration).accept(errors);
|
||||
validateStatus(response).accept(errors);
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateIssuer(LogoutResponse response,
|
||||
RelyingPartyRegistration registration) {
|
||||
return (errors) -> {
|
||||
if (response.getIssuer() == null) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse"));
|
||||
return;
|
||||
}
|
||||
String issuer = response.getIssuer().getValue();
|
||||
if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) {
|
||||
errors.add(
|
||||
new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateDestination(LogoutResponse response,
|
||||
RelyingPartyRegistration registration) {
|
||||
return (errors) -> {
|
||||
if (response.getDestination() == null) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
|
||||
"Failed to find destination in LogoutResponse"));
|
||||
return;
|
||||
}
|
||||
String destination = response.getDestination();
|
||||
if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION,
|
||||
"Failed to match destination to configured destination"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateStatus(LogoutResponse response) {
|
||||
return (errors) -> {
|
||||
if (response.getStatus() == null) {
|
||||
return;
|
||||
}
|
||||
if (response.getStatus().getStatusCode() == null) {
|
||||
return;
|
||||
}
|
||||
if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) {
|
||||
return;
|
||||
}
|
||||
if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) {
|
||||
return;
|
||||
}
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed"));
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Collection<Saml2Error>> validateLogoutRequest(LogoutResponse response, String id) {
|
||||
return (errors) -> {
|
||||
if (response.getInResponseTo() == null) {
|
||||
return;
|
||||
}
|
||||
if (response.getInResponseTo().equals(id)) {
|
||||
return;
|
||||
}
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE,
|
||||
"LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest"));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
|
||||
import org.opensaml.core.criterion.EntityIdCriterion;
|
||||
import org.opensaml.saml.common.xml.SAMLConstants;
|
||||
import org.opensaml.saml.criterion.ProtocolCriterion;
|
||||
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.RequestAbstractType;
|
||||
import org.opensaml.saml.saml2.core.StatusResponseType;
|
||||
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
|
||||
import org.opensaml.security.credential.Credential;
|
||||
import org.opensaml.security.credential.CredentialResolver;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion;
|
||||
import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion;
|
||||
import org.opensaml.security.credential.impl.CollectionCredentialResolver;
|
||||
import org.opensaml.security.criteria.UsageCriterion;
|
||||
import org.opensaml.security.x509.BasicX509Credential;
|
||||
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
|
||||
import org.opensaml.xmlsec.signature.Signature;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
||||
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
|
||||
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for verifying SAML component signatures with OpenSAML
|
||||
*
|
||||
* For internal use only.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
|
||||
final class OpenSamlVerificationUtils {
|
||||
|
||||
static VerifierPartial verifySignature(StatusResponseType object, RelyingPartyRegistration registration) {
|
||||
return new VerifierPartial(object, registration);
|
||||
}
|
||||
|
||||
static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyRegistration registration) {
|
||||
return new VerifierPartial(object, registration);
|
||||
}
|
||||
|
||||
static class VerifierPartial {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final CriteriaSet criteria;
|
||||
|
||||
private final SignatureTrustEngine trustEngine;
|
||||
|
||||
VerifierPartial(StatusResponseType object, RelyingPartyRegistration registration) {
|
||||
this.id = object.getID();
|
||||
this.criteria = verificationCriteria(object.getIssuer());
|
||||
this.trustEngine = trustEngine(registration);
|
||||
}
|
||||
|
||||
VerifierPartial(RequestAbstractType object, RelyingPartyRegistration registration) {
|
||||
this.id = object.getID();
|
||||
this.criteria = verificationCriteria(object.getIssuer());
|
||||
this.trustEngine = trustEngine(registration);
|
||||
}
|
||||
|
||||
Collection<Saml2Error> redirect(Saml2LogoutRequest request) {
|
||||
return redirect(new RedirectSignature(request));
|
||||
}
|
||||
|
||||
Collection<Saml2Error> redirect(Saml2LogoutResponse response) {
|
||||
return redirect(new RedirectSignature(response));
|
||||
}
|
||||
|
||||
Collection<Saml2Error> redirect(RedirectSignature signature) {
|
||||
if (signature.getAlgorithm() == null) {
|
||||
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Missing signature algorithm for object [" + this.id + "]"));
|
||||
}
|
||||
if (!signature.hasSignature()) {
|
||||
return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Missing signature for object [" + this.id + "]"));
|
||||
}
|
||||
Collection<Saml2Error> errors = new ArrayList<>();
|
||||
String algorithmUri = signature.getAlgorithm();
|
||||
try {
|
||||
if (!this.trustEngine.validate(signature.getSignature(), signature.getContent(), algorithmUri,
|
||||
this.criteria, null)) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Invalid signature for object [" + this.id + "]"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Invalid signature for object [" + this.id + "]: "));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
Collection<Saml2Error> post(Signature signature) {
|
||||
Collection<Saml2Error> errors = new ArrayList<>();
|
||||
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
|
||||
try {
|
||||
profileValidator.validate(signature);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Invalid signature for object [" + this.id + "]: "));
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.trustEngine.validate(signature, this.criteria)) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Invalid signature for object [" + this.id + "]"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
|
||||
"Invalid signature for object [" + this.id + "]: "));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private CriteriaSet verificationCriteria(Issuer issuer) {
|
||||
CriteriaSet criteria = new CriteriaSet();
|
||||
criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())));
|
||||
criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)));
|
||||
criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
|
||||
return criteria;
|
||||
}
|
||||
|
||||
private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) {
|
||||
Set<Credential> credentials = new HashSet<>();
|
||||
Collection<Saml2X509Credential> keys = registration.getAssertingPartyDetails()
|
||||
.getVerificationX509Credentials();
|
||||
for (Saml2X509Credential key : keys) {
|
||||
BasicX509Credential cred = new BasicX509Credential(key.getCertificate());
|
||||
cred.setUsageType(UsageType.SIGNING);
|
||||
cred.setEntityId(registration.getAssertingPartyDetails().getEntityId());
|
||||
credentials.add(cred);
|
||||
}
|
||||
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
|
||||
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
|
||||
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
|
||||
}
|
||||
|
||||
private static class RedirectSignature {
|
||||
|
||||
private final String algorithm;
|
||||
|
||||
private final byte[] signature;
|
||||
|
||||
private final byte[] content;
|
||||
|
||||
RedirectSignature(Saml2LogoutRequest request) {
|
||||
this.algorithm = request.getParameter("SigAlg");
|
||||
if (request.getParameter("Signature") != null) {
|
||||
this.signature = Saml2Utils.samlDecode(request.getParameter("Signature"));
|
||||
}
|
||||
else {
|
||||
this.signature = null;
|
||||
}
|
||||
this.content = content(request.getSamlRequest(), "SAMLRequest", request.getRelayState(),
|
||||
request.getParameter("SigAlg"));
|
||||
}
|
||||
|
||||
RedirectSignature(Saml2LogoutResponse response) {
|
||||
this.algorithm = response.getParameter("SigAlg");
|
||||
if (response.getParameter("Signature") != null) {
|
||||
this.signature = Saml2Utils.samlDecode(response.getParameter("Signature"));
|
||||
}
|
||||
else {
|
||||
this.signature = null;
|
||||
}
|
||||
this.content = content(response.getSamlResponse(), "SAMLResponse", response.getRelayState(),
|
||||
response.getParameter("SigAlg"));
|
||||
}
|
||||
|
||||
static byte[] content(String samlObject, String objectParameterName, String relayState, String algorithm) {
|
||||
if (relayState != null) {
|
||||
return String
|
||||
.format("%s=%s&RelayState=%s&SigAlg=%s", objectParameterName,
|
||||
UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1),
|
||||
UriUtils.encode(relayState, StandardCharsets.ISO_8859_1),
|
||||
UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1))
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
else {
|
||||
return String
|
||||
.format("%s=%s&SigAlg=%s", objectParameterName,
|
||||
UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1),
|
||||
UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1))
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
String getAlgorithm() {
|
||||
return this.algorithm;
|
||||
}
|
||||
|
||||
byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
boolean hasSignature() {
|
||||
return this.signature != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private OpenSamlVerificationUtils() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
|
||||
|
||||
/**
|
||||
* A class that represents a signed and serialized SAML 2.0 Logout Request
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class Saml2LogoutRequest implements Serializable {
|
||||
|
||||
private final String location;
|
||||
|
||||
private final Saml2MessageBinding binding;
|
||||
|
||||
private final Map<String, String> parameters;
|
||||
|
||||
private final String id;
|
||||
|
||||
private final String relyingPartyRegistrationId;
|
||||
|
||||
private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map<String, String> parameters, String id,
|
||||
String relyingPartyRegistrationId) {
|
||||
this.location = location;
|
||||
this.binding = binding;
|
||||
this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters));
|
||||
this.id = id;
|
||||
this.relyingPartyRegistrationId = relyingPartyRegistrationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique identifier for this Logout Request
|
||||
* @return the Logout Request identifier
|
||||
*/
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location of the asserting party's <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
|
||||
* @return the SingleLogoutService location
|
||||
*/
|
||||
public String getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the binding for the asserting party's <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
|
||||
* @return the SingleLogoutService binding
|
||||
*/
|
||||
public Saml2MessageBinding getBinding() {
|
||||
return this.binding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signed and serialized <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} parameters, a short-hand for <code>
|
||||
* getParameters().get(name)
|
||||
* </code>
|
||||
*
|
||||
* Useful when specifying additional query parameters for the Logout Request
|
||||
* @param name the parameter's name
|
||||
* @return the parameter's value
|
||||
*/
|
||||
public String getParameter(String name) {
|
||||
return this.parameters.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all parameters
|
||||
*
|
||||
* Useful when specifying additional query parameters for the Logout Request
|
||||
* @return the Logout Request query parameters
|
||||
*/
|
||||
public Map<String, String> getParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* The identifier for the {@link RelyingPartyRegistration} associated with this Logout
|
||||
* Request
|
||||
* @return the {@link RelyingPartyRegistration} id
|
||||
*/
|
||||
public String getRelyingPartyRegistrationId() {
|
||||
return this.relyingPartyRegistrationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Builder} instance from this {@link RelyingPartyRegistration}
|
||||
*
|
||||
* Specifically, this will pull the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
|
||||
* location and binding from the {@link RelyingPartyRegistration}
|
||||
* @param registration the {@link RelyingPartyRegistration} to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) {
|
||||
return new Builder(registration);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private String location;
|
||||
|
||||
private Saml2MessageBinding binding;
|
||||
|
||||
private Map<String, String> parameters = new HashMap<>();
|
||||
|
||||
private String id;
|
||||
|
||||
private Builder(RelyingPartyRegistration registration) {
|
||||
this.registration = registration;
|
||||
this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceLocation();
|
||||
this.binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this signed and serialized and Base64-encoded <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 SAML 2.0 Message Binding
|
||||
*
|
||||
* By default, the asserting party's configured binding is used
|
||||
* @param binding the SAML 2.0 Message Binding to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder binding(Saml2MessageBinding binding) {
|
||||
this.binding = binding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this location for the SAML 2.0 logout endpoint
|
||||
*
|
||||
* By default, the asserting party's endpoint is used
|
||||
* @param location the SAML 2.0 location to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder location(String location) {
|
||||
this.location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this value for the relay state when sending the Logout Request to the
|
||||
* asserting party
|
||||
*
|
||||
* It should not be URL-encoded as this will be done when the request is sent
|
||||
* @param relayState the relay state
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder relayState(String relayState) {
|
||||
this.parameters.put("RelayState", relayState);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the unique id used in the {@link #samlRequest}
|
||||
* @param id the Logout Request id
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder id(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Consumer} to modify the set of query parameters
|
||||
*
|
||||
* No parameter should be URL-encoded as this will be done when the request is
|
||||
* sent
|
||||
* @param parametersConsumer the {@link Consumer}
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder parameters(Consumer<Map<String, String>> parametersConsumer) {
|
||||
parametersConsumer.accept(this.parameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link Saml2LogoutRequest}
|
||||
* @return a constructed {@link Saml2LogoutRequest}
|
||||
*/
|
||||
public Saml2LogoutRequest build() {
|
||||
return new Saml2LogoutRequest(this.location, this.binding, this.parameters, this.id,
|
||||
this.registration.getRegistrationId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
/**
|
||||
* Validates SAML 2.0 Logout Requests
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public interface Saml2LogoutRequestValidator {
|
||||
|
||||
/**
|
||||
* Authenticates the SAML 2.0 Logout Request received from the SAML 2.0 Asserting
|
||||
* Party.
|
||||
*
|
||||
* By default, verifies the signature, validates the issuer, destination, and user
|
||||
* identifier.
|
||||
* @param parameters the {@link Saml2LogoutRequestValidatorParameters} needed
|
||||
* @return the authentication result
|
||||
*/
|
||||
Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters);
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
/**
|
||||
* A holder of the parameters needed to invoke {@link Saml2LogoutRequestValidator}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public class Saml2LogoutRequestValidatorParameters {
|
||||
|
||||
private final Saml2LogoutRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
/**
|
||||
* Construct a {@link Saml2LogoutRequestValidatorParameters}
|
||||
* @param request the SAML 2.0 Logout Request received from the asserting party
|
||||
* @param registration the associated {@link RelyingPartyRegistration}
|
||||
* @param authentication the current user
|
||||
*/
|
||||
public Saml2LogoutRequestValidatorParameters(Saml2LogoutRequest request, RelyingPartyRegistration registration,
|
||||
Authentication authentication) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* The SAML 2.0 Logout Request sent by the asserting party
|
||||
* @return the logout request
|
||||
*/
|
||||
public Saml2LogoutRequest getLogoutRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link RelyingPartyRegistration} representing this relying party
|
||||
* @return the relying party
|
||||
*/
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current {@link Authentication}
|
||||
* @return the authenticated user
|
||||
*/
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
|
||||
|
||||
/**
|
||||
* A class that represents a signed and serialized SAML 2.0 Logout Response
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class Saml2LogoutResponse {
|
||||
|
||||
private final String location;
|
||||
|
||||
private final Saml2MessageBinding binding;
|
||||
|
||||
private final Map<String, String> parameters;
|
||||
|
||||
private Saml2LogoutResponse(String location, Saml2MessageBinding binding, Map<String, String> parameters) {
|
||||
this.location = location;
|
||||
this.binding = binding;
|
||||
this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response location of the asserting party's <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
|
||||
* @return the SingleLogoutService response location
|
||||
*/
|
||||
public String getResponseLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the binding for the asserting party's <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
|
||||
* @return the SingleLogoutService binding
|
||||
*/
|
||||
public Saml2MessageBinding getBinding() {
|
||||
return this.binding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signed and serialized <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 the Logout Response query parameters
|
||||
*/
|
||||
public Map<String, String> getParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Builder} instance from this {@link RelyingPartyRegistration}
|
||||
*
|
||||
* Specifically, this will pull the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService</a>
|
||||
* response location and binding from the {@link RelyingPartyRegistration}
|
||||
* @param registration the {@link RelyingPartyRegistration} to use
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) {
|
||||
return new Builder(registration);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String location;
|
||||
|
||||
private Saml2MessageBinding binding;
|
||||
|
||||
private Map<String, String> parameters = new HashMap<>();
|
||||
|
||||
private Builder(RelyingPartyRegistration registration) {
|
||||
this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation();
|
||||
this.binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this signed and serialized and Base64-encoded <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 SAML 2.0 Message Binding
|
||||
*
|
||||
* By default, the asserting party's configured binding is used
|
||||
* @param binding the SAML 2.0 Message Binding to use
|
||||
* @return the {@link Saml2LogoutRequest.Builder} for further configurations
|
||||
*/
|
||||
public Builder binding(Saml2MessageBinding binding) {
|
||||
this.binding = binding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this location for the SAML 2.0 logout endpoint
|
||||
*
|
||||
* By default, the asserting party's endpoint is used
|
||||
* @param location the SAML 2.0 location to use
|
||||
* @return the {@link Saml2LogoutRequest.Builder} for further configurations
|
||||
*/
|
||||
public Builder location(String location) {
|
||||
this.location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this value for the relay state when sending the Logout Request to the
|
||||
* asserting party
|
||||
*
|
||||
* It should not be URL-encoded as this will be done when the response is sent
|
||||
* @param relayState the relay state
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder relayState(String relayState) {
|
||||
this.parameters.put("RelayState", relayState);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Consumer} to modify the set of query parameters
|
||||
*
|
||||
* No parameter should be URL-encoded as this will be done when the response is
|
||||
* sent, though any signature specified should be Base64-encoded
|
||||
* @param parametersConsumer the {@link Consumer}
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder parameters(Consumer<Map<String, String>> parametersConsumer) {
|
||||
parametersConsumer.accept(this.parameters);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link Saml2LogoutResponse}
|
||||
* @return a constructed {@link Saml2LogoutResponse}
|
||||
*/
|
||||
public Saml2LogoutResponse build() {
|
||||
return new Saml2LogoutResponse(this.location, this.binding, this.parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
/**
|
||||
* Validates SAML 2.0 Logout Responses
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public interface Saml2LogoutResponseValidator {
|
||||
|
||||
/**
|
||||
* Authenticates the SAML 2.0 Logout Response received from the SAML 2.0 Asserting
|
||||
* Party.
|
||||
*
|
||||
* By default, verifies the signature, validates the issuer, destination, and status.
|
||||
* It also ensures that it aligns with the given logout request.
|
||||
* @param parameters the {@link Saml2LogoutResponseValidatorParameters} needed
|
||||
* @return the authentication result
|
||||
*/
|
||||
Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters);
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
/**
|
||||
* A holder of the parameters needed to invoke {@link Saml2LogoutResponseValidator}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public class Saml2LogoutResponseValidatorParameters {
|
||||
|
||||
private final Saml2LogoutResponse response;
|
||||
|
||||
private final Saml2LogoutRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
/**
|
||||
* Construct a {@link Saml2LogoutRequestValidatorParameters}
|
||||
* @param response the SAML 2.0 Logout Response received from the asserting party
|
||||
* @param request the SAML 2.0 Logout Request send by this application
|
||||
* @param registration the associated {@link RelyingPartyRegistration}
|
||||
*/
|
||||
public Saml2LogoutResponseValidatorParameters(Saml2LogoutResponse response, Saml2LogoutRequest request,
|
||||
RelyingPartyRegistration registration) {
|
||||
this.response = response;
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
}
|
||||
|
||||
/**
|
||||
* The SAML 2.0 Logout Response received from the asserting party
|
||||
* @return the logout response
|
||||
*/
|
||||
public Saml2LogoutResponse getLogoutResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The SAML 2.0 Logout Request sent by this application
|
||||
* @return the logout request
|
||||
*/
|
||||
public Saml2LogoutRequest getLogoutRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link RelyingPartyRegistration} representing this relying party
|
||||
* @return the relying party
|
||||
*/
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A result emitted from a SAML 2.0 Logout validation attempt
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class Saml2LogoutValidatorResult {
|
||||
|
||||
static final Saml2LogoutValidatorResult NO_ERRORS = new Saml2LogoutValidatorResult(Collections.emptyList());
|
||||
|
||||
private final Collection<Saml2Error> errors;
|
||||
|
||||
private Saml2LogoutValidatorResult(Collection<Saml2Error> errors) {
|
||||
Assert.notNull(errors, "errors cannot be null");
|
||||
this.errors = new ArrayList<>(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Say whether this result indicates success
|
||||
* @return whether this result has errors
|
||||
*/
|
||||
public boolean hasErrors() {
|
||||
return !this.errors.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return error details regarding the validation attempt
|
||||
* @return the collection of results in this result, if any; returns an empty list
|
||||
* otherwise
|
||||
*/
|
||||
public Collection<Saml2Error> getErrors() {
|
||||
return Collections.unmodifiableCollection(this.errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a successful {@link Saml2LogoutValidatorResult}
|
||||
* @return an {@link Saml2LogoutValidatorResult} with no errors
|
||||
*/
|
||||
public static Saml2LogoutValidatorResult success() {
|
||||
return NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link Saml2LogoutValidatorResult.Builder}, starting with the given
|
||||
* {@code errors}.
|
||||
*
|
||||
* Note that a result with no errors is considered a success.
|
||||
* @param errors
|
||||
* @return
|
||||
*/
|
||||
public static Saml2LogoutValidatorResult.Builder withErrors(Saml2Error... errors) {
|
||||
return new Builder(errors);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final Collection<Saml2Error> errors;
|
||||
|
||||
private Builder(Saml2Error... errors) {
|
||||
this(Arrays.asList(errors));
|
||||
}
|
||||
|
||||
private Builder(Collection<Saml2Error> errors) {
|
||||
Assert.noNullElements(errors, "errors cannot have null elements");
|
||||
this.errors = new ArrayList<>(errors);
|
||||
}
|
||||
|
||||
public Builder errors(Consumer<Collection<Saml2Error>> errorsConsumer) {
|
||||
errorsConsumer.accept(this.errors);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Saml2LogoutValidatorResult build() {
|
||||
return new Saml2LogoutValidatorResult(this.errors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterOutputStream;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
|
||||
/**
|
||||
* Utility methods for working with serialized SAML messages.
|
||||
*
|
||||
* For internal use only.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
final class Saml2Utils {
|
||||
|
||||
private Saml2Utils() {
|
||||
}
|
||||
|
||||
static String samlEncode(byte[] b) {
|
||||
return Base64.getEncoder().encodeToString(b);
|
||||
}
|
||||
|
||||
static byte[] samlDecode(String s) {
|
||||
return Base64.getDecoder().decode(s);
|
||||
}
|
||||
|
||||
static byte[] samlDeflate(String s) {
|
||||
try {
|
||||
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true));
|
||||
deflater.write(s.getBytes(StandardCharsets.UTF_8));
|
||||
deflater.finish();
|
||||
return b.toByteArray();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new Saml2Exception("Unable to deflate string", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static String samlInflate(byte[] b) {
|
||||
try {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
|
||||
iout.write(b);
|
||||
iout.finish();
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new Saml2Exception("Unable to inflate string", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
|
|||
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
|
||||
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.xmlsec.signature.KeyInfo;
|
||||
|
@ -85,6 +86,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
|
|||
spSsoDescriptor.getKeyDescriptors()
|
||||
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
|
||||
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
|
||||
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
|
||||
return spSsoDescriptor;
|
||||
}
|
||||
|
||||
|
@ -123,6 +125,14 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
|
|||
return assertionConsumerService;
|
||||
}
|
||||
|
||||
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration) {
|
||||
SingleLogoutService singleLogoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
|
||||
singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation());
|
||||
singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
|
||||
singleLogoutService.setBinding(registration.getSingleLogoutServiceBinding().getUrn());
|
||||
return singleLogoutService;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T build(QName elementName) {
|
||||
XMLObjectBuilder<?> builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName);
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
|||
import org.opensaml.saml.saml2.metadata.Extensions;
|
||||
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
|
||||
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
|
||||
|
@ -105,6 +106,10 @@ class OpenSamlAssertingPartyMetadataConverter {
|
|||
builder.assertingPartyDetails(
|
||||
(party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())));
|
||||
}
|
||||
if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) {
|
||||
throw new Saml2Exception(
|
||||
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
|
||||
}
|
||||
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
|
||||
Saml2MessageBinding binding;
|
||||
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
|
||||
|
@ -119,10 +124,27 @@ class OpenSamlAssertingPartyMetadataConverter {
|
|||
builder.assertingPartyDetails(
|
||||
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
|
||||
.singleSignOnServiceBinding(binding));
|
||||
return builder;
|
||||
break;
|
||||
}
|
||||
throw new Saml2Exception(
|
||||
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
|
||||
for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
|
||||
Saml2MessageBinding binding;
|
||||
if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
|
||||
binding = Saml2MessageBinding.POST;
|
||||
}
|
||||
else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
|
||||
binding = Saml2MessageBinding.REDIRECT;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
String responseLocation = (singleLogoutService.getResponseLocation() == null)
|
||||
? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation();
|
||||
builder.assertingPartyDetails(
|
||||
(party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation())
|
||||
.singleLogoutServiceResponseLocation(responseLocation).singleLogoutServiceBinding(binding));
|
||||
break;
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
|
||||
|
|
|
@ -81,6 +81,12 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
private final Saml2MessageBinding assertionConsumerServiceBinding;
|
||||
|
||||
private final String singleLogoutServiceLocation;
|
||||
|
||||
private final String singleLogoutServiceResponseLocation;
|
||||
|
||||
private final Saml2MessageBinding singleLogoutServiceBinding;
|
||||
|
||||
private final ProviderDetails providerDetails;
|
||||
|
||||
private final List<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials;
|
||||
|
@ -90,7 +96,9 @@ public final class RelyingPartyRegistration {
|
|||
private final Collection<Saml2X509Credential> signingX509Credentials;
|
||||
|
||||
private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
|
||||
Saml2MessageBinding assertionConsumerServiceBinding, ProviderDetails providerDetails,
|
||||
Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation,
|
||||
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding,
|
||||
ProviderDetails providerDetails,
|
||||
Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials,
|
||||
Collection<Saml2X509Credential> decryptionX509Credentials,
|
||||
Collection<Saml2X509Credential> signingX509Credentials) {
|
||||
|
@ -118,6 +126,9 @@ public final class RelyingPartyRegistration {
|
|||
this.entityId = entityId;
|
||||
this.assertionConsumerServiceLocation = assertionConsumerServiceLocation;
|
||||
this.assertionConsumerServiceBinding = assertionConsumerServiceBinding;
|
||||
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||
this.providerDetails = providerDetails;
|
||||
this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials));
|
||||
this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials));
|
||||
|
@ -177,6 +188,52 @@ public final class RelyingPartyRegistration {
|
|||
return this.assertionConsumerServiceBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Binding</a>
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Binding="..."/> in the
|
||||
* relying party's <SPSSODescriptor>.
|
||||
* @return the SingleLogoutService Binding
|
||||
* @since 5.6
|
||||
*/
|
||||
public Saml2MessageBinding getSingleLogoutServiceBinding() {
|
||||
return this.singleLogoutServiceBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Location="..."/> in the
|
||||
* relying party's <SPSSODescriptor>.
|
||||
* @return the SingleLogoutService Location
|
||||
* @since 5.6
|
||||
*/
|
||||
public String getSingleLogoutServiceLocation() {
|
||||
return this.singleLogoutServiceLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Response Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService
|
||||
* ResponseLocation="..."/> in the relying party's <SPSSODescriptor>.
|
||||
* @return the SingleLogoutService Response Location
|
||||
* @since 5.6
|
||||
*/
|
||||
public String getSingleLogoutServiceResponseLocation() {
|
||||
return this.singleLogoutServiceResponseLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated
|
||||
* with this relying party
|
||||
|
@ -364,6 +421,9 @@ public final class RelyingPartyRegistration {
|
|||
.decryptionX509Credentials((c) -> c.addAll(registration.getDecryptionX509Credentials()))
|
||||
.assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation())
|
||||
.assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding())
|
||||
.singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation())
|
||||
.singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation())
|
||||
.singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding())
|
||||
.assertingPartyDetails((assertingParty) -> assertingParty
|
||||
.entityId(registration.getAssertingPartyDetails().getEntityId())
|
||||
.wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned())
|
||||
|
@ -376,7 +436,13 @@ public final class RelyingPartyRegistration {
|
|||
.singleSignOnServiceLocation(
|
||||
registration.getAssertingPartyDetails().getSingleSignOnServiceLocation())
|
||||
.singleSignOnServiceBinding(
|
||||
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()));
|
||||
registration.getAssertingPartyDetails().getSingleSignOnServiceBinding())
|
||||
.singleLogoutServiceLocation(
|
||||
registration.getAssertingPartyDetails().getSingleLogoutServiceLocation())
|
||||
.singleLogoutServiceResponseLocation(
|
||||
registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation())
|
||||
.singleLogoutServiceBinding(
|
||||
registration.getAssertingPartyDetails().getSingleLogoutServiceBinding()));
|
||||
}
|
||||
|
||||
private static Saml2X509Credential fromDeprecated(
|
||||
|
@ -445,10 +511,17 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
private final Saml2MessageBinding singleSignOnServiceBinding;
|
||||
|
||||
private final String singleLogoutServiceLocation;
|
||||
|
||||
private final String singleLogoutServiceResponseLocation;
|
||||
|
||||
private final Saml2MessageBinding singleLogoutServiceBinding;
|
||||
|
||||
private AssertingPartyDetails(String entityId, boolean wantAuthnRequestsSigned, List<String> signingAlgorithms,
|
||||
Collection<Saml2X509Credential> verificationX509Credentials,
|
||||
Collection<Saml2X509Credential> encryptionX509Credentials, String singleSignOnServiceLocation,
|
||||
Saml2MessageBinding singleSignOnServiceBinding) {
|
||||
Saml2MessageBinding singleSignOnServiceBinding, String singleLogoutServiceLocation,
|
||||
String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding) {
|
||||
Assert.hasText(entityId, "entityId cannot be null or empty");
|
||||
Assert.notEmpty(signingAlgorithms, "signingAlgorithms cannot be empty");
|
||||
Assert.notNull(verificationX509Credentials, "verificationX509Credentials cannot be null");
|
||||
|
@ -472,6 +545,9 @@ public final class RelyingPartyRegistration {
|
|||
this.encryptionX509Credentials = encryptionX509Credentials;
|
||||
this.singleSignOnServiceLocation = singleSignOnServiceLocation;
|
||||
this.singleSignOnServiceBinding = singleSignOnServiceBinding;
|
||||
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -565,6 +641,51 @@ public final class RelyingPartyRegistration {
|
|||
return this.singleSignOnServiceBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Location="..."/> in
|
||||
* the asserting party's <IDPSSODescriptor>.
|
||||
* @return the SingleLogoutService Location
|
||||
* @since 5.6
|
||||
*/
|
||||
public String getSingleLogoutServiceLocation() {
|
||||
return this.singleLogoutServiceLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Response Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Location="..."/> in
|
||||
* the asserting party's <IDPSSODescriptor>.
|
||||
* @return the SingleLogoutService Response Location
|
||||
* @since 5.6
|
||||
*/
|
||||
public String getSingleLogoutServiceResponseLocation() {
|
||||
return this.singleLogoutServiceResponseLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Binding</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Binding="..."/> in
|
||||
* the asserting party's <IDPSSODescriptor>.
|
||||
* @return the SingleLogoutService Binding
|
||||
* @since 5.6
|
||||
*/
|
||||
public Saml2MessageBinding getSingleLogoutServiceBinding() {
|
||||
return this.singleLogoutServiceBinding;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String entityId;
|
||||
|
@ -581,6 +702,12 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT;
|
||||
|
||||
private String singleLogoutServiceLocation;
|
||||
|
||||
private String singleLogoutServiceResponseLocation;
|
||||
|
||||
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.REDIRECT;
|
||||
|
||||
/**
|
||||
* Set the asserting party's <a href=
|
||||
* "https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor">EntityID</a>.
|
||||
|
@ -677,6 +804,59 @@ public final class RelyingPartyRegistration {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService
|
||||
* Location="..."/> in the asserting party's <IDPSSODescriptor>.
|
||||
* @param singleLogoutServiceLocation the SingleLogoutService Location
|
||||
* @return the {@link AssertingPartyDetails.Builder} for further configuration
|
||||
* @since 5.6
|
||||
*/
|
||||
public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
|
||||
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Response Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService
|
||||
* ResponseLocation="..."/> in the asserting party's
|
||||
* <IDPSSODescriptor>.
|
||||
* @param singleLogoutServiceResponseLocation the SingleLogoutService Response
|
||||
* Location
|
||||
* @return the {@link AssertingPartyDetails.Builder} for further configuration
|
||||
* @since 5.6
|
||||
*/
|
||||
public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) {
|
||||
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Binding</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Binding="..."/>
|
||||
* in the asserting party's <IDPSSODescriptor>.
|
||||
* @param singleLogoutServiceBinding the SingleLogoutService Binding
|
||||
* @return the {@link AssertingPartyDetails.Builder} for further configuration
|
||||
* @since 5.6
|
||||
*/
|
||||
public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
|
||||
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an immutable ProviderDetails object representing the configuration
|
||||
* for an Identity Provider, IDP
|
||||
|
@ -689,7 +869,9 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
return new AssertingPartyDetails(this.entityId, this.wantAuthnRequestsSigned, signingAlgorithms,
|
||||
this.verificationX509Credentials, this.encryptionX509Credentials,
|
||||
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding);
|
||||
this.singleSignOnServiceLocation, this.singleSignOnServiceBinding,
|
||||
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||
this.singleLogoutServiceBinding);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -830,6 +1012,12 @@ public final class RelyingPartyRegistration {
|
|||
|
||||
private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST;
|
||||
|
||||
private String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
|
||||
|
||||
private String singleLogoutServiceResponseLocation;
|
||||
|
||||
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.POST;
|
||||
|
||||
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
|
||||
|
||||
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new HashSet<>();
|
||||
|
@ -933,6 +1121,58 @@ public final class RelyingPartyRegistration {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Binding</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Binding="..."/> in
|
||||
* the relying party's <SPSSODescriptor>.
|
||||
* @param singleLogoutServiceBinding the SingleLogoutService Binding
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 5.6
|
||||
*/
|
||||
public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
|
||||
this.singleLogoutServiceBinding = singleLogoutServiceBinding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService Location="..."/> in
|
||||
* the relying party's <SPSSODescriptor>.
|
||||
* @param singleLogoutServiceLocation the SingleLogoutService Location
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 5.6
|
||||
*/
|
||||
public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
|
||||
this.singleLogoutServiceLocation = singleLogoutServiceLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the <a href=
|
||||
* "https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService
|
||||
* Response Location</a>
|
||||
*
|
||||
* <p>
|
||||
* Equivalent to the value found in <SingleLogoutService
|
||||
* ResponseLocation="..."/> in the relying party's <SPSSODescriptor>.
|
||||
* @param singleLogoutServiceResponseLocation the SingleLogoutService Response
|
||||
* Location
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 5.6
|
||||
*/
|
||||
public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) {
|
||||
this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this {@link Consumer} to further configure the Asserting Party details
|
||||
* @param assertingPartyDetails The {@link Consumer} to apply
|
||||
|
@ -1075,10 +1315,14 @@ public final class RelyingPartyRegistration {
|
|||
for (Saml2X509Credential credential : this.providerDetails.assertingPartyDetailsBuilder.encryptionX509Credentials) {
|
||||
this.credentials.add(toDeprecated(credential));
|
||||
}
|
||||
if (this.singleLogoutServiceResponseLocation == null) {
|
||||
this.singleLogoutServiceResponseLocation = this.singleLogoutServiceLocation;
|
||||
}
|
||||
return new RelyingPartyRegistration(this.registrationId, this.entityId,
|
||||
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
|
||||
this.providerDetails.build(), this.credentials, this.decryptionX509Credentials,
|
||||
this.signingX509Credentials);
|
||||
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||
this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials,
|
||||
this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|||
* @since 5.4
|
||||
*/
|
||||
public final class DefaultRelyingPartyRegistrationResolver
|
||||
implements RelyingPartyRegistrationResolver, Converter<HttpServletRequest, RelyingPartyRegistration> {
|
||||
implements Converter<HttpServletRequest, RelyingPartyRegistration>, RelyingPartyRegistrationResolver {
|
||||
|
||||
private Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
@ -98,9 +98,14 @@ public final class DefaultRelyingPartyRegistrationResolver
|
|||
String relyingPartyEntityId = templateResolver.apply(relyingPartyRegistration.getEntityId());
|
||||
String assertionConsumerServiceLocation = templateResolver
|
||||
.apply(relyingPartyRegistration.getAssertionConsumerServiceLocation());
|
||||
String singleLogoutServiceLocation = templateResolver
|
||||
.apply(relyingPartyRegistration.getSingleLogoutServiceLocation());
|
||||
String singleLogoutServiceResponseLocation = templateResolver
|
||||
.apply(relyingPartyRegistration.getSingleLogoutServiceResponseLocation());
|
||||
return RelyingPartyRegistration.withRelyingPartyRegistration(relyingPartyRegistration)
|
||||
.entityId(relyingPartyEntityId).assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.build();
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation).build();
|
||||
}
|
||||
|
||||
private Function<String, String> templateResolver(String applicationUri, RelyingPartyRegistration relyingParty) {
|
||||
|
@ -108,6 +113,9 @@ public final class DefaultRelyingPartyRegistrationResolver
|
|||
}
|
||||
|
||||
private static String resolveUrlTemplate(String template, String baseUrl, RelyingPartyRegistration relyingParty) {
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
String entityId = relyingParty.getAssertingPartyDetails().getEntityId();
|
||||
String registrationId = relyingParty.getRegistrationId();
|
||||
Map<String, String> uriVariables = new HashMap<>();
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link Saml2LogoutRequestRepository} that stores
|
||||
* {@link Saml2LogoutRequest} in the {@code HttpSession}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @see Saml2LogoutRequestRepository
|
||||
* @see Saml2LogoutRequest
|
||||
*/
|
||||
public final class HttpSessionLogoutRequestRepository implements Saml2LogoutRequestRepository {
|
||||
|
||||
private static final String DEFAULT_LOGOUT_REQUEST_ATTR_NAME = HttpSessionLogoutRequestRepository.class.getName()
|
||||
+ ".LOGOUT_REQUEST";
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutRequest loadLogoutRequest(HttpServletRequest request) {
|
||||
Assert.notNull(request, "request cannot be null");
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
Saml2LogoutRequest logoutRequest = (Saml2LogoutRequest) session.getAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME);
|
||||
if (stateParameterEquals(request, logoutRequest)) {
|
||||
return logoutRequest;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void saveLogoutRequest(Saml2LogoutRequest logoutRequest, HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
Assert.notNull(request, "request cannot be null");
|
||||
Assert.notNull(response, "response cannot be null");
|
||||
if (logoutRequest == null) {
|
||||
request.getSession().removeAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME);
|
||||
return;
|
||||
}
|
||||
String state = logoutRequest.getRelayState();
|
||||
Assert.hasText(state, "logoutRequest.state cannot be empty");
|
||||
request.getSession().setAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME, logoutRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutRequest removeLogoutRequest(HttpServletRequest request, HttpServletResponse response) {
|
||||
Assert.notNull(request, "request cannot be null");
|
||||
Assert.notNull(response, "response cannot be null");
|
||||
Saml2LogoutRequest logoutRequest = loadLogoutRequest(request);
|
||||
if (logoutRequest == null) {
|
||||
return null;
|
||||
}
|
||||
request.getSession().removeAttribute(DEFAULT_LOGOUT_REQUEST_ATTR_NAME);
|
||||
return logoutRequest;
|
||||
}
|
||||
|
||||
private String getStateParameter(HttpServletRequest request) {
|
||||
return request.getParameter("RelayState");
|
||||
}
|
||||
|
||||
private boolean stateParameterEquals(HttpServletRequest request, Saml2LogoutRequest logoutRequest) {
|
||||
String stateParameter = getStateParameter(request);
|
||||
if (stateParameter == null || logoutRequest == null) {
|
||||
return false;
|
||||
}
|
||||
String relayState = logoutRequest.getRelayState();
|
||||
return MessageDigest.isEqual(Utf8.encode(stateParameter), Utf8.encode(relayState));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.opensaml.core.config.ConfigurationService;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
||||
import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutRequestMarshaller;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* For internal use only. Intended for consolidating common behavior related to minting a
|
||||
* SAML 2.0 Logout Request.
|
||||
*/
|
||||
final class OpenSamlLogoutRequestResolver {
|
||||
|
||||
static {
|
||||
OpenSamlInitializationService.initialize();
|
||||
}
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final LogoutRequestMarshaller marshaller;
|
||||
|
||||
private final IssuerBuilder issuerBuilder;
|
||||
|
||||
private final NameIDBuilder nameIdBuilder;
|
||||
|
||||
private final LogoutRequestBuilder logoutRequestBuilder;
|
||||
|
||||
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSamlLogoutRequestResolver}
|
||||
*/
|
||||
OpenSamlLogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
|
||||
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
|
||||
this.marshaller = (LogoutRequestMarshaller) registry.getMarshallerFactory()
|
||||
.getMarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.marshaller, "logoutRequestMarshaller must be configured in OpenSAML");
|
||||
this.logoutRequestBuilder = (LogoutRequestBuilder) registry.getBuilderFactory()
|
||||
.getBuilder(LogoutRequest.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.logoutRequestBuilder, "logoutRequestBuilder must be configured in OpenSAML");
|
||||
this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML");
|
||||
this.nameIdBuilder = (NameIDBuilder) registry.getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.nameIdBuilder, "nameIdBuilder must be configured in OpenSAML");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare to create, sign, and serialize a SAML 2.0 Logout Request.
|
||||
*
|
||||
* By default, includes a {@code NameID} based on the {@link Authentication} instance
|
||||
* as well as the {@code Destination} and {@code Issuer} based on the
|
||||
* {@link RelyingPartyRegistration} derived from the {@link Authentication}.
|
||||
* @param request the HTTP request
|
||||
* @param authentication the current user
|
||||
* @return a signed and serialized SAML 2.0 Logout Request
|
||||
*/
|
||||
Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return resolve(request, authentication, (registration, logoutRequest) -> {
|
||||
});
|
||||
}
|
||||
|
||||
Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication,
|
||||
BiConsumer<RelyingPartyRegistration, LogoutRequest> logoutRequestConsumer) {
|
||||
String registrationId = getRegistrationId(authentication);
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId);
|
||||
if (registration == null) {
|
||||
return null;
|
||||
}
|
||||
LogoutRequest logoutRequest = this.logoutRequestBuilder.buildObject();
|
||||
logoutRequest.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
|
||||
Issuer issuer = this.issuerBuilder.buildObject();
|
||||
issuer.setValue(registration.getEntityId());
|
||||
logoutRequest.setIssuer(issuer);
|
||||
NameID nameId = this.nameIdBuilder.buildObject();
|
||||
nameId.setValue(authentication.getName());
|
||||
logoutRequest.setNameID(nameId);
|
||||
logoutRequestConsumer.accept(registration, logoutRequest);
|
||||
if (logoutRequest.getID() == null) {
|
||||
logoutRequest.setID("LR" + UUID.randomUUID());
|
||||
}
|
||||
String relayState = UUID.randomUUID().toString();
|
||||
Saml2LogoutRequest.Builder result = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.id(logoutRequest.getID());
|
||||
if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) {
|
||||
String xml = serialize(OpenSamlSigningUtils.sign(logoutRequest, registration));
|
||||
String samlRequest = Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8));
|
||||
return result.samlRequest(samlRequest).relayState(relayState).build();
|
||||
}
|
||||
else {
|
||||
String xml = serialize(logoutRequest);
|
||||
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
|
||||
result.samlRequest(deflatedAndEncoded);
|
||||
QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration)
|
||||
.param("SAMLRequest", deflatedAndEncoded).param("RelayState", relayState);
|
||||
return result.parameters((params) -> params.putAll(partial.parameters())).build();
|
||||
}
|
||||
}
|
||||
|
||||
private String getRegistrationId(Authentication authentication) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Attempting to resolve registrationId from " + authentication);
|
||||
}
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof Saml2AuthenticatedPrincipal) {
|
||||
return ((Saml2AuthenticatedPrincipal) principal).getRelyingPartyRegistrationId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String serialize(LogoutRequest logoutRequest) {
|
||||
try {
|
||||
Element element = this.marshaller.marshall(logoutRequest);
|
||||
return SerializeSupport.nodeToString(element);
|
||||
}
|
||||
catch (MarshallingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import net.shibboleth.utilities.java.support.xml.ParserPool;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.opensaml.core.config.ConfigurationService;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
import org.opensaml.saml.saml2.core.Status;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutResponseBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutResponseMarshaller;
|
||||
import org.opensaml.saml.saml2.core.impl.StatusBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* For internal use only. Intended for consolidating common behavior related to minting a
|
||||
* SAML 2.0 Logout Response.
|
||||
*/
|
||||
final class OpenSamlLogoutResponseResolver {
|
||||
|
||||
static {
|
||||
OpenSamlInitializationService.initialize();
|
||||
}
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final ParserPool parserPool;
|
||||
|
||||
private final LogoutRequestUnmarshaller unmarshaller;
|
||||
|
||||
private final LogoutResponseMarshaller marshaller;
|
||||
|
||||
private final LogoutResponseBuilder logoutResponseBuilder;
|
||||
|
||||
private final IssuerBuilder issuerBuilder;
|
||||
|
||||
private final StatusBuilder statusBuilder;
|
||||
|
||||
private final StatusCodeBuilder statusCodeBuilder;
|
||||
|
||||
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSamlLogoutResponseResolver}
|
||||
*/
|
||||
OpenSamlLogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
|
||||
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
|
||||
this.parserPool = registry.getParserPool();
|
||||
this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory()
|
||||
.getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME);
|
||||
this.marshaller = (LogoutResponseMarshaller) registry.getMarshallerFactory()
|
||||
.getMarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.marshaller, "logoutResponseMarshaller must be configured in OpenSAML");
|
||||
this.logoutResponseBuilder = (LogoutResponseBuilder) registry.getBuilderFactory()
|
||||
.getBuilder(LogoutResponse.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.logoutResponseBuilder, "logoutResponseBuilder must be configured in OpenSAML");
|
||||
this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.issuerBuilder, "issuerBuilder must be configured in OpenSAML");
|
||||
this.statusBuilder = (StatusBuilder) registry.getBuilderFactory().getBuilder(Status.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.statusBuilder, "statusBuilder must be configured in OpenSAML");
|
||||
this.statusCodeBuilder = (StatusCodeBuilder) registry.getBuilderFactory()
|
||||
.getBuilder(StatusCode.DEFAULT_ELEMENT_NAME);
|
||||
Assert.notNull(this.statusCodeBuilder, "statusCodeBuilder must be configured in OpenSAML");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare to create, sign, and serialize a SAML 2.0 Logout Response.
|
||||
*
|
||||
* By default, includes a {@code RelayState} based on the {@link HttpServletRequest}
|
||||
* as well as the {@code Destination} and {@code Issuer} based on the
|
||||
* {@link RelyingPartyRegistration} derived from the {@link Authentication}. The
|
||||
* logout response is also marked as {@code SUCCESS}.
|
||||
* @param request the HTTP request
|
||||
* @param authentication the current user
|
||||
* @return a signed and serialized SAML 2.0 Logout Response
|
||||
*/
|
||||
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return resolve(request, authentication, (registration, logoutResponse) -> {
|
||||
});
|
||||
}
|
||||
|
||||
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication,
|
||||
BiConsumer<RelyingPartyRegistration, LogoutResponse> logoutResponseConsumer) {
|
||||
String registrationId = getRegistrationId(authentication);
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId);
|
||||
if (registration == null) {
|
||||
return null;
|
||||
}
|
||||
String serialized = request.getParameter("SAMLRequest");
|
||||
byte[] b = Saml2Utils.samlDecode(serialized);
|
||||
LogoutRequest logoutRequest = parse(inflateIfRequired(registration, b));
|
||||
LogoutResponse logoutResponse = this.logoutResponseBuilder.buildObject();
|
||||
logoutResponse.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
|
||||
Issuer issuer = this.issuerBuilder.buildObject();
|
||||
issuer.setValue(registration.getEntityId());
|
||||
logoutResponse.setIssuer(issuer);
|
||||
StatusCode code = this.statusCodeBuilder.buildObject();
|
||||
code.setValue(StatusCode.SUCCESS);
|
||||
Status status = this.statusBuilder.buildObject();
|
||||
status.setStatusCode(code);
|
||||
logoutResponse.setStatus(status);
|
||||
logoutResponse.setInResponseTo(logoutRequest.getID());
|
||||
if (logoutResponse.getID() == null) {
|
||||
logoutResponse.setID("LR" + UUID.randomUUID());
|
||||
}
|
||||
logoutResponseConsumer.accept(registration, logoutResponse);
|
||||
Saml2LogoutResponse.Builder result = Saml2LogoutResponse.withRelyingPartyRegistration(registration);
|
||||
if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) {
|
||||
String xml = serialize(OpenSamlSigningUtils.sign(logoutResponse, registration));
|
||||
String samlResponse = Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8));
|
||||
result.samlResponse(samlResponse);
|
||||
if (request.getParameter("RelayState") != null) {
|
||||
result.relayState(request.getParameter("RelayState"));
|
||||
}
|
||||
return result.build();
|
||||
}
|
||||
else {
|
||||
String xml = serialize(logoutResponse);
|
||||
String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml));
|
||||
result.samlResponse(deflatedAndEncoded);
|
||||
QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration).param("SAMLResponse",
|
||||
deflatedAndEncoded);
|
||||
if (request.getParameter("RelayState") != null) {
|
||||
partial.param("RelayState", request.getParameter("RelayState"));
|
||||
}
|
||||
return result.parameters((params) -> params.putAll(partial.parameters())).build();
|
||||
}
|
||||
}
|
||||
|
||||
private String getRegistrationId(Authentication authentication) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Attempting to resolve registrationId from " + authentication);
|
||||
}
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof Saml2AuthenticatedPrincipal) {
|
||||
return ((Saml2AuthenticatedPrincipal) principal).getRelyingPartyRegistrationId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String inflateIfRequired(RelyingPartyRegistration registration, byte[] b) {
|
||||
if (registration.getSingleLogoutServiceBinding() == Saml2MessageBinding.REDIRECT) {
|
||||
return Saml2Utils.samlInflate(b);
|
||||
}
|
||||
return new String(b, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LogoutRequest parse(String request) throws Saml2Exception {
|
||||
try {
|
||||
Document document = this.parserPool
|
||||
.parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8)));
|
||||
Element element = document.getDocumentElement();
|
||||
return (LogoutRequest) this.unmarshaller.unmarshall(element);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception("Failed to deserialize LogoutRequest", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String serialize(LogoutResponse logoutResponse) {
|
||||
try {
|
||||
Element element = this.marshaller.marshall(logoutResponse);
|
||||
return SerializeSupport.nodeToString(element);
|
||||
}
|
||||
catch (MarshallingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,250 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
/**
|
||||
* A filter for handling logout requests in the form of a <saml2:LogoutRequest> sent
|
||||
* from the asserting party.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @see Saml2LogoutRequestValidator
|
||||
* @see Saml2AssertingPartyInitiatedLogoutSuccessHandler
|
||||
*/
|
||||
public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final Saml2LogoutRequestValidator logoutRequestValidator;
|
||||
|
||||
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
|
||||
|
||||
private final Saml2LogoutResponseResolver logoutResponseResolver;
|
||||
|
||||
private final LogoutHandler handler;
|
||||
|
||||
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout/saml2/slo");
|
||||
|
||||
/**
|
||||
* Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout
|
||||
* Requests from the asserting party
|
||||
* @param relyingPartyRegistrationResolver the strategy for resolving a
|
||||
* {@link RelyingPartyRegistration}
|
||||
* @param logoutRequestValidator the SAML 2.0 Logout Request authenticator
|
||||
* @param logoutResponseResolver the strategy for creating a SAML 2.0 Logout Response
|
||||
* @param handlers the actions that perform logout
|
||||
*/
|
||||
public Saml2LogoutRequestFilter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver,
|
||||
Saml2LogoutRequestValidator logoutRequestValidator, Saml2LogoutResponseResolver logoutResponseResolver,
|
||||
LogoutHandler... handlers) {
|
||||
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
|
||||
this.logoutRequestValidator = logoutRequestValidator;
|
||||
this.logoutResponseResolver = logoutResponseResolver;
|
||||
this.handler = new CompositeLogoutHandler(handlers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (!this.logoutRequestMatcher.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.getParameter("SAMLRequest") == null) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
|
||||
getRegistrationId(authentication));
|
||||
if (registration == null) {
|
||||
this.logger
|
||||
.trace("Did not process logout request since failed to find associated RelyingPartyRegistration");
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
if (!isCorrectBinding(request, registration)) {
|
||||
this.logger.trace("Did not process logout request since used incorrect binding");
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
String serialized = request.getParameter("SAMLRequest");
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest(serialized).relayState(request.getParameter("RelayState"))
|
||||
.binding(registration.getSingleLogoutServiceBinding())
|
||||
.location(registration.getSingleLogoutServiceLocation())
|
||||
.parameters((params) -> params.put("SigAlg", request.getParameter("SigAlg")))
|
||||
.parameters((params) -> params.put("Signature", request.getParameter("Signature"))).build();
|
||||
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(logoutRequest,
|
||||
registration, authentication);
|
||||
Saml2LogoutValidatorResult result = this.logoutRequestValidator.validate(parameters);
|
||||
if (result.hasErrors()) {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, result.getErrors().iterator().next().toString());
|
||||
this.logger.debug(LogMessage.format("Failed to validate LogoutRequest: %s", result.getErrors()));
|
||||
return;
|
||||
}
|
||||
this.handler.logout(request, response, authentication);
|
||||
Saml2LogoutResponse logoutResponse = this.logoutResponseResolver.resolve(request, authentication);
|
||||
if (logoutResponse == null) {
|
||||
this.logger.trace("Returning 401 since no logout response generated");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
if (logoutResponse.getBinding() == Saml2MessageBinding.REDIRECT) {
|
||||
doRedirect(request, response, logoutResponse);
|
||||
}
|
||||
else {
|
||||
doPost(response, logoutResponse);
|
||||
}
|
||||
}
|
||||
|
||||
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
|
||||
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
|
||||
this.logoutRequestMatcher = logoutRequestMatcher;
|
||||
}
|
||||
|
||||
private String getRegistrationId(Authentication authentication) {
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof Saml2AuthenticatedPrincipal) {
|
||||
return ((Saml2AuthenticatedPrincipal) principal).getRelyingPartyRegistrationId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) {
|
||||
Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding();
|
||||
if (requiredBinding == Saml2MessageBinding.POST) {
|
||||
return "POST".equals(request.getMethod());
|
||||
}
|
||||
return "GET".equals(request.getMethod());
|
||||
}
|
||||
|
||||
private void doRedirect(HttpServletRequest request, HttpServletResponse response,
|
||||
Saml2LogoutResponse logoutResponse) throws IOException {
|
||||
String location = logoutResponse.getResponseLocation();
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
|
||||
addParameter("SAMLResponse", logoutResponse::getParameter, uriBuilder);
|
||||
addParameter("RelayState", logoutResponse::getParameter, uriBuilder);
|
||||
addParameter("SigAlg", logoutResponse::getParameter, uriBuilder);
|
||||
addParameter("Signature", logoutResponse::getParameter, uriBuilder);
|
||||
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
|
||||
}
|
||||
|
||||
private void addParameter(String name, Function<String, String> parameters, UriComponentsBuilder builder) {
|
||||
Assert.hasText(name, "name cannot be empty or null");
|
||||
if (StringUtils.hasText(parameters.apply(name))) {
|
||||
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
|
||||
UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
}
|
||||
|
||||
private void doPost(HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException {
|
||||
String location = logoutResponse.getResponseLocation();
|
||||
String saml = logoutResponse.getSamlResponse();
|
||||
String relayState = logoutResponse.getRelayState();
|
||||
String html = createSamlPostRequestFormData(location, saml, relayState);
|
||||
response.setContentType(MediaType.TEXT_HTML_VALUE);
|
||||
response.getWriter().write(html);
|
||||
}
|
||||
|
||||
private String createSamlPostRequestFormData(String location, String saml, String relayState) {
|
||||
StringBuilder html = new StringBuilder();
|
||||
html.append("<!DOCTYPE html>\n");
|
||||
html.append("<html>\n").append(" <head>\n");
|
||||
html.append(" <meta charset=\"utf-8\" />\n");
|
||||
html.append(" </head>\n");
|
||||
html.append(" <body onload=\"document.forms[0].submit()\">\n");
|
||||
html.append(" <noscript>\n");
|
||||
html.append(" <p>\n");
|
||||
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
|
||||
html.append(" you must press the Continue button once to proceed.\n");
|
||||
html.append(" </p>\n");
|
||||
html.append(" </noscript>\n");
|
||||
html.append(" \n");
|
||||
html.append(" <form action=\"");
|
||||
html.append(location);
|
||||
html.append("\" method=\"post\">\n");
|
||||
html.append(" <div>\n");
|
||||
html.append(" <input type=\"hidden\" name=\"SAMLResponse\" value=\"");
|
||||
html.append(HtmlUtils.htmlEscape(saml));
|
||||
html.append("\"/>\n");
|
||||
if (StringUtils.hasText(relayState)) {
|
||||
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
|
||||
html.append(HtmlUtils.htmlEscape(relayState));
|
||||
html.append("\"/>\n");
|
||||
}
|
||||
html.append(" </div>\n");
|
||||
html.append(" <noscript>\n");
|
||||
html.append(" <div>\n");
|
||||
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
|
||||
html.append(" </div>\n");
|
||||
html.append(" </noscript>\n");
|
||||
html.append(" </form>\n");
|
||||
html.append(" \n");
|
||||
html.append(" </body>\n");
|
||||
html.append("</html>");
|
||||
return html.toString();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for the persistence of
|
||||
* {@link Saml2LogoutRequest} between requests.
|
||||
*
|
||||
* <p>
|
||||
* Used by the {@link Saml2RelyingPartyInitiatedLogoutSuccessHandler} for persisting the
|
||||
* Logout Request before it initiates the SAML 2.0 SLO flow. As well, used by
|
||||
* {@code OpenSamlLogoutResponseHandler} for resolving the Logout Request associated with
|
||||
* that Logout Response.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @see Saml2LogoutRequest
|
||||
* @see HttpSessionLogoutRequestRepository
|
||||
*/
|
||||
public interface Saml2LogoutRequestRepository {
|
||||
|
||||
/**
|
||||
* Returns the {@link Saml2LogoutRequest} associated to the provided
|
||||
* {@code HttpServletRequest} or {@code null} if not available.
|
||||
* @param request the {@code HttpServletRequest}
|
||||
* @return the {@link Saml2LogoutRequest} or {@code null} if not available
|
||||
*/
|
||||
Saml2LogoutRequest loadLogoutRequest(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* Persists the {@link Saml2LogoutRequest} associating it to the provided
|
||||
* {@code HttpServletRequest} and/or {@code HttpServletResponse}.
|
||||
* @param logoutRequest the {@link Saml2LogoutRequest}
|
||||
* @param request the {@code HttpServletRequest}
|
||||
* @param response the {@code HttpServletResponse}
|
||||
*/
|
||||
void saveLogoutRequest(Saml2LogoutRequest logoutRequest, HttpServletRequest request, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* Removes and returns the {@link Saml2LogoutRequest} associated to the provided
|
||||
* {@code HttpServletRequest} and {@code HttpServletResponse} or if not available
|
||||
* returns {@code null}.
|
||||
* @param request the {@code HttpServletRequest}
|
||||
* @param response the {@code HttpServletResponse}
|
||||
* @return the {@link Saml2LogoutRequest} or {@code null} if not available
|
||||
*/
|
||||
Saml2LogoutRequest removeLogoutRequest(HttpServletRequest request, HttpServletResponse response);
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
/**
|
||||
* Creates a signed SAML 2.0 Logout Request based on information from the
|
||||
* {@link HttpServletRequest} and current {@link Authentication}.
|
||||
*
|
||||
* The returned logout request is suitable for sending to the asserting party based on,
|
||||
* for example, the location and binding specified in
|
||||
* {@link RelyingPartyRegistration#getAssertingPartyDetails()}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @see RelyingPartyRegistration
|
||||
*/
|
||||
public interface Saml2LogoutRequestResolver {
|
||||
|
||||
/**
|
||||
* Prepare to create, sign, and serialize a SAML 2.0 Logout Request.
|
||||
*
|
||||
* By default, includes a {@code NameID} based on the {@link Authentication} instance.
|
||||
* @param request the HTTP request
|
||||
* @param authentication the current user
|
||||
* @return a signed and serialized SAML 2.0 Logout Request
|
||||
*/
|
||||
Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication);
|
||||
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidatorParameters;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* A filter for handling a <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 LogoutSuccessHandler} that relies
|
||||
* on the user being logged in.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @see Saml2LogoutRequestRepository
|
||||
* @see Saml2LogoutResponseValidator
|
||||
*/
|
||||
public final class Saml2LogoutResponseFilter extends OncePerRequestFilter {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
|
||||
|
||||
private final Saml2LogoutResponseValidator logoutResponseValidator;
|
||||
|
||||
private final LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
|
||||
|
||||
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout/saml2/slo");
|
||||
|
||||
/**
|
||||
* Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout
|
||||
* Responses from the asserting party
|
||||
* @param relyingPartyRegistrationResolver the strategy for resolving a
|
||||
* {@link RelyingPartyRegistration}
|
||||
* @param logoutResponseValidator authenticates the SAML 2.0 Logout Response
|
||||
* @param logoutSuccessHandler the action to perform now that logout has succeeded
|
||||
*/
|
||||
public Saml2LogoutResponseFilter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver,
|
||||
Saml2LogoutResponseValidator logoutResponseValidator, LogoutSuccessHandler logoutSuccessHandler) {
|
||||
this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver;
|
||||
this.logoutResponseValidator = logoutResponseValidator;
|
||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (!this.logoutRequestMatcher.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.getParameter("SAMLResponse") == null) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Saml2LogoutRequest logoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
|
||||
if (logoutRequest == null) {
|
||||
this.logger.trace("Did not process logout response since could not find associated LogoutRequest");
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Failed to find associated LogoutRequest");
|
||||
return;
|
||||
}
|
||||
RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request,
|
||||
logoutRequest.getRelyingPartyRegistrationId());
|
||||
if (registration == null) {
|
||||
this.logger
|
||||
.trace("Did not process logout request since failed to find associated RelyingPartyRegistration");
|
||||
Saml2Error error = new Saml2Error(Saml2ErrorCodes.RELYING_PARTY_REGISTRATION_NOT_FOUND,
|
||||
"Failed to find associated RelyingPartyRegistration");
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, error.toString());
|
||||
return;
|
||||
}
|
||||
if (!isCorrectBinding(request, registration)) {
|
||||
this.logger.trace("Did not process logout request since used incorrect binding");
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
String serialized = request.getParameter("SAMLResponse");
|
||||
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
|
||||
.samlResponse(serialized).relayState(request.getParameter("RelayState"))
|
||||
.binding(registration.getSingleLogoutServiceBinding())
|
||||
.location(registration.getSingleLogoutServiceResponseLocation())
|
||||
.parameters((params) -> params.put("SigAlg", request.getParameter("SigAlg")))
|
||||
.parameters((params) -> params.put("Signature", request.getParameter("Signature"))).build();
|
||||
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(logoutResponse,
|
||||
logoutRequest, registration);
|
||||
Saml2LogoutValidatorResult result = this.logoutResponseValidator.validate(parameters);
|
||||
if (result.hasErrors()) {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, result.getErrors().iterator().next().toString());
|
||||
this.logger.debug(LogMessage.format("Failed to validate LogoutResponse: %s", result.getErrors()));
|
||||
return;
|
||||
}
|
||||
this.logoutSuccessHandler.onLogoutSuccess(request, response, null);
|
||||
}
|
||||
|
||||
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
|
||||
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
|
||||
this.logoutRequestMatcher = logoutRequestMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Saml2LogoutRequestRepository} for retrieving the SAML 2.0 Logout
|
||||
* Request associated with the request's {@code RelayState}
|
||||
* @param logoutRequestRepository the {@link Saml2LogoutRequestRepository} to use
|
||||
*/
|
||||
public void setLogoutRequestRepository(Saml2LogoutRequestRepository logoutRequestRepository) {
|
||||
Assert.notNull(logoutRequestRepository, "logoutRequestRepository cannot be null");
|
||||
this.logoutRequestRepository = logoutRequestRepository;
|
||||
}
|
||||
|
||||
private boolean isCorrectBinding(HttpServletRequest request, RelyingPartyRegistration registration) {
|
||||
Saml2MessageBinding requiredBinding = registration.getSingleLogoutServiceBinding();
|
||||
if (requiredBinding == Saml2MessageBinding.POST) {
|
||||
return "POST".equals(request.getMethod());
|
||||
}
|
||||
return "GET".equals(request.getMethod());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
/**
|
||||
* Creates a signed SAML 2.0 Logout Response based on information from the
|
||||
* {@link HttpServletRequest} and current {@link Authentication}.
|
||||
*
|
||||
* The returned logout response is suitable for sending to the asserting party based on,
|
||||
* for example, the location and binding specified in
|
||||
* {@link RelyingPartyRegistration#getAssertingPartyDetails()}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @see RelyingPartyRegistration
|
||||
*/
|
||||
public interface Saml2LogoutResponseResolver {
|
||||
|
||||
/**
|
||||
* Prepare to create, sign, and serialize a SAML 2.0 Logout Response.
|
||||
* @param request the HTTP request
|
||||
* @param authentication the current user
|
||||
* @return a signed and serialized SAML 2.0 Logout Response
|
||||
*/
|
||||
Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication);
|
||||
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
/**
|
||||
* A success handler for issuing a SAML 2.0 Logout Request to the the SAML 2.0 Asserting
|
||||
* Party
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class Saml2RelyingPartyInitiatedLogoutSuccessHandler implements LogoutSuccessHandler {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final Saml2LogoutRequestResolver logoutRequestResolver;
|
||||
|
||||
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
|
||||
|
||||
/**
|
||||
* Constructs a {@link Saml2RelyingPartyInitiatedLogoutSuccessHandler} using the
|
||||
* provided parameters
|
||||
* @param logoutRequestResolver the {@link Saml2LogoutRequestResolver} to use
|
||||
*/
|
||||
public Saml2RelyingPartyInitiatedLogoutSuccessHandler(Saml2LogoutRequestResolver logoutRequestResolver) {
|
||||
this.logoutRequestResolver = logoutRequestResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce and send a SAML 2.0 Logout Response based on the SAML 2.0 Logout Request
|
||||
* received from the asserting party
|
||||
* @param request the HTTP request
|
||||
* @param response the HTTP response
|
||||
* @param authentication the current principal details
|
||||
* @throws IOException when failing to write to the response
|
||||
*/
|
||||
@Override
|
||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException {
|
||||
Saml2LogoutRequest logoutRequest = this.logoutRequestResolver.resolve(request, authentication);
|
||||
if (logoutRequest == null) {
|
||||
this.logger.trace("Returning 401 since no logout request generated");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
|
||||
if (logoutRequest.getBinding() == Saml2MessageBinding.REDIRECT) {
|
||||
doRedirect(request, response, logoutRequest);
|
||||
}
|
||||
else {
|
||||
doPost(response, logoutRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Saml2LogoutRequestRepository} for saving the SAML 2.0 Logout
|
||||
* Request
|
||||
* @param logoutRequestRepository the {@link Saml2LogoutRequestRepository} to use
|
||||
*/
|
||||
public void setLogoutRequestRepository(Saml2LogoutRequestRepository logoutRequestRepository) {
|
||||
Assert.notNull(logoutRequestRepository, "logoutRequestRepository cannot be null");
|
||||
this.logoutRequestRepository = logoutRequestRepository;
|
||||
}
|
||||
|
||||
private void doRedirect(HttpServletRequest request, HttpServletResponse response, Saml2LogoutRequest logoutRequest)
|
||||
throws IOException {
|
||||
String location = logoutRequest.getLocation();
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
|
||||
addParameter("SAMLRequest", logoutRequest::getParameter, uriBuilder);
|
||||
addParameter("RelayState", logoutRequest::getParameter, uriBuilder);
|
||||
addParameter("SigAlg", logoutRequest::getParameter, uriBuilder);
|
||||
addParameter("Signature", logoutRequest::getParameter, uriBuilder);
|
||||
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
|
||||
}
|
||||
|
||||
private void addParameter(String name, Function<String, String> parameters, UriComponentsBuilder builder) {
|
||||
Assert.hasText(name, "name cannot be empty or null");
|
||||
if (StringUtils.hasText(parameters.apply(name))) {
|
||||
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
|
||||
UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
}
|
||||
|
||||
private void doPost(HttpServletResponse response, Saml2LogoutRequest logoutRequest) throws IOException {
|
||||
String location = logoutRequest.getLocation();
|
||||
String saml = logoutRequest.getSamlRequest();
|
||||
String relayState = logoutRequest.getRelayState();
|
||||
String html = createSamlPostRequestFormData(location, saml, relayState);
|
||||
response.setContentType(MediaType.TEXT_HTML_VALUE);
|
||||
response.getWriter().write(html);
|
||||
}
|
||||
|
||||
private String createSamlPostRequestFormData(String location, String saml, String relayState) {
|
||||
StringBuilder html = new StringBuilder();
|
||||
html.append("<!DOCTYPE html>\n");
|
||||
html.append("<html>\n").append(" <head>\n");
|
||||
html.append(" <meta charset=\"utf-8\" />\n");
|
||||
html.append(" </head>\n");
|
||||
html.append(" <body onload=\"document.forms[0].submit()\">\n");
|
||||
html.append(" <noscript>\n");
|
||||
html.append(" <p>\n");
|
||||
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
|
||||
html.append(" you must press the Continue button once to proceed.\n");
|
||||
html.append(" </p>\n");
|
||||
html.append(" </noscript>\n");
|
||||
html.append(" \n");
|
||||
html.append(" <form action=\"");
|
||||
html.append(location);
|
||||
html.append("\" method=\"post\">\n");
|
||||
html.append(" <div>\n");
|
||||
html.append(" <input type=\"hidden\" name=\"SAMLRequest\" value=\"");
|
||||
html.append(HtmlUtils.htmlEscape(saml));
|
||||
html.append("\"/>\n");
|
||||
if (StringUtils.hasText(relayState)) {
|
||||
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
|
||||
html.append(HtmlUtils.htmlEscape(relayState));
|
||||
html.append("\"/>\n");
|
||||
}
|
||||
html.append(" </div>\n");
|
||||
html.append(" <noscript>\n");
|
||||
html.append(" <div>\n");
|
||||
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
|
||||
html.append(" </div>\n");
|
||||
html.append(" </noscript>\n");
|
||||
html.append(" </form>\n");
|
||||
html.append(" \n");
|
||||
html.append(" </body>\n");
|
||||
html.append("</html>");
|
||||
return html.toString();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterOutputStream;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
|
||||
/**
|
||||
* Utility methods for working with serialized SAML messages.
|
||||
*
|
||||
* For internal use only.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
final class Saml2Utils {
|
||||
|
||||
private Saml2Utils() {
|
||||
}
|
||||
|
||||
static String samlEncode(byte[] b) {
|
||||
return Base64.getEncoder().encodeToString(b);
|
||||
}
|
||||
|
||||
static byte[] samlDecode(String s) {
|
||||
return Base64.getDecoder().decode(s);
|
||||
}
|
||||
|
||||
static byte[] samlDeflate(String s) {
|
||||
try {
|
||||
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true));
|
||||
deflater.write(s.getBytes(StandardCharsets.UTF_8));
|
||||
deflater.finish();
|
||||
return b.toByteArray();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new Saml2Exception("Unable to deflate string", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static String samlInflate(byte[] b) {
|
||||
try {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
|
||||
iout.write(b);
|
||||
iout.finish();
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new Saml2Exception("Unable to inflate string", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
|
||||
* OpenSAML 3
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
|
||||
* {@code OpenSaml4LogoutRequestResolver}
|
||||
*/
|
||||
public final class OpenSaml3LogoutRequestResolver implements Saml2LogoutRequestResolver {
|
||||
|
||||
private final OpenSamlLogoutRequestResolver logoutRequestResolver;
|
||||
|
||||
private Consumer<LogoutRequestParameters> parametersConsumer = (parameters) -> {
|
||||
};
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSaml3LogoutRequestResolver}
|
||||
*/
|
||||
public OpenSaml3LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.logoutRequestResolver = new OpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return this.logoutRequestResolver.resolve(request, authentication, (registration, logoutRequest) -> {
|
||||
logoutRequest.setIssueInstant(new DateTime(this.clock.millis()));
|
||||
this.parametersConsumer
|
||||
.accept(new LogoutRequestParameters(request, registration, authentication, logoutRequest));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
|
||||
* @param parametersConsumer a consumer that accepts an
|
||||
* {@link LogoutRequestParameters}
|
||||
*/
|
||||
public void setParametersConsumer(Consumer<LogoutRequestParameters> parametersConsumer) {
|
||||
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
|
||||
this.parametersConsumer = parametersConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Clock} for generating the issued {@link DateTime}
|
||||
* @param clock the {@link Clock} to use
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public static final class LogoutRequestParameters {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
private final LogoutRequest logoutRequest;
|
||||
|
||||
public LogoutRequestParameters(HttpServletRequest request, RelyingPartyRegistration registration,
|
||||
Authentication authentication, LogoutRequest logoutRequest) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authentication = authentication;
|
||||
this.logoutRequest = logoutRequest;
|
||||
}
|
||||
|
||||
public HttpServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public LogoutRequest getLogoutRequest() {
|
||||
return this.logoutRequest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutResponseResolver} for resolving SAML 2.0 Logout Responses with
|
||||
* OpenSAML 3
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
* @deprecated Because OpenSAML 3 has reached End-of-Life, please update to
|
||||
* {@code OpenSaml4LogoutResponseResolver}
|
||||
*/
|
||||
public final class OpenSaml3LogoutResponseResolver implements Saml2LogoutResponseResolver {
|
||||
|
||||
private final OpenSamlLogoutResponseResolver logoutResponseResolver;
|
||||
|
||||
private Consumer<LogoutResponseParameters> parametersConsumer = (parameters) -> {
|
||||
};
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSaml3LogoutResponseResolver}
|
||||
*/
|
||||
public OpenSaml3LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return this.logoutResponseResolver.resolve(request, authentication, (registration, logoutResponse) -> {
|
||||
logoutResponse.setIssueInstant(new DateTime(this.clock.millis()));
|
||||
this.parametersConsumer
|
||||
.accept(new LogoutResponseParameters(request, registration, authentication, logoutResponse));
|
||||
});
|
||||
}
|
||||
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutResponse}
|
||||
* @param parametersConsumer a consumer that accepts an
|
||||
* {@link LogoutResponseParameters}
|
||||
*/
|
||||
public void setParametersConsumer(Consumer<LogoutResponseParameters> parametersConsumer) {
|
||||
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
|
||||
this.parametersConsumer = parametersConsumer;
|
||||
}
|
||||
|
||||
public static final class LogoutResponseParameters {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
private final LogoutResponse logoutResponse;
|
||||
|
||||
public LogoutResponseParameters(HttpServletRequest request, RelyingPartyRegistration registration,
|
||||
Authentication authentication, LogoutResponse logoutResponse) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authentication = authentication;
|
||||
this.logoutResponse = logoutResponse;
|
||||
}
|
||||
|
||||
public HttpServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public LogoutResponse getLogoutResponse() {
|
||||
return this.logoutResponse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSaml3LogoutRequestResolver}
|
||||
*/
|
||||
public class OpenSaml3LogoutRequestResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
@Test
|
||||
public void resolveWhenCustomParametersConsumerThenUses() {
|
||||
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
|
||||
HttpServletRequest request = new MockHttpServletRequest();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication);
|
||||
assertThat(logoutRequest.getId()).isEqualTo("myid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setParametersConsumerWhenNullThenIllegalArgument() {
|
||||
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver.LogoutResponseParameters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSaml3LogoutResponseResolver}
|
||||
*/
|
||||
public class OpenSaml3LogoutResponseResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
@Test
|
||||
public void resolveWhenCustomParametersConsumerThenUses() {
|
||||
OpenSaml3LogoutResponseResolver logoutResponseResolver = new OpenSaml3LogoutResponseResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
|
||||
logoutResponseResolver.setParametersConsumer(parametersConsumer);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
request.setParameter("SAMLRequest",
|
||||
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication);
|
||||
assertThat(logoutResponse).isNotNull();
|
||||
verify(parametersConsumer).accept(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setParametersConsumerWhenNullThenIllegalArgument() {
|
||||
OpenSaml3LogoutRequestResolver logoutRequestResolver = new OpenSaml3LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutRequestResolver} for resolving SAML 2.0 Logout Requests with
|
||||
* OpenSAML 4
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class OpenSaml4LogoutRequestResolver implements Saml2LogoutRequestResolver {
|
||||
|
||||
private final OpenSamlLogoutRequestResolver logoutRequestResolver;
|
||||
|
||||
private Consumer<LogoutRequestParameters> parametersConsumer = (parameters) -> {
|
||||
};
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSaml4LogoutRequestResolver}
|
||||
*/
|
||||
public OpenSaml4LogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.logoutRequestResolver = new OpenSamlLogoutRequestResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return this.logoutRequestResolver.resolve(request, authentication, (registration, logoutRequest) -> {
|
||||
logoutRequest.setIssueInstant(Instant.now(this.clock));
|
||||
this.parametersConsumer
|
||||
.accept(new LogoutRequestParameters(request, registration, authentication, logoutRequest));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutRequest}
|
||||
* @param parametersConsumer a consumer that accepts an
|
||||
* {@link LogoutRequestParameters}
|
||||
*/
|
||||
public void setParametersConsumer(Consumer<LogoutRequestParameters> parametersConsumer) {
|
||||
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
|
||||
this.parametersConsumer = parametersConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Clock} for determining the issued {@link Instant}
|
||||
* @param clock the {@link Clock} to use
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public static final class LogoutRequestParameters {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
private final LogoutRequest logoutRequest;
|
||||
|
||||
public LogoutRequestParameters(HttpServletRequest request, RelyingPartyRegistration registration,
|
||||
Authentication authentication, LogoutRequest logoutRequest) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authentication = authentication;
|
||||
this.logoutRequest = logoutRequest;
|
||||
}
|
||||
|
||||
public HttpServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public LogoutRequest getLogoutRequest() {
|
||||
return this.logoutRequest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Saml2LogoutResponseResolver} for resolving SAML 2.0 Logout Responses with
|
||||
* OpenSAML 4
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.6
|
||||
*/
|
||||
public final class OpenSaml4LogoutResponseResolver implements Saml2LogoutResponseResolver {
|
||||
|
||||
private final OpenSamlLogoutResponseResolver logoutResponseResolver;
|
||||
|
||||
private Consumer<LogoutResponseParameters> parametersConsumer = (parameters) -> {
|
||||
};
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* Construct a {@link OpenSaml4LogoutResponseResolver}
|
||||
*/
|
||||
public OpenSaml4LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
||||
this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication) {
|
||||
return this.logoutResponseResolver.resolve(request, authentication, (registration, logoutResponse) -> {
|
||||
logoutResponse.setIssueInstant(Instant.now(this.clock));
|
||||
this.parametersConsumer
|
||||
.accept(new LogoutResponseParameters(request, registration, authentication, logoutResponse));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Consumer} for modifying the OpenSAML {@link LogoutResponse}
|
||||
* @param parametersConsumer a consumer that accepts an
|
||||
* {@link LogoutResponseParameters}
|
||||
*/
|
||||
public void setParametersConsumer(Consumer<LogoutResponseParameters> parametersConsumer) {
|
||||
Assert.notNull(parametersConsumer, "parametersConsumer cannot be null");
|
||||
this.parametersConsumer = parametersConsumer;
|
||||
}
|
||||
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public static final class LogoutResponseParameters {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private final RelyingPartyRegistration registration;
|
||||
|
||||
private final Authentication authentication;
|
||||
|
||||
private final LogoutResponse logoutResponse;
|
||||
|
||||
public LogoutResponseParameters(HttpServletRequest request, RelyingPartyRegistration registration,
|
||||
Authentication authentication, LogoutResponse logoutResponse) {
|
||||
this.request = request;
|
||||
this.registration = registration;
|
||||
this.authentication = authentication;
|
||||
this.logoutResponse = logoutResponse;
|
||||
}
|
||||
|
||||
public HttpServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public LogoutResponse getLogoutResponse() {
|
||||
return this.logoutResponse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSaml4LogoutRequestResolver}
|
||||
*/
|
||||
public class OpenSaml4LogoutRequestResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
@Test
|
||||
public void resolveWhenCustomParametersConsumerThenUses() {
|
||||
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid"));
|
||||
HttpServletRequest request = new MockHttpServletRequest();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication);
|
||||
assertThat(logoutRequest.getId()).isEqualTo("myid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setParametersConsumerWhenNullThenIllegalArgument() {
|
||||
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver.LogoutResponseParameters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSaml4LogoutResponseResolver}
|
||||
*/
|
||||
public class OpenSaml4LogoutResponseResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
@Test
|
||||
public void resolveWhenCustomParametersConsumerThenUses() {
|
||||
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
Consumer<LogoutResponseParameters> parametersConsumer = mock(Consumer.class);
|
||||
logoutResponseResolver.setParametersConsumer(parametersConsumer);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
request.setParameter("SAMLRequest",
|
||||
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutResponse logoutResponse = logoutResponseResolver.resolve(request, authentication);
|
||||
assertThat(logoutResponse).isNotNull();
|
||||
verify(parametersConsumer).accept(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setParametersConsumerWhenNullThenIllegalArgument() {
|
||||
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null));
|
||||
}
|
||||
|
||||
}
|
|
@ -54,6 +54,8 @@ import org.opensaml.saml.saml2.core.EncryptedAssertion;
|
|||
import org.opensaml.saml.saml2.core.EncryptedAttribute;
|
||||
import org.opensaml.saml.saml2.core.EncryptedID;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.Status;
|
||||
|
@ -63,6 +65,10 @@ import org.opensaml.saml.saml2.core.SubjectConfirmation;
|
|||
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
|
||||
import org.opensaml.saml.saml2.core.impl.AttributeBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.AttributeStatementBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.LogoutResponseBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.StatusBuilder;
|
||||
import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder;
|
||||
import org.opensaml.saml.saml2.encryption.Encrypter;
|
||||
|
@ -83,6 +89,7 @@ import org.springframework.security.saml2.Saml2Exception;
|
|||
import org.springframework.security.saml2.core.OpenSamlInitializationService;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
public final class TestOpenSamlObjects {
|
||||
|
||||
|
@ -93,7 +100,7 @@ public final class TestOpenSamlObjects {
|
|||
|
||||
private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias";
|
||||
|
||||
private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
|
||||
public static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias";
|
||||
|
||||
private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp";
|
||||
|
||||
|
@ -221,7 +228,7 @@ public final class TestOpenSamlObjects {
|
|||
return signable;
|
||||
}
|
||||
|
||||
static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
|
||||
public static <T extends SignableSAMLObject> T signed(T signable, Saml2X509Credential credential, String entityId) {
|
||||
return signed(signable, credential, entityId, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
|
||||
}
|
||||
|
||||
|
@ -342,6 +349,41 @@ public final class TestOpenSamlObjects {
|
|||
return status;
|
||||
}
|
||||
|
||||
public static LogoutRequest assertingPartyLogoutRequest(RelyingPartyRegistration registration) {
|
||||
LogoutRequestBuilder logoutRequestBuilder = new LogoutRequestBuilder();
|
||||
LogoutRequest logoutRequest = logoutRequestBuilder.buildObject();
|
||||
logoutRequest.setID("id");
|
||||
NameIDBuilder nameIdBuilder = new NameIDBuilder();
|
||||
NameID nameId = nameIdBuilder.buildObject();
|
||||
nameId.setValue("user");
|
||||
logoutRequest.setNameID(nameId);
|
||||
IssuerBuilder issuerBuilder = new IssuerBuilder();
|
||||
Issuer issuer = issuerBuilder.buildObject();
|
||||
issuer.setValue(registration.getAssertingPartyDetails().getEntityId());
|
||||
logoutRequest.setIssuer(issuer);
|
||||
logoutRequest.setDestination(registration.getSingleLogoutServiceLocation());
|
||||
return logoutRequest;
|
||||
}
|
||||
|
||||
public static LogoutResponse assertingPartyLogoutResponse(RelyingPartyRegistration registration) {
|
||||
LogoutResponseBuilder logoutResponseBuilder = new LogoutResponseBuilder();
|
||||
LogoutResponse logoutResponse = logoutResponseBuilder.buildObject();
|
||||
logoutResponse.setID("id");
|
||||
StatusBuilder statusBuilder = new StatusBuilder();
|
||||
StatusCodeBuilder statusCodeBuilder = new StatusCodeBuilder();
|
||||
StatusCode code = statusCodeBuilder.buildObject();
|
||||
code.setValue(StatusCode.SUCCESS);
|
||||
Status status = statusBuilder.buildObject();
|
||||
status.setStatusCode(code);
|
||||
logoutResponse.setStatus(status);
|
||||
IssuerBuilder issuerBuilder = new IssuerBuilder();
|
||||
Issuer issuer = issuerBuilder.buildObject();
|
||||
issuer.setValue(registration.getAssertingPartyDetails().getEntityId());
|
||||
logoutResponse.setIssuer(issuer);
|
||||
logoutResponse.setDestination(registration.getSingleLogoutServiceResponseLocation());
|
||||
return logoutResponse;
|
||||
}
|
||||
|
||||
static <T extends XMLObject> T build(QName qName) {
|
||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSamlLogoutRequestValidator}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class OpenSamlLogoutRequestValidatorTests {
|
||||
|
||||
private final OpenSamlLogoutRequestValidator manager = new OpenSamlLogoutRequestValidator();
|
||||
|
||||
@Test
|
||||
public void handleWhenPostBindingThenValidates() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
sign(logoutRequest, registration);
|
||||
Saml2LogoutRequest request = post(logoutRequest, registration);
|
||||
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
|
||||
registration, authentication(registration));
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
|
||||
RelyingPartyRegistration registration = registration()
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
|
||||
.build();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
Saml2LogoutRequest request = redirect(logoutRequest, registration, OpenSamlSigningUtils.sign(registration));
|
||||
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
|
||||
registration, authentication(registration));
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
logoutRequest.getIssuer().setValue("wrong");
|
||||
sign(logoutRequest, registration);
|
||||
Saml2LogoutRequest request = post(logoutRequest, registration);
|
||||
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
|
||||
registration, authentication(registration));
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isTrue();
|
||||
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenMismatchedUserThenInvalidRequestError() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
logoutRequest.getNameID().setValue("wrong");
|
||||
sign(logoutRequest, registration);
|
||||
Saml2LogoutRequest request = post(logoutRequest, registration);
|
||||
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
|
||||
registration, authentication(registration));
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isTrue();
|
||||
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenMissingUserThenSubjectNotFoundError() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
logoutRequest.setNameID(null);
|
||||
sign(logoutRequest, registration);
|
||||
Saml2LogoutRequest request = post(logoutRequest, registration);
|
||||
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
|
||||
registration, authentication(registration));
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isTrue();
|
||||
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.SUBJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
logoutRequest.setDestination("wrong");
|
||||
sign(logoutRequest, registration);
|
||||
Saml2LogoutRequest request = post(logoutRequest, registration);
|
||||
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request,
|
||||
registration, authentication(registration));
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isTrue();
|
||||
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder registration() {
|
||||
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
|
||||
return builder.assertingPartyDetails((party) -> party
|
||||
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
|
||||
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
|
||||
}
|
||||
|
||||
private Authentication authentication(RelyingPartyRegistration registration) {
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
|
||||
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
|
||||
return new Saml2Authentication(principal, "response", new ArrayList<>());
|
||||
}
|
||||
|
||||
private Saml2LogoutRequest post(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
|
||||
return Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest(Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8))).build();
|
||||
}
|
||||
|
||||
private Saml2LogoutRequest redirect(LogoutRequest logoutRequest, RelyingPartyRegistration registration,
|
||||
QueryParametersPartial partial) {
|
||||
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest)));
|
||||
Map<String, String> parameters = partial.param("SAMLRequest", serialized).parameters();
|
||||
return Saml2LogoutRequest.withRelyingPartyRegistration(registration).samlRequest(serialized)
|
||||
.parameters((params) -> params.putAll(parameters)).build();
|
||||
}
|
||||
|
||||
private void sign(LogoutRequest logoutRequest, RelyingPartyRegistration registration) {
|
||||
TestOpenSamlObjects.signed(logoutRequest, registration.getSigningX509Credentials().iterator().next(),
|
||||
registration.getAssertingPartyDetails().getEntityId());
|
||||
}
|
||||
|
||||
private String serialize(XMLObject object) {
|
||||
return OpenSamlSigningUtils.serialize(object);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSamlLogoutResponseValidator}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class OpenSamlLogoutResponseValidatorTests {
|
||||
|
||||
private final OpenSamlLogoutResponseValidator manager = new OpenSamlLogoutResponseValidator();
|
||||
|
||||
@Test
|
||||
public void handleWhenAuthenticatedThenHandles() {
|
||||
RelyingPartyRegistration registration = signing(verifying(registration())).build();
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
|
||||
.build();
|
||||
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
|
||||
sign(logoutResponse, registration);
|
||||
Saml2LogoutResponse response = post(logoutResponse, registration);
|
||||
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
|
||||
logoutRequest, registration);
|
||||
this.manager.validate(parameters);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenRedirectBindingThenValidatesSignatureParameter() {
|
||||
RelyingPartyRegistration registration = signing(verifying(registration()))
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
|
||||
.build();
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
|
||||
.build();
|
||||
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
|
||||
Saml2LogoutResponse response = redirect(logoutResponse, registration, OpenSamlSigningUtils.sign(registration));
|
||||
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
|
||||
logoutRequest, registration);
|
||||
this.manager.validate(parameters);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenInvalidIssuerThenInvalidSignatureError() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
|
||||
.build();
|
||||
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
|
||||
logoutResponse.getIssuer().setValue("wrong");
|
||||
sign(logoutResponse, registration);
|
||||
Saml2LogoutResponse response = post(logoutResponse, registration);
|
||||
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
|
||||
logoutRequest, registration);
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isTrue();
|
||||
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenMismatchedDestinationThenInvalidDestinationError() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
|
||||
.build();
|
||||
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
|
||||
logoutResponse.setDestination("wrong");
|
||||
sign(logoutResponse, registration);
|
||||
Saml2LogoutResponse response = post(logoutResponse, registration);
|
||||
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
|
||||
logoutRequest, registration);
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isTrue();
|
||||
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWhenStatusNotSuccessThenInvalidResponseError() {
|
||||
RelyingPartyRegistration registration = registration().build();
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration).id("id")
|
||||
.build();
|
||||
LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration);
|
||||
logoutResponse.getStatus().getStatusCode().setValue(StatusCode.UNKNOWN_PRINCIPAL);
|
||||
sign(logoutResponse, registration);
|
||||
Saml2LogoutResponse response = post(logoutResponse, registration);
|
||||
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response,
|
||||
logoutRequest, registration);
|
||||
Saml2LogoutValidatorResult result = this.manager.validate(parameters);
|
||||
assertThat(result.hasErrors()).isTrue();
|
||||
assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE);
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder registration() {
|
||||
return signing(verifying(TestRelyingPartyRegistrations.noCredentials()))
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST));
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) {
|
||||
return builder.assertingPartyDetails((party) -> party
|
||||
.verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())));
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) {
|
||||
return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential()));
|
||||
}
|
||||
|
||||
private Saml2LogoutResponse post(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
|
||||
return Saml2LogoutResponse.withRelyingPartyRegistration(registration)
|
||||
.samlResponse(Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Saml2LogoutResponse redirect(LogoutResponse logoutResponse, RelyingPartyRegistration registration,
|
||||
QueryParametersPartial partial) {
|
||||
String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse)));
|
||||
Map<String, String> parameters = partial.param("SAMLResponse", serialized).parameters();
|
||||
return Saml2LogoutResponse.withRelyingPartyRegistration(registration).samlResponse(serialized)
|
||||
.parameters((params) -> params.putAll(parameters)).build();
|
||||
}
|
||||
|
||||
private void sign(LogoutResponse logoutResponse, RelyingPartyRegistration registration) {
|
||||
TestOpenSamlObjects.signed(logoutResponse, registration.getSigningX509Credentials().iterator().next(),
|
||||
registration.getAssertingPartyDetails().getEntityId());
|
||||
}
|
||||
|
||||
private String serialize(XMLObject object) {
|
||||
return OpenSamlSigningUtils.serialize(object);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.authentication.logout;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.core.xml.io.Marshaller;
|
||||
import org.opensaml.core.xml.io.MarshallingException;
|
||||
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
|
||||
import org.opensaml.security.SecurityException;
|
||||
import org.opensaml.security.credential.BasicCredential;
|
||||
import org.opensaml.security.credential.Credential;
|
||||
import org.opensaml.security.credential.CredentialSupport;
|
||||
import org.opensaml.security.credential.UsageType;
|
||||
import org.opensaml.xmlsec.SignatureSigningParameters;
|
||||
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
|
||||
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
|
||||
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
|
||||
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
|
||||
import org.opensaml.xmlsec.signature.SignableXMLObject;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureSupport;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for signing SAML components with OpenSAML
|
||||
*
|
||||
* For internal use only.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
final class OpenSamlSigningUtils {
|
||||
|
||||
static String serialize(XMLObject object) {
|
||||
try {
|
||||
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
|
||||
Element element = marshaller.marshall(object);
|
||||
return SerializeSupport.nodeToString(element);
|
||||
}
|
||||
catch (MarshallingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static <O extends SignableXMLObject> O sign(O object, RelyingPartyRegistration relyingPartyRegistration) {
|
||||
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
|
||||
try {
|
||||
SignatureSupport.signObject(object, parameters);
|
||||
return object;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static QueryParametersPartial sign(RelyingPartyRegistration registration) {
|
||||
return new QueryParametersPartial(registration);
|
||||
}
|
||||
|
||||
private static SignatureSigningParameters resolveSigningParameters(
|
||||
RelyingPartyRegistration relyingPartyRegistration) {
|
||||
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
|
||||
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
|
||||
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
|
||||
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
|
||||
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
|
||||
CriteriaSet criteria = new CriteriaSet();
|
||||
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
|
||||
signingConfiguration.setSigningCredentials(credentials);
|
||||
signingConfiguration.setSignatureAlgorithms(algorithms);
|
||||
signingConfiguration.setSignatureReferenceDigestMethods(digests);
|
||||
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
|
||||
criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration));
|
||||
try {
|
||||
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
|
||||
Assert.notNull(parameters, "Failed to resolve any signing credential");
|
||||
return parameters;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
|
||||
List<Credential> credentials = new ArrayList<>();
|
||||
for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
|
||||
X509Certificate certificate = x509Credential.getCertificate();
|
||||
PrivateKey privateKey = x509Credential.getPrivateKey();
|
||||
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
|
||||
credential.setEntityId(relyingPartyRegistration.getEntityId());
|
||||
credential.setUsageType(UsageType.SIGNING);
|
||||
credentials.add(credential);
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
static class QueryParametersPartial {
|
||||
|
||||
final RelyingPartyRegistration registration;
|
||||
|
||||
final Map<String, String> components = new LinkedHashMap<>();
|
||||
|
||||
QueryParametersPartial(RelyingPartyRegistration registration) {
|
||||
this.registration = registration;
|
||||
}
|
||||
|
||||
QueryParametersPartial param(String key, String value) {
|
||||
this.components.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
Map<String, String> parameters() {
|
||||
SignatureSigningParameters parameters = resolveSigningParameters(this.registration);
|
||||
Credential credential = parameters.getSigningCredential();
|
||||
String algorithmUri = parameters.getSignatureAlgorithm();
|
||||
this.components.put("SigAlg", algorithmUri);
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
|
||||
for (Map.Entry<String, String> component : this.components.entrySet()) {
|
||||
builder.queryParam(component.getKey(),
|
||||
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
String queryString = builder.build(true).toString().substring(1);
|
||||
try {
|
||||
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
|
||||
queryString.getBytes(StandardCharsets.UTF_8));
|
||||
String b64Signature = Saml2Utils.samlEncode(rawSignature);
|
||||
this.components.put("Signature", b64Signature);
|
||||
}
|
||||
catch (SecurityException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
return this.components;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private OpenSamlSigningUtils() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -41,7 +41,8 @@ public class OpenSamlMetadataResolverTests {
|
|||
.contains("<md:KeyDescriptor use=\"encryption\">")
|
||||
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
|
||||
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
|
||||
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -56,7 +57,8 @@ public class OpenSamlMetadataResolverTests {
|
|||
.contains("WantAssertionsSigned=\"true\"").doesNotContain("<md:KeyDescriptor use=\"signing\">")
|
||||
.doesNotContain("<md:KeyDescriptor use=\"encryption\">")
|
||||
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"")
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"");
|
||||
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
|
||||
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,17 +37,23 @@ public final class TestRelyingPartyRegistrations {
|
|||
String apEntityId = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php";
|
||||
Saml2X509Credential verificationCertificate = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
||||
String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php";
|
||||
String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo";
|
||||
return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(rpEntityId)
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.credentials((c) -> c.add(signingCredential))
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation).credentials((c) -> c.add(signingCredential))
|
||||
.providerDetails((c) -> c.entityId(apEntityId).webSsoUrl(singleSignOnServiceLocation))
|
||||
.credentials((c) -> c.add(verificationCertificate));
|
||||
}
|
||||
|
||||
public static RelyingPartyRegistration.Builder noCredentials() {
|
||||
return RelyingPartyRegistration.withRegistrationId("registration-id").entityId("rp-entity-id")
|
||||
.assertionConsumerServiceLocation("https://rp.example.org/acs").assertingPartyDetails((party) -> party
|
||||
.entityId("ap-entity-id").singleSignOnServiceLocation("https://ap.example.org/sso"));
|
||||
.singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request")
|
||||
.singleLogoutServiceResponseLocation("https://rp.example.org/logout/saml2/response")
|
||||
.assertionConsumerServiceLocation("https://rp.example.org/acs")
|
||||
.assertingPartyDetails((party) -> party.entityId("ap-entity-id")
|
||||
.singleSignOnServiceLocation("https://ap.example.org/sso")
|
||||
.singleLogoutServiceLocation("https://ap.example.org/logout/saml2/request")
|
||||
.singleLogoutServiceResponseLocation("https://ap.example.org/logout/saml2/response"));
|
||||
}
|
||||
|
||||
public static RelyingPartyRegistration.Builder full() {
|
||||
|
|
|
@ -52,6 +52,9 @@ public class DefaultRelyingPartyRegistrationResolverTests {
|
|||
.isEqualTo("http://localhost/saml2/service-provider-metadata/" + this.registration.getRegistrationId());
|
||||
assertThat(registration.getAssertionConsumerServiceLocation())
|
||||
.isEqualTo("http://localhost/login/saml2/sso/" + this.registration.getRegistrationId());
|
||||
assertThat(registration.getSingleLogoutServiceLocation()).isEqualTo("http://localhost/logout/saml2/slo");
|
||||
assertThat(registration.getSingleLogoutServiceResponseLocation())
|
||||
.isEqualTo("http://localhost/logout/saml2/slo");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpSessionLogoutRequestRepository}
|
||||
*/
|
||||
public class HttpSessionLogoutRequestRepositoryTests {
|
||||
|
||||
HttpSessionLogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
|
||||
|
||||
@Test
|
||||
public void loadLogoutRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository.loadLogoutRequest(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadLogoutRequestWhenNotSavedThenReturnNull() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addParameter("RelayState", "state-1234");
|
||||
Saml2LogoutRequest logoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
|
||||
assertThat(logoutRequest).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadLogoutRequestWhenSavedThenReturnLogoutRequest() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
|
||||
request.addParameter("RelayState", logoutRequest.getRelayState());
|
||||
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
|
||||
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadLogoutRequestWhenMultipleSavedThenReplacesLogoutRequest() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Saml2LogoutRequest one = createLogoutRequest().relayState("state-1122").build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(one, request, response);
|
||||
Saml2LogoutRequest two = createLogoutRequest().relayState("state-3344").build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(two, request, response);
|
||||
request.setParameter("RelayState", one.getRelayState());
|
||||
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isNull();
|
||||
request.setParameter("RelayState", two.getRelayState());
|
||||
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isEqualTo(two);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadLogoutRequestWhenSavedAndStateParameterNullThenReturnNull() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, new MockHttpServletResponse());
|
||||
assertThat(this.logoutRequestRepository.loadLogoutRequest(request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLogoutRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository
|
||||
.saveLogoutRequest(logoutRequest, null, new MockHttpServletResponse()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLogoutRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() {
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository
|
||||
.saveLogoutRequest(logoutRequest, new MockHttpServletRequest(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLogoutRequestWhenStateNullThenThrowIllegalArgumentException() {
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().relayState(null).build();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.logoutRequestRepository
|
||||
.saveLogoutRequest(logoutRequest, new MockHttpServletRequest(), new MockHttpServletResponse()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLogoutRequestWhenNotNullThenSaved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, new MockHttpServletResponse());
|
||||
request.addParameter("RelayState", logoutRequest.getRelayState());
|
||||
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
|
||||
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLogoutRequestWhenNoExistingSessionAndDistributedSessionThenSaved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setSession(new MockDistributedHttpSession());
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, new MockHttpServletResponse());
|
||||
request.addParameter("RelayState", logoutRequest.getRelayState());
|
||||
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
|
||||
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLogoutRequestWhenExistingSessionAndDistributedSessionThenSaved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setSession(new MockDistributedHttpSession());
|
||||
Saml2LogoutRequest logoutRequest1 = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest1, request, new MockHttpServletResponse());
|
||||
Saml2LogoutRequest logoutRequest2 = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest2, request, new MockHttpServletResponse());
|
||||
request.addParameter("RelayState", logoutRequest2.getRelayState());
|
||||
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
|
||||
assertThat(loadedLogoutRequest).isEqualTo(logoutRequest2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveLogoutRequestWhenNullThenRemoved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
|
||||
request.addParameter("RelayState", logoutRequest.getRelayState());
|
||||
this.logoutRequestRepository.saveLogoutRequest(null, request, response);
|
||||
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
|
||||
assertThat(loadedLogoutRequest).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeLogoutRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> this.logoutRequestRepository.removeLogoutRequest(null, new MockHttpServletResponse()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeLogoutRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.logoutRequestRepository.removeLogoutRequest(new MockHttpServletRequest(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeLogoutRequestWhenSavedThenRemoved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
|
||||
request.addParameter("RelayState", logoutRequest.getRelayState());
|
||||
Saml2LogoutRequest removedLogoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
|
||||
Saml2LogoutRequest loadedLogoutRequest = this.logoutRequestRepository.loadLogoutRequest(request);
|
||||
assertThat(removedLogoutRequest).isNotNull();
|
||||
assertThat(loadedLogoutRequest).isNull();
|
||||
}
|
||||
|
||||
// gh-5263
|
||||
@Test
|
||||
public void removeLogoutRequestWhenSavedThenRemovedFromSession() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Saml2LogoutRequest logoutRequest = createLogoutRequest().build();
|
||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, request, response);
|
||||
request.addParameter("RelayState", logoutRequest.getRelayState());
|
||||
Saml2LogoutRequest removedLogoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
|
||||
String sessionAttributeName = HttpSessionLogoutRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST";
|
||||
assertThat(removedLogoutRequest).isNotNull();
|
||||
assertThat(request.getSession().getAttribute(sessionAttributeName)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeLogoutRequestWhenNotSavedThenNotRemoved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addParameter("RelayState", "state-1234");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Saml2LogoutRequest removedLogoutRequest = this.logoutRequestRepository.removeLogoutRequest(request, response);
|
||||
assertThat(removedLogoutRequest).isNull();
|
||||
}
|
||||
|
||||
private Saml2LogoutRequest.Builder createLogoutRequest() {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
return Saml2LogoutRequest.withRelyingPartyRegistration(registration).samlRequest("request").id("id")
|
||||
.parameters((params) -> params.put("RelayState", "state-1234"));
|
||||
}
|
||||
|
||||
static class MockDistributedHttpSession extends MockHttpSession {
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String name) {
|
||||
return wrap(super.getAttribute(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value) {
|
||||
super.setAttribute(name, wrap(value));
|
||||
}
|
||||
|
||||
private Object wrap(Object object) {
|
||||
if (object instanceof Map) {
|
||||
object = new HashMap<>((Map<Object, Object>) object);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSamlLogoutRequestResolver}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class OpenSamlLogoutRequestResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
OpenSamlLogoutRequestResolver logoutRequestResolver = new OpenSamlLogoutRequestResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
|
||||
@Test
|
||||
public void resolveRedirectWhenAuthenticatedThenIncludesName() {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
Saml2Authentication authentication = authentication(registration);
|
||||
HttpServletRequest request = new MockHttpServletRequest();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutRequest saml2LogoutRequest = this.logoutRequestResolver.resolve(request, authentication);
|
||||
assertThat(saml2LogoutRequest.getParameter("SigAlg")).isNotNull();
|
||||
assertThat(saml2LogoutRequest.getParameter("Signature")).isNotNull();
|
||||
assertThat(saml2LogoutRequest.getParameter("RelayState")).isNotNull();
|
||||
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
|
||||
LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding);
|
||||
assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvePostWhenAuthenticatedThenIncludesName() {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
|
||||
Saml2Authentication authentication = authentication(registration);
|
||||
HttpServletRequest request = new MockHttpServletRequest();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutRequest saml2LogoutRequest = this.logoutRequestResolver.resolve(request, authentication);
|
||||
assertThat(saml2LogoutRequest.getParameter("SigAlg")).isNull();
|
||||
assertThat(saml2LogoutRequest.getParameter("Signature")).isNull();
|
||||
assertThat(saml2LogoutRequest.getParameter("RelayState")).isNotNull();
|
||||
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
|
||||
LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding);
|
||||
assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName());
|
||||
}
|
||||
|
||||
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
|
||||
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
|
||||
return new Saml2Authentication(principal, "response", new ArrayList<>());
|
||||
}
|
||||
|
||||
private LogoutRequest getLogoutRequest(String samlRequest, Saml2MessageBinding binding) {
|
||||
if (binding == Saml2MessageBinding.REDIRECT) {
|
||||
samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest));
|
||||
}
|
||||
else {
|
||||
samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8);
|
||||
}
|
||||
try {
|
||||
Document document = XMLObjectProviderRegistrySupport.getParserPool()
|
||||
.parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8)));
|
||||
Element element = document.getDocumentElement();
|
||||
return (LogoutRequest) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element)
|
||||
.unmarshall(element);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml.saml2.core.LogoutResponse;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link OpenSamlLogoutResponseResolver}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class OpenSamlLogoutResponseResolverTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
OpenSamlLogoutResponseResolver logoutResponseResolver = new OpenSamlLogoutResponseResolver(
|
||||
this.relyingPartyRegistrationResolver);
|
||||
|
||||
@Test
|
||||
public void resolveRedirectWhenAuthenticatedThenSuccess() {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
request.setParameter("SAMLRequest",
|
||||
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
|
||||
request.setParameter("RelayState", "abcd");
|
||||
Authentication authentication = authentication(registration);
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutResponse saml2LogoutResponse = this.logoutResponseResolver.resolve(request, authentication);
|
||||
assertThat(saml2LogoutResponse.getParameter("SigAlg")).isNotNull();
|
||||
assertThat(saml2LogoutResponse.getParameter("Signature")).isNotNull();
|
||||
assertThat(saml2LogoutResponse.getParameter("RelayState")).isSameAs("abcd");
|
||||
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
|
||||
LogoutResponse logoutResponse = getLogoutResponse(saml2LogoutResponse.getSamlResponse(), binding);
|
||||
assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvePostWhenAuthenticatedThenSuccess() {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
||||
request.setParameter("SAMLRequest",
|
||||
Saml2Utils.samlEncode(OpenSamlSigningUtils.serialize(logoutRequest).getBytes()));
|
||||
request.setParameter("RelayState", "abcd");
|
||||
Authentication authentication = authentication(registration);
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
Saml2LogoutResponse saml2LogoutResponse = this.logoutResponseResolver.resolve(request, authentication);
|
||||
assertThat(saml2LogoutResponse.getParameter("SigAlg")).isNull();
|
||||
assertThat(saml2LogoutResponse.getParameter("Signature")).isNull();
|
||||
assertThat(saml2LogoutResponse.getParameter("RelayState")).isSameAs("abcd");
|
||||
Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding();
|
||||
LogoutResponse logoutResponse = getLogoutResponse(saml2LogoutResponse.getSamlResponse(), binding);
|
||||
assertThat(logoutResponse.getStatus().getStatusCode().getValue()).isEqualTo(StatusCode.SUCCESS);
|
||||
}
|
||||
|
||||
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
|
||||
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
|
||||
return new Saml2Authentication(principal, "response", new ArrayList<>());
|
||||
}
|
||||
|
||||
private LogoutResponse getLogoutResponse(String saml2Response, Saml2MessageBinding binding) {
|
||||
if (binding == Saml2MessageBinding.REDIRECT) {
|
||||
saml2Response = Saml2Utils.samlInflate(Saml2Utils.samlDecode(saml2Response));
|
||||
}
|
||||
else {
|
||||
saml2Response = new String(Saml2Utils.samlDecode(saml2Response), StandardCharsets.UTF_8);
|
||||
}
|
||||
try {
|
||||
Document document = XMLObjectProviderRegistrySupport.getParserPool()
|
||||
.parse(new ByteArrayInputStream(saml2Response.getBytes(StandardCharsets.UTF_8)));
|
||||
Element element = document.getDocumentElement();
|
||||
return (LogoutResponse) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element)
|
||||
.unmarshall(element);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link Saml2LogoutRequestFilter}
|
||||
*/
|
||||
public class Saml2LogoutRequestFilterTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
Saml2LogoutRequestValidator logoutRequestValidator = mock(Saml2LogoutRequestValidator.class);
|
||||
|
||||
LogoutHandler logoutHandler = mock(LogoutHandler.class);
|
||||
|
||||
Saml2LogoutResponseResolver logoutResponseResolver = mock(Saml2LogoutResponseResolver.class);
|
||||
|
||||
Saml2LogoutRequestFilter logoutRequestProcessingFilter = new Saml2LogoutRequestFilter(
|
||||
this.relyingPartyRegistrationResolver, this.logoutRequestValidator, this.logoutResponseResolver,
|
||||
this.logoutHandler);
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenSamlRequestThenRedirects() throws Exception {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
request.setParameter("SAMLRequest", "request");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
given(this.logoutRequestValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
|
||||
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
|
||||
.samlResponse("response").build();
|
||||
given(this.logoutResponseResolver.resolve(any(), any())).willReturn(logoutResponse);
|
||||
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verify(this.logoutRequestValidator).validate(any());
|
||||
verify(this.logoutHandler).logout(any(), any(), any());
|
||||
verify(this.logoutResponseResolver).resolve(any(), any());
|
||||
String content = response.getHeader("Location");
|
||||
assertThat(content).contains("SAMLResponse");
|
||||
assertThat(content)
|
||||
.startsWith(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenSamlRequestThenPosts() throws Exception {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
request.setParameter("SAMLRequest", "request");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration);
|
||||
given(this.logoutRequestValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
|
||||
Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration)
|
||||
.samlResponse("response").build();
|
||||
given(this.logoutResponseResolver.resolve(any(), any())).willReturn(logoutResponse);
|
||||
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verify(this.logoutRequestValidator).validate(any());
|
||||
verify(this.logoutHandler).logout(any(), any(), any());
|
||||
verify(this.logoutResponseResolver).resolve(any(), any());
|
||||
String content = response.getContentAsString();
|
||||
assertThat(content).contains("SAMLResponse");
|
||||
assertThat(content).contains(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenRequestMismatchesThenNoLogout() throws Exception {
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout");
|
||||
request.setServletPath("/logout");
|
||||
request.setParameter("SAMLResponse", "response");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verifyNoInteractions(this.logoutRequestValidator, this.logoutHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNoSamlRequestOrResponseThenNoLogout() throws Exception {
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verifyNoInteractions(this.logoutRequestValidator, this.logoutHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenValidationFailsThen401() throws Exception {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
request.setParameter("SAMLRequest", "request");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(request, null)).willReturn(registration);
|
||||
given(this.logoutRequestValidator.validate(any()))
|
||||
.willReturn(Saml2LogoutValidatorResult.withErrors(new Saml2Error("error", "description")).build());
|
||||
this.logoutRequestProcessingFilter.doFilter(request, response, new MockFilterChain());
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
verifyNoInteractions(this.logoutHandler);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link Saml2LogoutResponseFilter}
|
||||
*/
|
||||
public class Saml2LogoutResponseFilterTests {
|
||||
|
||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class);
|
||||
|
||||
Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class);
|
||||
|
||||
Saml2LogoutResponseValidator logoutResponseValidator = mock(Saml2LogoutResponseValidator.class);
|
||||
|
||||
LogoutSuccessHandler logoutSuccessHandler = mock(LogoutSuccessHandler.class);
|
||||
|
||||
Saml2LogoutResponseFilter logoutResponseProcessingFilter = new Saml2LogoutResponseFilter(
|
||||
this.relyingPartyRegistrationResolver, this.logoutResponseValidator, this.logoutSuccessHandler);
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.logoutResponseProcessingFilter.setLogoutRequestRepository(this.logoutRequestRepository);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenSamlResponsePostThenLogout() throws Exception {
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
request.setParameter("SAMLResponse", "response");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(request, "registration-id")).willReturn(registration);
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest("request").build();
|
||||
given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest);
|
||||
given(this.logoutResponseValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
|
||||
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verify(this.logoutResponseValidator).validate(any());
|
||||
verify(this.logoutSuccessHandler).onLogoutSuccess(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenSamlResponseRedirectThenLogout() throws Exception {
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
request.setParameter("SAMLResponse", "response");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
|
||||
.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT).build();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(request, "registration-id")).willReturn(registration);
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest("request").build();
|
||||
given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest);
|
||||
given(this.logoutResponseValidator.validate(any())).willReturn(Saml2LogoutValidatorResult.success());
|
||||
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verify(this.logoutResponseValidator).validate(any());
|
||||
verify(this.logoutSuccessHandler).onLogoutSuccess(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenRequestMismatchesThenNoLogout() throws Exception {
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout");
|
||||
request.setServletPath("/logout");
|
||||
request.setParameter("SAMLRequest", "request");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verifyNoInteractions(this.logoutResponseValidator, this.logoutSuccessHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNoSamlRequestOrResponseThenNoLogout() throws Exception {
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verifyNoInteractions(this.logoutResponseValidator, this.logoutSuccessHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenValidatorFailsThenStops() throws Exception {
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password");
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo");
|
||||
request.setServletPath("/logout/saml2/slo");
|
||||
request.setParameter("SAMLResponse", "response");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
given(this.relyingPartyRegistrationResolver.resolve(request, "registration-id")).willReturn(registration);
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest("request").build();
|
||||
given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest);
|
||||
given(this.logoutResponseValidator.validate(any()))
|
||||
.willReturn(Saml2LogoutValidatorResult.withErrors(new Saml2Error("error", "description")).build());
|
||||
this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
|
||||
verify(this.logoutResponseValidator).validate(any());
|
||||
verifyNoInteractions(this.logoutSuccessHandler);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.saml2.provider.service.web.authentication.logout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link Saml2RelyingPartyInitiatedLogoutSuccessHandler}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class Saml2RelyingPartyInitiatedLogoutSuccessHandlerTests {
|
||||
|
||||
Saml2LogoutRequestResolver logoutRequestResolver = mock(Saml2LogoutRequestResolver.class);
|
||||
|
||||
Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class);
|
||||
|
||||
Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = new Saml2RelyingPartyInitiatedLogoutSuccessHandler(
|
||||
this.logoutRequestResolver);
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.logoutRequestSuccessHandler.setLogoutRequestRepository(this.logoutRequestRepository);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onLogoutSuccessWhenRedirectThenRedirectsToAssertingParty() throws Exception {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build();
|
||||
Authentication authentication = authentication(registration);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest("request").build();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/saml2/logout");
|
||||
request.setServletPath("/saml2/logout");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
given(this.logoutRequestResolver.resolve(any(), any())).willReturn(logoutRequest);
|
||||
this.logoutRequestSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||
String content = response.getHeader("Location");
|
||||
assertThat(content).contains("SAMLRequest");
|
||||
assertThat(content).startsWith(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onLogoutSuccessWhenPostThenPostsToAssertingParty() throws Exception {
|
||||
RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full()
|
||||
.assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build();
|
||||
Authentication authentication = authentication(registration);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest("request").build();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/saml2/logout");
|
||||
request.setServletPath("/saml2/logout");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
given(this.logoutRequestResolver.resolve(any(), any())).willReturn(logoutRequest);
|
||||
this.logoutRequestSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||
String content = response.getContentAsString();
|
||||
assertThat(content).contains("SAMLRequest");
|
||||
assertThat(content).contains(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation());
|
||||
}
|
||||
|
||||
private Saml2Authentication authentication(RelyingPartyRegistration registration) {
|
||||
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>());
|
||||
principal.setRelyingPartyRegistrationId(registration.getRegistrationId());
|
||||
return new Saml2Authentication(principal, "response", new ArrayList<>());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue