For the latest stable version, please use Spring Security 6.4.2! |
Performing Single Logout
Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
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 asaml2: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 asaml2:LogoutResponse
to the asserting party.
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 andsaml2: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 andsaml2:LogoutResponse
s
You can begin from the initial minimal example and add the following configuration:
@Value("${private.key}") RSAPrivateKey key;
@Value("${public.certificate}") X509Certificate certificate;
@Bean
RelyingPartyRegistrationRepository registrations() {
Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate);
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("id")
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
.signingX509Credentials((signing) -> signing.add(credential)) (1)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Bean
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.saml2Login(withDefaults())
.saml2Logout(withDefaults()); (2)
return http.build();
}
1 | - First, add your signing key to the RelyingPartyRegistration instance or to multiple instances |
2 | - Second, indicate that your application wants to use SAML SLO to logout the end user |
Runtime Expectations
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:
-
Logout the user and invalidate the session
-
Use a
Saml2LogoutRequestResolver
to create, sign, and serialize a<saml2:LogoutRequest>
based on theRelyingPartyRegistration
associated with the currently logged-in user. -
Send a redirect or post to the asserting party based on the
RelyingPartyRegistration
-
Deserialize, verify, and process the
<saml2:LogoutResponse>
sent by the asserting party -
Redirect to any configured successful logout endpoint
Also, your application can participate in an AP-initiated logout when the asserting party sends a <saml2:LogoutRequest>
to /logout/saml2/slo
:
-
Use a
Saml2LogoutRequestHandler
to deserialize, verify, and process the<saml2:LogoutRequest>
sent by the asserting party -
Logout the user and invalidate the session
-
Create, sign, and serialize a
<saml2:LogoutResponse>
based on theRelyingPartyRegistration
associated with the just logged-out user -
Send a redirect or post to the asserting party based on the
RelyingPartyRegistration
Adding saml2Logout adds the capability for logout to the service provider.
Because it is an optional capability, you need to enable it for each individual RelyingPartyRegistration .
You can do this by setting the RelyingPartyRegistration.Builder#singleLogoutServiceLocation property.
|
Configuring Logout Endpoints
There are three behaviors that can be triggered by different endpoints:
-
RP-initiated logout, which allows an authenticated user to
POST
and trigger the logout process by sending the asserting party a<saml2:LogoutRequest>
-
AP-initiated logout, which allows an asserting party to send a
<saml2:LogoutRequest>
to the application -
AP logout response, which allows an asserting party to send a
<saml2:LogoutResponse>
in response to the RP-initiated<saml2:LogoutRequest>
The first is triggered by performing normal POST /logout
when the principal is of type Saml2AuthenticatedPrincipal
.
The second is triggered by POSTing to the /logout/saml2/slo
endpoint with a SAMLRequest
signed by the asserting party.
The third is triggered by POSTing to the /logout/saml2/slo
endpoint with a SAMLResponse
signed by the asserting party.
Because the user is already logged in or the original Logout Request is known, the registrationId
is already known.
For this reason, {registrationId}
is not part of these URLs by default.
This URL is 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
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
.logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
);
You should also configure these endpoints in your RelyingPartyRegistration
.
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 - fromRelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation
-
The
ID
attribute - a GUID -
The
<Issuer>
element - fromRelyingPartyRegistration#getEntityId
-
The
<NameID>
element - fromAuthentication#getName
To add other values, you can use delegation, like so:
@Bean
Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) {
OpenSaml4LogoutRequestResolver logoutRequestResolver
new OpenSaml4LogoutRequestResolver(registrationResolver);
logoutRequestResolver.setParametersConsumer((parameters) -> {
String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute");
String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
LogoutRequest logoutRequest = parameters.getLogoutRequest();
NameID nameId = logoutRequest.getNameID();
nameId.setValue(name);
nameId.setFormat(format);
});
return logoutRequestResolver;
}
Then, you can supply your custom Saml2LogoutRequestResolver
in the DSL as follows:
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestResolver(this.logoutRequestResolver)
)
);
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 - fromRelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation
-
The
ID
attribute - a GUID -
The
<Issuer>
element - fromRelyingPartyRegistration#getEntityId
-
The
<Status>
element -SUCCESS
To add other values, you can use delegation, like so:
@Bean
public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) {
OpenSaml4LogoutResponseResolver logoutRequestResolver =
new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
logoutRequestResolver.setParametersConsumer((parameters) -> {
if (checkOtherPrevailingConditions(parameters.getRequest())) {
parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT);
}
});
return logoutRequestResolver;
}
Then, you can supply your custom Saml2LogoutResponseResolver
in the DSL as follows:
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestResolver(this.logoutRequestResolver)
)
);
Customizing <saml2:LogoutRequest>
Authentication
To customize validation, you can implement your own Saml2LogoutRequestValidator
.
At this point, the validation is minimal, so you may be able to first delegate to the default Saml2LogoutRequestValidator
like so:
@Component
public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator();
@Override
public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) {
// verify signature, issuer, destination, and principal name
Saml2LogoutValidatorResult result = delegate.authenticate(authentication);
LogoutRequest logoutRequest = // ... parse using OpenSAML
// perform custom validation
}
}
Then, you can supply your custom Saml2LogoutRequestValidator
in the DSL as follows:
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator)
)
);
Customizing <saml2:LogoutResponse>
Authentication
To customize validation, you can implement your own Saml2LogoutResponseValidator
.
At this point, the validation is minimal, so you may be able to first delegate to the default Saml2LogoutResponseValidator
like so:
@Component
public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator();
@Override
public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) {
// verify signature, issuer, destination, and status
Saml2LogoutValidatorResult result = delegate.authenticate(parameters);
LogoutResponse logoutResponse = // ... parse using OpenSAML
// perform custom validation
}
}
Then, you can supply your custom Saml2LogoutResponseValidator
in the DSL as follows:
http
.saml2Logout((saml2) -> saml2
.logoutResponse((response) -> response
.logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
)
);
Customizing <saml2:LogoutRequest>
storage
When your application sends a <saml2:LogoutRequest>
, the value is stored in the session so that the RelayState
parameter and the InResponseTo
attribute in the <saml2:LogoutResponse>
can be verified.
If you want to store logout requests in some place other than the session, you can supply your custom implementation in the DSL, like so:
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestRepository(myCustomLogoutRequestRepository)
)
);