For the latest stable version, please use Spring Security 6.2.4!

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

  • Gradle

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

  • Kotlin

OpenSamlAuthenticationProvider versionThree = new OpenSamlAuthenticationProvider();
versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor);
versionThree.setResponseTimeValidationSkew(myDuration);
val versionThree: OpenSamlAuthenticationProvider = OpenSamlAuthenticationProvider()
versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor)
versionThree.setResponseTimeValidationSkew(myDuration)

This should change to:

  • Java

  • Kotlin

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

@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

@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

@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

@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;
}

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

  • Kotlin

new Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(),
    registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials())
Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(),
    registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials())

to:

  • Java

  • Kotlin

new Saml2AuthenticationToken(saml2Response, registration)
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

  • Kotlin

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());
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

  • Kotlin

String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId();
String assertionConsumerServiceLocation = registration.getAssertionConsumerServiceLocation();
String singleSignOnServiceLocation = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation();
String entityId = registration.getEntityId();
List<Saml2X509Credential> verifying = registration.getAssertingPartyDetails().getVerificationX509Credentials();
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 RelyingPartyRegistration's JavaDoc.