Add Saml2LogoutConfigurer
Closes gh-9497
This commit is contained in:
@@ -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)
|
||||
)
|
||||
);
|
||||
----
|
||||
|
||||
Reference in New Issue
Block a user