166 lines
6.3 KiB
Plaintext
166 lines
6.3 KiB
Plaintext
= Saml 2.0 Migrations
|
|
|
|
== 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)
|
|
----
|
|
======
|