Skip to content

Commit

Permalink
Add Single Logout Support
Browse files Browse the repository at this point in the history
Closes gh-8731
  • Loading branch information
jzheaux committed Sep 13, 2021
1 parent 6488295 commit c63d618
Show file tree
Hide file tree
Showing 49 changed files with 5,738 additions and 34 deletions.
278 changes: 262 additions & 16 deletions docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
----
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading

0 comments on commit c63d618

Please sign in to comment.