Skip to content

Commit c63d618

Browse files
committed
Add Single Logout Support
Closes gh-8731
1 parent 6488295 commit c63d618

File tree

49 files changed

+5738
-34
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+5738
-34
lines changed

docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc

Lines changed: 262 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,35 +1618,281 @@ filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
16181618
[[servlet-saml2login-logout]]
16191619
=== Performing Single Logout
16201620

1621-
Spring Security does not yet support single logout.
1621+
Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
16221622

1623-
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
1623+
Briefly, there are two use cases Spring Security supports:
1624+
1625+
* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party.
1626+
Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond
1627+
* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party.
1628+
Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party.
1629+
1630+
[NOTE]
1631+
In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot.
1632+
Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser.
1633+
1634+
=== Minimal Configuration for Single Logout
1635+
1636+
To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things:
1637+
1638+
* First, the asserting party must support SAML 2.0 Single Logout
1639+
* 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
1640+
* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
1641+
1642+
==== RP-Initiated Single Logout
1643+
1644+
Given those, then for RP-initiated Single Logout, you can begin from the initial minimal example and add the following configuration:
1645+
1646+
[source,java]
1647+
----
1648+
@Value("${private.key}") RSAPrivateKey key;
1649+
@Value("${public.certificate}") X509Certificate certificate;
1650+
1651+
@Bean
1652+
RelyingPartyRegistrationRepository registrations() {
1653+
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
1654+
.fromMetadataLocation("https://ap.example.org/metadata")
1655+
.registrationId("id")
1656+
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
1657+
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
1658+
.build();
1659+
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
1660+
}
1661+
1662+
@Bean
1663+
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
1664+
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
1665+
LogoutHandler logoutResponseHandler = logoutResponseHandler(registrationResolver);
1666+
LogoutSuccessHandler logoutRequestSuccessHandler = logoutRequestSuccessHandler(registrationResolver);
1667+
1668+
http
1669+
.authorizeRequests((authorize) -> authorize
1670+
.anyRequest().authenticated()
1671+
)
1672+
.saml2Login(withDefaults())
1673+
.logout((logout) -> logout
1674+
.logoutUrl("/saml2/logout")
1675+
.logoutSuccessHandler(successHandler))
1676+
.addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class);
1677+
1678+
return http.build();
1679+
}
1680+
1681+
private LogoutSuccessHandler logoutRequestSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
1682+
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(registrationResolver);
1683+
return new Saml2LogoutRequestSuccessHandler(logoutRequestResolver);
1684+
}
1685+
1686+
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
1687+
return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
1688+
}
1689+
----
1690+
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
1691+
<2> - Second, supply a `LogoutSuccessHandler` for initiating Single Logout, sending a `saml2:LogoutRequest` to the asserting party
1692+
<3> - Third, supply the `LogoutHandler` s needed to handle the `saml2:LogoutResponse` s sent from the asserting party.
1693+
1694+
==== Runtime Expectations for RP-Initiated
1695+
1696+
Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
1697+
Your application will then do the following:
1698+
1699+
1. Logout the user and invalidate the session
1700+
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.
1701+
3. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
1702+
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
1703+
5. Redirect to any configured successful logout endpoint
1704+
1705+
[TIP]
1706+
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`.
1707+
1708+
==== AP-Initiated Single Logout
1709+
1710+
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:
1711+
1712+
[source,java]
1713+
----
1714+
@Value("${private.key}") RSAPrivateKey key;
1715+
@Value("${public.certificate}") X509Certificate certificate;
1716+
1717+
@Bean
1718+
RelyingPartyRegistrationRepository registrations() {
1719+
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
1720+
.fromMetadataLocation("https://ap.example.org/metadata")
1721+
.registrationId("id")
1722+
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
1723+
.build();
1724+
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
1725+
}
1726+
1727+
@Bean
1728+
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
1729+
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
1730+
LogoutHandler logoutRequestHandler = logoutRequestHandler(registrationResolver);
1731+
LogoutSuccessHandler logoutResponseSuccessHandler = logoutResponseSuccessHandler(registrationResolver);
1732+
1733+
http
1734+
.authorizeRequests((authorize) -> authorize
1735+
.anyRequest().authenticated()
1736+
)
1737+
.saml2Login(withDefaults())
1738+
.addFilterBefore(new Saml2LogoutRequestFilter(logoutResponseSuccessHandler, logoutRequestHandler), CsrfFilter.class);
1739+
1740+
return http.build();
1741+
}
1742+
1743+
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
1744+
return new CompositeLogoutHandler(
1745+
new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver),
1746+
new SecurityContextLogoutHandler(),
1747+
new LogoutSuccessEventPublishingLogoutHandler());
1748+
}
1749+
1750+
private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
1751+
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(registrationResolver);
1752+
return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver);
1753+
}
1754+
----
1755+
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
1756+
<2> - Second, supply the `LogoutHandler` needed to handle the `saml2:LogoutRequest` s sent from the asserting party.
1757+
<3> - Third, supply a `LogoutSuccessHandler` for completing Single Logout, sending a `saml2:LogoutResponse` to the asserting party
1758+
1759+
==== Runtime Expectations for AP-Initiated
1760+
1761+
Given the above configuration, an asserting party can send a `POST /logout/saml2` to your application that includes a `<saml2:LogoutRequest>`
1762+
Also, your application can participate in an AP-initated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
1763+
1764+
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
1765+
2. Logout the user and invalidate the session
1766+
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
1767+
4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
1768+
1769+
[TIP]
1770+
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`
1771+
1772+
[NOTE]
1773+
In the event that you need to support both logout flows, you can combine the above to configurations.
1774+
1775+
=== Configuring Logout Endpoints
1776+
1777+
There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
1778+
* `/logout` - the endpoint for initiating single logout with an asserting party
1779+
* `/logout/saml2/slo` - the endpoint for receiving logout requests or responses from an asserting party
1780+
1781+
Because the user is already logged in, the `registrationId` is already known.
1782+
For this reason, `+{registrationId}+` is not part of these URLs by default.
1783+
1784+
These URLs are customizable in the DSL.
1785+
1786+
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`.
1787+
To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
16241788

16251789
====
16261790
.Java
16271791
[source,java,role="primary"]
16281792
----
1793+
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
1794+
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
16291795
http
16301796
// ...
1631-
.logout(logout -> logout
1632-
.logoutSuccessHandler(myCustomSuccessHandler())
1633-
.logoutRequestMatcher(myRequestMatcher())
1634-
)
1797+
.addFilterBefore(filter, CsrfFilter.class);
16351798
----
16361799
1637-
.Kotlin
1638-
[source,kotlin,role="secondary"]
1800+
=== Customizing `<saml2:LogoutRequest>` Resolution
1801+
1802+
It's common to need to set other values in the `<saml2:LogoutRequest>` than the defaults that Spring Security provides.
1803+
1804+
By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
1805+
1806+
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation`
1807+
* The `ID` attribute - a GUID
1808+
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
1809+
* The `<NameID>` element - from `Authentication#getName`
1810+
1811+
To add other values, you can use delegation, like so:
1812+
1813+
[source,java]
16391814
----
1640-
http {
1641-
logout {
1642-
// ...
1643-
logoutSuccessHandler = myCustomSuccessHandler()
1644-
logoutRequestMatcher = myRequestMatcher()
1815+
OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver);
1816+
return (request, response, authentication) -> {
1817+
OpenSamlLogoutRequestBuilder builder = delegate.resolveLogoutRequest(request, response, authentication); <1>
1818+
builder.name(((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute")); <2>
1819+
builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now()));
1820+
return builder.logoutRequest(); <3>
1821+
};
1822+
----
1823+
<1> - Spring Security applies default values to a `<saml2:LogoutRequest>`
1824+
<2> - Your application specifies customizations
1825+
<3> - You complete the invocation by calling `request()`
1826+
1827+
[NOTE]
1828+
Support for OpenSAML 4 is coming.
1829+
In anticipation of that, `OpenSamlLogoutRequestResolver` does not add an `IssueInstant`.
1830+
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.
1831+
1832+
=== Customizing `<saml2:LogoutResponse>` Resolution
1833+
1834+
It's common to need to set other values in the `<saml2:LogoutResponse>` than the defaults that Spring Security provides.
1835+
1836+
By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
1837+
1838+
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation`
1839+
* The `ID` attribute - a GUID
1840+
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
1841+
* The `<Status>` element - `SUCCESS`
1842+
1843+
To add other values, you can use delegation, like so:
1844+
1845+
[source,java]
1846+
----
1847+
OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver);
1848+
return (request, response, authentication) -> {
1849+
OpenSamlLogoutResponseBuilder builder = delegate.resolveLogoutResponse(request, response, authentication); <1>
1850+
if (checkOtherPrevailingConditions()) {
1851+
builder.status(StatusCode.PARTIAL_LOGOUT); <2>
16451852
}
1853+
builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now()));
1854+
return builder.logoutResponse(); <3>
1855+
};
1856+
----
1857+
<1> - Spring Security applies default values to a `<saml2:LogoutResponse>`
1858+
<2> - Your application specifies customizations
1859+
<3> - You complete the invocation by calling `response()`
1860+
1861+
[NOTE]
1862+
Support for OpenSAML 4 is coming.
1863+
In anticipation of that, `OpenSamlLogoutResponseResolver` does not add an `IssueInstant`.
1864+
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.
1865+
1866+
=== Customizing `<saml2:LogoutRequest>` Validation
1867+
1868+
To customize validation, you can implement your own `LogoutHandler`.
1869+
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
1870+
1871+
[source,java]
1872+
----
1873+
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
1874+
OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver);
1875+
return (request, response, authentication) -> {
1876+
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name
1877+
LogoutRequest logoutRequest = // ... parse using OpenSAML
1878+
// perform custom validation
1879+
}
16461880
}
16471881
----
1648-
====
16491882
1650-
The success handler will send logout requests to the asserting party.
1883+
=== Customizing `<saml2:LogoutResponse>` Validation
16511884
1652-
The request matcher will detect logout requests from the asserting party.
1885+
To customize validation, you can implement your own `LogoutHandler`.
1886+
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
1887+
1888+
[source,java]
1889+
----
1890+
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
1891+
OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver);
1892+
return (request, response, authentication) -> {
1893+
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status
1894+
LogoutResponse logoutResponse = // ... parse using OpenSAML
1895+
// perform custom validation
1896+
}
1897+
}
1898+
----

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ public interface Saml2ErrorCodes {
3737
*/
3838
String MALFORMED_RESPONSE_DATA = "malformed_response_data";
3939

40+
/**
41+
* Request is invalid in a general way.
42+
*
43+
* @since 5.6
44+
*/
45+
String INVALID_REQUEST = "invalid_request";
46+
4047
/**
4148
* Response is invalid in a general way.
4249
*

0 commit comments

Comments
 (0)