= 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)
----
======
