= SAML Migrations

The following steps relate to changes around how to configure SAML 2.0.

== Use OpenSAML 4

OpenSAML 3 has reached its end-of-life.
As such, Spring Security 6 drops support for it, bumping up its OpenSAML baseline to 4.

To prepare for the upgrade, update your pom to depend on OpenSAML 4 instead of 3:

====
.Maven
[source,maven,role="primary"]
----
<dependencyManagement>
    <dependency>
        <groupId>org.opensaml</groupId>
        <artifactId>opensaml-core</artifactId>
        <version>4.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.opensaml</groupId>
        <artifactId>opensaml-saml-api</artifactId>
        <version>4.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.opensaml</groupId>
        <artifactId>opensaml-saml-impl</artifactId>
        <version>4.2.1</version>
    </dependency>
</dependencyManagement>
----

.Gradle
[source,gradle,role="secondary"]
----
dependencies {
    constraints {
        api "org.opensaml:opensaml-core:4.2.1"
        api "org.opensaml:opensaml-saml-api:4.2.1"
        api "org.opensaml:opensaml-saml-impl:4.2.1"
    }
}
----
====

You must use at least OpenSAML 4.1.1 to update to Spring Security 6's SAML support.

== Use `OpenSaml4AuthenticationProvider`

In order to support both OpenSAML 3 and 4 at the same time, Spring Security released `OpenSamlAuthenticationProvider` and `OpenSaml4AuthenticationProvider`.
In 6.0, because OpenSAML3 support is removed, `OpenSamlAuthenticationProvider` is removed as well.

Not all methods in `OpenSamlAuthenticationProvider` were ported 1-to-1 to `OpenSaml4AuthenticationProvider`.
As such, some adjustment will be required to make the challenge.

Consider the following representative usage of `OpenSamlAuthenticationProvider`:

====
.Java
[source,java,role="primary"]
----
OpenSamlAuthenticationProvider versionThree = new OpenSamlAuthenticationProvider();
versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor);
versionThree.setResponseTimeValidationSkew(myDuration);
----

.Kotlin
[source,kotlin,role="secondary"]
----
val versionThree: OpenSamlAuthenticationProvider = OpenSamlAuthenticationProvider()
versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor)
versionThree.setResponseTimeValidationSkew(myDuration)
----
====

This should change to:

====
.Java
[source,java,role="primary"]
----
Converter<ResponseToken, Saml2Authentication> delegate = OpenSaml4AuthenticationProvider
        .createDefaultResponseAuthenticationConverter();
OpenSaml4AuthenticationProvider versionFour = new OpenSaml4AuthenticationProvider();
versionFour.setResponseAuthenticationConverter((responseToken) -> {
	Saml2Authentication authentication = delegate.convert(responseToken);
	Assertion assertion = responseToken.getResponse().getAssertions().get(0);
	AuthenticatedPrincipal principal = (AuthenticatedPrincipal) authentication.getPrincipal();
	Collection<GrantedAuthority> authorities = myAuthoritiesExtractor.convert(assertion);
	return new Saml2Authentication(principal, authentication.getSaml2Response(), authorities);
});
Converter<AssertionToken, Saml2ResponseValidationResult> validator = OpenSaml4AuthenticationProvider
        .createDefaultAssertionValidatorWithParameters((p) -> p.put(CLOCK_SKEW, myDuration));
versionFour.setAssertionValidator(validator);
----

.Kotlin
[source,kotlin,role="secondary"]
----
val delegate = OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter()
val versionFour = OpenSaml4AuthenticationProvider()
versionFour.setResponseAuthenticationConverter({
    responseToken -> {
        val authentication = delegate.convert(responseToken)
        val assertion = responseToken.getResponse().getAssertions().get(0)
        val principal = (AuthenticatedPrincipal) authentication.getPrincipal()
        val authorities = myAuthoritiesExtractor.convert(assertion)
        return Saml2Authentication(principal, authentication.getSaml2Response(), authorities)
    }
})
val validator = OpenSaml4AuthenticationProvider
        .createDefaultAssertionValidatorWithParameters({ p -> p.put(CLOCK_SKEW, myDuration) })
