Add Saml2LogoutConfigurer

Closes gh-9497
This commit is contained in:
Josh Cummings
2021-03-25 10:44:26 -06:00
parent c63d618b26
commit 4f06fc6ed1
7 changed files with 1317 additions and 159 deletions

View File

@@ -1,4 +1,5 @@
[[servlet-saml2login]]
== SAML 2.0 Login
:figures: images/servlet/saml2
@@ -791,7 +792,7 @@ spring:
okta:
signing.credentials: &relying-party-credentials
- private-key-location: classpath:rp.key
- certificate-location: classpath:rp.crt
certificate-location: classpath:rp.crt
identityprovider:
entity-id: ...
azure:
@@ -1639,9 +1640,7 @@ To use Spring Security's SAML 2.0 Single Logout feature, you will need the follo
* 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:
You can begin from the initial minimal example and add the following configuration:
[source,java]
----
@@ -1650,48 +1649,31 @@ Given those, then for RP-initiated Single Logout, you can begin from the initial
@Bean
RelyingPartyRegistrationRepository registrations() {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
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(Saml2X509Credential.signing(key, certificate))) <1>
.signingX509Credentials((signing) -> signing.add(credential)) <1>
.build();
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@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);
.saml2Logout(withDefaults()); <2>
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.
<2> - Second, indicate that your application wants to use SAML SLO to logout the end user
==== Runtime Expectations for RP-Initiated
==== 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:
@@ -1702,86 +1684,30 @@ Your application will then do the following:
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`:
Also, your application can participate in an AP-initiated 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
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>`
Because the user is already logged in, the `registrationId` is already known.
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.
These URLs are customizable in the DSL.
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:
@@ -1790,12 +1716,15 @@ To reduce changes in configuration for the asserting party, you can configure th
.Java
[source,java,role="primary"]
----
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
http
// ...
.addFilterBefore(filter, CsrfFilter.class);
.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
@@ -1812,22 +1741,33 @@ To add other values, you can use delegation, like so:
[source,java]
----
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>
};
@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;
}
----
<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.
Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows:
[source,java]
----
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestResolver(this.logoutRequestResolver)
)
);
----
=== Customizing `<saml2:LogoutResponse>` Resolution
@@ -1844,55 +1784,111 @@ 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>
};
@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;
}
----
<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:
Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows:
[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
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:
[source,java]
----
@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
}
}
}
----
=== Customizing `<saml2:LogoutResponse>` 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:
Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows:
[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
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:
[source,java]
----
@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:
[source,java]
----
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:
[source,java]
----
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestRepository(myCustomLogoutRequestRepository)
)
);
----