Skip to content

Commit e807fae

Browse files
committed
Add Single Logout Support
Closes gh-8731
1 parent 2f734a0 commit e807fae

File tree

41 files changed

+4957
-35
lines changed

Some content is hidden

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

41 files changed

+4957
-35
lines changed

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

Lines changed: 267 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,20 +1053,279 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
10531053
[[servlet-saml2login-logout]]
10541054
=== Performing Single Logout
10551055

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

1058-
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
1058+
Briefly, there are two use cases Spring Security supports:
1059+
1060+
* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party.
1061+
Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond
1062+
* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party.
1063+
Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party.
1064+
1065+
[NOTE]
1066+
In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot.
1067+
Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser.
1068+
1069+
=== Minimal Configuration for Single Logout
1070+
1071+
To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things:
1072+
1073+
* First, the asserting party must support SAML 2.0 Single Logout
1074+
* 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
1075+
* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
1076+
1077+
==== RP-Initiated Single Logout
1078+
1079+
Given those, then for RP-initiated Single Logout, you can begin from the initial minimal example and add the following configuration:
1080+
1081+
[source,java]
1082+
----
1083+
@Value("${private.key}") RSAPrivateKey key;
1084+
@Value("${public.certificate}") X509Certificate certificate;
1085+
1086+
@Bean
1087+
RelyingPartyRegistrationRepository registrations() {
1088+
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
1089+
.fromMetadataLocation("https://ap.example.org/metadata")
1090+
.registrationId("id")
1091+
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
1092+
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
1093+
.build();
1094+
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
1095+
}
1096+
1097+
@Bean
1098+
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
1099+
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
1100+
LogoutHandler logoutResponseHandler = logoutResponseHandler(registrationResolver);
1101+
LogoutSuccessHandler logoutRequestSuccessHandler = logoutRequestSuccessHandler(registrationResolver);
1102+
1103+
http
1104+
.authorizeRequests((authorize) -> authorize
1105+
.anyRequest().authenticated()
1106+
)
1107+
.saml2Login(withDefaults())
1108+
.logout((logout) -> logout
1109+
.logoutUrl("/saml2/logout")
1110+
.logoutSuccessHandler(successHandler))
1111+
.addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class);
1112+
1113+
return http.build();
1114+
}
1115+
1116+
private LogoutSuccessHandler logoutRequestSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
1117+
OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(registrationResolver);
1118+
return new Saml2LogoutRequestSuccessHandler(logoutRequestResolver);
1119+
}
1120+
1121+
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
1122+
return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
1123+
}
1124+
----
1125+
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
1126+
<2> - Second, supply a `LogoutSuccessHandler` for initiating Single Logout, sending a `saml2:LogoutRequest` to the asserting party
1127+
<3> - Third, supply the `LogoutHandler` s needed to handle the `saml2:LogoutResponse` s sent from the asserting party.
1128+
1129+
==== Runtime Expectations for RP-Initiated
1130+
1131+
Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
1132+
Your application will then do the following:
1133+
1134+
1. Logout the user and invalidate the session
1135+
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.
1136+
3. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
1137+
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
1138+
5. Redirect to any configured successful logout endpoint
1139+
1140+
[TIP]
1141+
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`.
1142+
1143+
==== AP-Initiated Single Logout
1144+
1145+
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:
1146+
1147+
[source,java]
1148+
----
1149+
@Value("${private.key}") RSAPrivateKey key;
1150+
@Value("${public.certificate}") X509Certificate certificate;
1151+
1152+
@Bean
1153+
RelyingPartyRegistrationRepository registrations() {
1154+
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
1155+
.fromMetadataLocation("https://ap.example.org/metadata")
1156+
.registrationId("id")
1157+
.signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
1158+
.build();
1159+
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
1160+
}
1161+
1162+
@Bean
1163+
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
1164+
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
1165+
LogoutHandler logoutRequestHandler = logoutRequestHandler(registrationResolver);
1166+
LogoutSuccessHandler logoutResponseSuccessHandler = logoutResponseSuccessHandler(registrationResolver);
1167+
1168+
http
1169+
.authorizeRequests((authorize) -> authorize
1170+
.anyRequest().authenticated()
1171+
)
1172+
.saml2Login(withDefaults())
1173+
.addFilterBefore(new Saml2LogoutRequestFilter(logoutResponseSuccessHandler, logoutRequestHandler), CsrfFilter.class);
1174+
1175+
return http.build();
1176+
}
1177+
1178+
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
1179+
return new CompositeLogoutHandler(
1180+
new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver),
1181+
new SecurityContextLogoutHandler(),
1182+
new LogoutSuccessEventPublishingLogoutHandler());
1183+
}
1184+
1185+
private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
1186+
OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(registrationResolver);
1187+
return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver);
1188+
}
1189+
----
1190+
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
1191+
<2> - Second, supply the `LogoutHandler` needed to handle the `saml2:LogoutRequest` s sent from the asserting party.
1192+
<3> - Third, supply a `LogoutSuccessHandler` for completing Single Logout, sending a `saml2:LogoutResponse` to the asserting party
1193+
1194+
==== Runtime Expectations for AP-Initiated
1195+
1196+
Given the above configuration, an asserting party can send a `POST /logout/saml2` to your application that includes a `<saml2:LogoutRequest>`
1197+
Also, your application can participate in an AP-initated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
1198+
1199+
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
1200+
2. Logout the user and invalidate the session
1201+
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
1202+
4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
1203+
1204+
[TIP]
1205+
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`
1206+
1207+
[NOTE]
1208+
In the event that you need to support both logout flows, you can combine the above to configurations.
1209+
1210+
=== Configuring Logout Endpoints
1211+
1212+
There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
1213+
* `/logout` - the endpoint for initiating single logout with an asserting party
1214+
* `/logout/saml2/slo` - the endpoint for receiving logout requests or responses from an asserting party
1215+
1216+
Because the user is already logged in, the `registrationId` is already known.
1217+
For this reason, `+{registrationId}+` is not part of these URLs by default.
1218+
1219+
These URLs are customizable in the DSL.
1220+
1221+
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`.
1222+
To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
10591223

10601224
[source,java]
10611225
----
1226+
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
1227+
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
10621228
http
10631229
// ...
1064-
.logout(logout -> logout
1065-
.logoutSuccessHandler(myCustomSuccessHandler())
1066-
.logoutRequestMatcher(myRequestMatcher())
1067-
)
1230+
.addFilterBefore(filter, CsrfFilter.class);
1231+
----
1232+
1233+
=== Customizing `<saml2:LogoutRequest>` Resolution
1234+
1235+
It's common to need to set other values in the `<saml2:LogoutRequest>` than the defaults that Spring Security provides.
1236+
1237+
By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
1238+
1239+
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation`
1240+
* The `ID` attribute - a GUID
1241+
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
1242+
* The `<NameID>` element - from `Authentication#getName`
1243+
1244+
To add other values, you can use delegation, like so:
1245+
1246+
[source,java]
1247+
----
1248+
OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver);
1249+
return (request, response, authentication) -> {
1250+
OpenSamlLogoutRequestBuilder builder = delegate.resolveLogoutRequest(request, response, authentication); <1>
1251+
builder.name(((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute")); <2>
1252+
builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now()));
1253+
return builder.logoutRequest(); <3>
1254+
};
1255+
----
1256+
<1> - Spring Security applies default values to a `<saml2:LogoutRequest>`
1257+
<2> - Your application specifies customizations
1258+
<3> - You complete the invocation by calling `request()`
1259+
1260+
[NOTE]
1261+
Support for OpenSAML 4 is coming.
1262+
In anticipation of that, `OpenSamlLogoutRequestResolver` does not add an `IssueInstant`.
1263+
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.
1264+
1265+
=== Customizing `<saml2:LogoutResponse>` Resolution
1266+
1267+
It's common to need to set other values in the `<saml2:LogoutResponse>` than the defaults that Spring Security provides.
1268+
1269+
By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
1270+
1271+
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation`
1272+
* The `ID` attribute - a GUID
1273+
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
1274+
* The `<Status>` element - `SUCCESS`
1275+
1276+
To add other values, you can use delegation, like so:
1277+
1278+
[source,java]
1279+
----
1280+
OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver);
1281+
return (request, response, authentication) -> {
1282+
OpenSamlLogoutResponseBuilder builder = delegate.resolveLogoutResponse(request, response, authentication); <1>
1283+
if (checkOtherPrevailingConditions()) {
1284+
builder.status(StatusCode.PARTIAL_LOGOUT); <2>
1285+
}
1286+
builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now()));
1287+
return builder.logoutResponse(); <3>
1288+
};
1289+
----
1290+
<1> - Spring Security applies default values to a `<saml2:LogoutResponse>`
1291+
<2> - Your application specifies customizations
1292+
<3> - You complete the invocation by calling `response()`
1293+
1294+
[NOTE]
1295+
Support for OpenSAML 4 is coming.
1296+
In anticipation of that, `OpenSamlLogoutResponseResolver` does not add an `IssueInstant`.
1297+
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.
1298+
1299+
=== Customizing `<saml2:LogoutRequest>` Validation
1300+
1301+
To customize validation, you can implement your own `LogoutHandler`.
1302+
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
1303+
1304+
[source,java]
1305+
----
1306+
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
1307+
OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver);
1308+
return (request, response, authentication) -> {
1309+
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name
1310+
LogoutRequest logoutRequest = // ... parse using OpenSAML
1311+
// perform custom validation
1312+
}
1313+
}
10681314
----
10691315

1070-
The success handler will send logout requests to the asserting party.
1316+
=== Customizing `<saml2:LogoutResponse>` Validation
10711317

1072-
The request matcher will detect logout requests from the asserting party.
1318+
To customize validation, you can implement your own `LogoutHandler`.
1319+
At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
1320+
1321+
[source,java]
1322+
----
1323+
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
1324+
OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver);
1325+
return (request, response, authentication) -> {
1326+
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status
1327+
LogoutResponse logoutResponse = // ... parse using OpenSAML
1328+
// perform custom validation
1329+
}
1330+
}
1331+
----

saml2/saml2-service-provider/core/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.5
44+
*/
45+
String INVALID_REQUEST = "invalid_request";
46+
4047
/**
4148
* Response is invalid in a general way.
4249
*

0 commit comments

Comments
 (0)