versionFour.setAssertionValidator(validator)
----
====

== Stop Using SAML 2.0 `Converter` constructors

In an early release of Spring Security's SAML 2.0 support, `Saml2MetadataFilter` and `Saml2AuthenticationTokenConverter` shipped with constructors of type `Converter`.
This level of abstraction made it tricky to evolve the class and so a dedicated interface `RelyingPartyRegistrationResolver` was introduced in a later release.

In 6.0, the `Converter` constructors are removed.
To prepare for this in 5.8, change classes that implement `Converter<HttpServletRequest, RelyingPartyRegistration>` to instead implement `RelyingPartyRegistrationResolver`.

== Change to Using `Saml2AuthenticationRequestResolver`

`Saml2AuthenticationContextResolver` and `Saml2AuthenticationRequestFactory` are removed in 6.0 as is the `Saml2WebSsoAuthenticationRequestFilter` that requires them.
They are replaced by `Saml2AuthenticationRequestResolver` and a new constructor in `Saml2WebSsoAuthenticationRequestFilter`.
The new interface removes an unnecessary transport object between the two classes.

Most applications need do nothing; however, if you use or configure `Saml2AuthenticationRequestContextResolver` or `Saml2AuthenticationRequestFactory`, try the following steps to convert instead use `Saml2AuthenticationRequestResolver`.

=== Use `setAuthnRequestCustomizer` instead of `setAuthenticationRequestContextConverter`

If you are calling `OpenSaml4AuthenticationReqeustFactory#setAuthenticationRequestContextConverter`, for example, like so:

====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory() {
    OpenSaml4AuthenticationRequestFactory factory = new OpenSaml4AuthenticationRequestFactory();
	factory.setAuthenticationRequestContextConverter((context) -> {
        AuthnRequestBuilder authnRequestBuilder =  ConfigurationService.get(XMLObjectProviderRegistry.class)
            .getBuilderFactory().getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
		IssuerBuilder issuerBuilder =  ConfigurationService.get(XMLObjectProviderRegistry.class)
            .getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
        tring issuer = context.getIssuer();
		String destination = context.getDestination();
		String assertionConsumerServiceUrl = context.getAssertionConsumerServiceUrl();
		String protocolBinding = context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn();
		AuthnRequest auth = authnRequestBuilder.buildObject();
		auth.setID("ARQ" + UUID.randomUUID().toString().substring(1));
		auth.setIssueInstant(Instant.now());
		auth.setForceAuthn(Boolean.TRUE);
		auth.setIsPassive(Boolean.FALSE);
		auth.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
		Issuer iss = issuerBuilder.buildObject();
		iss.setValue(issuer);
		auth.setIssuer(iss);
		auth.setDestination(destination);
		auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl);
	});
	return factory;
}
----
====

to ensure that ForceAuthn is set to `true`, you can instead do:

====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistrationResolver registrations) {
    OpenSaml4AuthenticationRequestResolver reaolver = new OpenSaml4AuthenticationRequestResolver(registrations);
	resolver.setAuthnRequestCustomizer((context) -> context.getAuthnRequest().setForceAuthn(Boolean.TRUE));
	return resolver;
}
----
====

Also, since `setAuthnRequestCustomizer` has direct access to the `HttpServletRequest`, there is no need for a `Saml2AuthenticationRequestContextResolver`.
Simply use `setAuthnRequestCustomizer` to read directly from `HttpServletRequest` this information you need.

=== Use `setAuthnRequestCustomizer` instead of `setProtocolBinding`

Instead of doing:

====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory() {
    OpenSaml4AuthenticationRequestFactory factory = new OpenSaml4AuthenticationRequestFactory();
	factory.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
	return factory;
}
----
====

you can do:

