2025-05-06 16:38:32 -06:00

405 lines
13 KiB
Plaintext

= Saml 2.0 Migrations
== Use OpenSAML 5 By Default
OpenSAML 4.x is no longer supported by the OpenSAML team.
As such, Spring Security will default to using its `OpenSaml5` components in all cases.
If you want to see how well your application will respond to this, do the following:
1. Update your OpenSAML dependencies to 5.x
2. If you are constructing an `OpenSaml4XXX` Spring Security component, change it to `OpenSaml5`.
If you cannot opt-in, then add the `opensaml-saml-api` and `opensaml-saml-impl` 4.x dependencies and exclude the 5.x dependencies from `spring-security-saml2-service-provider`.
== Continue Filter Chain When No Relying Party Found
In Spring Security 6, `Saml2WebSsoAuthenticationFilter` throws an exception when the request URI matches, but no relying party registration is found.
There are a number of cases when an application would not consider this an error situation.
For example, this filter doesn't know how the `AuthorizationFilter` will respond to a missing relying party.
In some cases it may be allowable.
In other cases, you may want your `AuthenticationEntryPoint` to be invoked, which would happen if this filter were to allow the request to continue to the `AuthorizationFilter`.
To improve this filter's flexibility, in Spring Security 7 it will continue the filter chain when there is no relying party registration found instead of throwing an exception.
For many applications, the only notable change will be that your `authenticationEntryPoint` will be invoked if the relying party registration cannot be found.
When you have only one asserting party, this means by default a new authentication request will be built and sent back to the asserting party, which may cause a "Too Many Redirects" loop.
To see if you are affected in this way, you can prepare for this change in 6 by setting the following property in `Saml2WebSsoAuthenticationFilter`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
http
.saml2Login((saml2) -> saml2
.withObjectPostProcessor(new ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter>() {
@Override
public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) {
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
return filter;
}
})
)
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
http {
saml2Login { }
withObjectPostProcessor(
object : ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter?>() {
override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter {
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true)
return filter
}
})
}
----
Xml::
+
[source,xml,role="secondary"]
----
<b:bean id="saml2PostProcessor" class="org.example.MySaml2WebSsoAuthenticationFilterBeanPostProcessor"/>
----
======
== Validate Response After Validating Assertions
In Spring Security 6, the order of authenticating a `<saml2:Response>` is as follows:
1. Verify the Response Signature, if any
2. Decrypt the Response
3. Validate Response attributes, like Destination and Issuer
4. For each assertion, verify the signature, decrypt, and then validate its fields
5. Check to ensure that the response has at least one assertion with a name field
This ordering sometimes poses challenges since some response validation is being done in Step 3 and some in Step 5.
Specifically, this poses a chellenge when an application doesn't have a name field and doesn't need it to be validated.
In Spring Security 7, this is simplified by moving response validation to after assertion validation and combining the two separate validation steps 3 and 5.
When this is complete, response validation will no longer check for the existence of the `NameID` attribute and rely on ``ResponseAuthenticationConverter``s to do this.
This will add support ``ResponseAuthenticationConverter``s that don't use the `NameID` element in their `Authentication` instance and so don't need it validated.
To opt-in to this behavior in advance, use `OpenSaml5AuthenticationProvider#setValidateResponseAfterAssertions` to `true` like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setValidateResponseAfterAssertions(true);
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
val provider = OpenSaml5AuthenticationProvider()
provider.setValidateResponseAfterAssertions(true)
----
======
This will change the authentication steps as follows:
1. Verify the Response Signature, if any
2. Decrypt the Response
3. For each assertion, verify the signature, decrypt, and then validate its fields
4. Validate Response attributes, like Destination and Issuer
Note that if you have a custom response authentication converter, then you are now responsible to check if the `NameID` element exists in the event that you need it.
Alternatively to updating your response authentication converter, you can specify a custom `ResponseValidator` that adds back in the check for the `NameID` element as follows:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setValidateResponseAfterAssertions(true);
ResponseValidator responseValidator = ResponseValidator.withDefaults((responseToken) -> {
Response response = responseToken.getResponse();
Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
"Assertion [" + firstAssertion.getID() + "] is missing a subject");
Saml2ResponseValidationResult failed = Saml2ResponseValidationResult.failure(error);
if (assertion.getSubject() == null) {
return failed;
}
if (assertion.getSubject().getNameID() == null) {
return failed;
}
if (assertion.getSubject().getNameID().getValue() == null) {
return failed;
}
return Saml2ResponseValidationResult.success();
});
provider.setResponseValidator(responseValidator);
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
val provider = OpenSaml5AuthenticationProvider()
provider.setValidateResponseAfterAssertions(true)
val responseValidator = ResponseValidator.withDefaults { responseToken: ResponseToken ->
val response = responseToken.getResponse()
val assertion = CollectionUtils.firstElement(response.getAssertions())
val error = Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
"Assertion [" + firstAssertion.getID() + "] is missing a subject")
val failed = Saml2ResponseValidationResult.failure(error)
if (assertion.getSubject() == null) {
return@withDefaults failed
}
if (assertion.getSubject().getNameID() == null) {
return@withDefaults failed
}
if (assertion.getSubject().getNameID().getValue() == null) {
return@withDefaults failed
}
return@withDefaults Saml2ResponseValidationResult.success()
}
provider.setResponseValidator(responseValidator)
----
======
== `RelyingPartyRegistration` Improvements
`RelyingPartyRegistration` links metadata from a relying party to metadata from an asserting party.
To prepare for some improvements to the API, please take the following steps:
1. If you are mutating a registration by using `RelyingPartyRegistration#withRelyingPartyRegistration`, instead call `RelyingPartyRegistration#mutate`
2. If you are providing or retrieving `AssertingPartyDetails`, use `getAssertingPartyMetadata` or `withAssertingPartyMetadata` instead.
== `OpenSaml5AuthenticationProvider` Improvements
Spring Security 7 will remove a handful of static factories from `OpenSaml5AuthenticationProvider` in favor of inner classes.
These inner classes simplify customization of the response validator, the assertion validator, and the response authentication converter.
=== Response Validation
Instead of doing:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
saml2.setResponseValidator((responseToken) -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
.andThen((result) -> result
.concat(myCustomValidator.convert(responseToken))
));
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
saml2.setResponseValidator { responseToken -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
.andThen { result -> result
.concat(myCustomValidator.convert(responseToken))
}
}
return saml2
}
----
======
use `OpenSaml5AuthenticationProvider.ResponseValidator`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator));
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator))
return saml2
}
----
======
=== Assertion Validation
Instead of doing:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
.createDefaultAssertionValidatorWithParameters(assertionToken -> {
Map<String, Object> params = new HashMap<>();
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
// ... other validation parameters
return new ValidationContext(params);
})
);
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
.createDefaultAssertionValidatorWithParameters { ->
val params = HashMap<String, Object>()
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis())
// ... other validation parameters
return ValidationContext(params)
}
)
return saml2
}
----
======
use `OpenSaml5AuthenticationProvider.AssertionValidator`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
Duration tenMinutes = Duration.ofMinutes(10);
authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build());
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
val tenMinutes = Duration.ofMinutes(10)
authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build())
return saml2
}
----
======
== Response Authentication Converter
Instead of doing:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
Converter<ResponseToken, Saml2Authentication> authenticationConverter() {
return (responseToken) -> {
Saml2Authentication authentication = OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter()
.convert(responseToken);
// ... work with OpenSAML's Assertion object to extract the principal
return new Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities());
};
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun authenticationConverter(): Converter<ResponseToken, Saml2Authentication> {
return { responseToken ->
val authentication =
OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter().convert(responseToken)
// ... work with OpenSAML's Assertion object to extract the principal
return Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities())
}
}
----
======
use `OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ResponseAuthenticationConverter authenticationConverter() {
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
authenticationConverter.setPrincipalNameConverter((assertion) -> {
// ... work with OpenSAML's Assertion object to extract the principal
});
return authenticationConverter;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
val authenticationConverter = ResponseAuthenticationConverter()
authenticationConverter.setPrincipalNameConverter { assertion ->
// ... work with OpenSAML's Assertion object to extract the principal
}
return authenticationConverter
}
----
======