This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Security 7.0.2!

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:

  • Java

  • Kotlin

  • Xml

http
    .saml2Login((saml2) -> saml2
        .withObjectPostProcessor(new ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter>() {
			@Override
            public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) {
				filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
				return filter;
            }
        })
    )
http {
    saml2Login { }
    withObjectPostProcessor(
        object : ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter?>() {
            override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter {
            filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true)
            return filter
        }
    })
}
<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 ResponseAuthenticationConverters to do this.

This will add support ResponseAuthenticationConverters 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:

  • Java

  • Kotlin

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setValidateResponseAfterAssertions(true);
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:

  • Java

  • Kotlin

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

  • Java

  • Kotlin

@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
	OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
	saml2.setResponseValidator((responseToken) -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
            .andThen((result) -> result
                .concat(myCustomValidator.convert(responseToken))
            ));
	return saml2;
}
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
	val saml2 = OpenSaml5AuthenticationProvider()
	saml2.setResponseValidator { responseToken -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
        .andThen { result -> result
            .concat(myCustomValidator.convert(responseToken))
        }
    }
	return saml2
}

use OpenSaml5AuthenticationProvider.ResponseValidator:

  • Java

  • Kotlin

@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
	OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
	saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator));
	return saml2;
}
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
	val saml2 = OpenSaml5AuthenticationProvider()
	saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator))
	return saml2
}

Assertion Validation

Instead of doing:

  • Java

  • Kotlin

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

  • Java

  • Kotlin

@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
	OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
	Duration tenMinutes = Duration.ofMinutes(10);
    authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build());
	return saml2;
}
@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:

  • Java

  • Kotlin

@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());
	};
}
@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:

  • Java

  • Kotlin

@Bean
ResponseAuthenticationConverter authenticationConverter() {
	ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
	authenticationConverter.setPrincipalNameConverter((assertion) -> {
		// ... work with OpenSAML's Assertion object to extract the principal
	});
	return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
    val authenticationConverter = ResponseAuthenticationConverter()
    authenticationConverter.setPrincipalNameConverter { assertion ->
		// ... work with OpenSAML's Assertion object to extract the principal
    }
    return authenticationConverter
}