====
.Java
[source,java,role="primary"]
----
@Bean
Saml2AuthenticationRequestResolver authenticationRequestResolver() {
	OpenSaml4AuthenticationRequestResolver reaolver = new OpenSaml4AuthenticationRequestResolver(registrations);
	resolver.setAuthnRequestCustomizer((context) -> context.getAuthnRequest()
            .setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"));
	return resolver;
}
----
====

[NOTE]
====
Since Spring Security only supports the `POST` binding for authentication, there is not very much value in overriding the protocol binding at this point in time.
====

== Use the latest `Saml2AuthenticationToken` constructor

In an early release, `Saml2AuthenticationToken` took several individual settings as constructor parameters.
This created a challenge each time a new parameter needed to be added.
Since most of these settings were part of `RelyingPartyRegistration`, a new constructor was added where a `RelyingPartyRegistration` could be provided, making the constructor more stable.
It also is valuable in that it more closely aligns with the design of `OAuth2LoginAuthenticationToken`.

Most applications do not construct this class directly since `Saml2WebSsoAuthenticationFilter` does.
However, in the event that your application constructs one, please change from:

====
.Java
[source,java,role="primary"]
----
new Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(),
    registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials())
----

.Kotlin
[source,kotlin,role="secondary"]
----
Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(),
    registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials())
----
====

to:

====
.Java
[source,java,role="primary"]
----
new Saml2AuthenticationToken(saml2Response, registration)
----

.Kotlin
[source,kotlin,role="secondary"]
----
Saml2AuthenticationToken(saml2Response, registration)
----
====

== Use `RelyingPartyRegistration` updated methods

In an early release of Spring Security's SAML support, there was some ambiguity on the meaning of certain `RelyingPartyRegistration` methods and their function.
As more capabilities were added to `RelyingPartyRegistration`, it became necessary to clarify this ambiguity by changing method names to ones that aligned with spec language.

The deprecated methods in `RelyingPartyRegstration` are removed.
To prepare for that, consider the following representative usage of `RelyingPartyRegistration`:

====
.Java
[source,java,role="primary"]
----
String idpEntityId = registration.getRemoteIdpEntityId();
String assertionConsumerServiceUrl = registration.getAssertionConsumerServiceUrlTemplate();
String idpWebSsoUrl = registration.getIdpWebSsoUrl();
String localEntityId = registration.getLocalEntityIdTemplate();
List<Saml2X509Credential> verifying = registration.getCredentials().stream()
        .filter(Saml2X509Credential::isSignatureVerficationCredential)
        .collect(Collectors.toList());
----

.Kotlin
[source,kotlin,role="secondary"]
----
val idpEntityId: String = registration.getRemoteIdpEntityId()
val assertionConsumerServiceUrl: String = registration.getAssertionConsumerServiceUrlTemplate()
val idpWebSsoUrl: String = registration.getIdpWebSsoUrl()
val localEntityId: String = registration.getLocalEntityIdTemplate()
val verifying: List<Saml2X509Credential> = registration.getCredentials()
        .filter(Saml2X509Credential::isSignatureVerficationCredential)
----
====

This should change to:

====
.Java
[source,java,role="primary"]
----
String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId();
String assertionConsumerServiceLocation = registration.getAssertionConsumerServiceLocation();
String singleSignOnServiceLocation = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation();
String entityId = registration.getEntityId();
List<Saml2X509Credential> verifying = registration.getAssertingPartyDetails().getVerificationX509Credentials();
----

.Kotlin
[source,kotlin,role="secondary"]
----
val assertingPartyEntityId: String = registration.getAssertingPartyDetails().getEntityId()
val assertionConsumerServiceLocation: String = registration.getAssertionConsumerServiceLocation()
val singleSignOnServiceLocation: String = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()
val entityId: String = registration.getEntityId()
val verifying: List<Saml2X509Credential> = registration.getAssertingPartyDetails().getVerificationX509Credentials()
----
====

For a complete listing of all changed methods, please see {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[``RelyingPartyRegistration``'s JavaDoc].
