This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Security 6.4.5!spring-doc.cn

Authenticating <saml2:Response>s

To verify SAML 2.0 Responses, Spring Security uses Saml2AuthenticationTokenConverter to populate the Authentication request and OpenSaml4AuthenticationProvider to authenticate it.spring-doc.cn

You can configure this in a number of ways including:spring-doc.cn

  1. Changing the way the RelyingPartyRegistration is Looked Upspring-doc.cn

  2. Setting a clock skew to timestamp validationspring-doc.cn

  3. Mapping the response to a list of GrantedAuthority instancesspring-doc.cn

  4. Customizing the strategy for validating assertionsspring-doc.cn

  5. Customizing the strategy for decrypting response and assertion elementsspring-doc.cn

To configure these, you’ll use the saml2Login#authenticationManager method in the DSL.spring-doc.cn

Changing the SAML Response Processing Endpoint

The default endpoint is /login/saml2/sso/{registrationId}. You can change this in the DSL and in the associated metadata like so:spring-doc.cn

@Bean
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
	http
        // ...
        .saml2Login((saml2) -> saml2.loginProcessingUrl("/saml2/login/sso"))
        // ...

    return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
	http {
        // ...
        .saml2Login {
            loginProcessingUrl = "/saml2/login/sso"
        }
        // ...
    }

    return http.build()
}
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")

Changing RelyingPartyRegistration lookup

By default, this converter will match against any associated <saml2:AuthnRequest> or any registrationId it finds in the URL. Or, if it cannot find one in either of those cases, then it attempts to look it up by the <saml2:Response#Issuer> element.spring-doc.cn

There are a number of circumstances where you might need something more sophisticated, like if you are supporting ARTIFACT binding. In those cases, you can customize lookup through a custom AuthenticationConverter, which you can customize like so:spring-doc.cn

@Bean
SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
	http
        // ...
        .saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
        // ...

    return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain {
	http {
        // ...
        .saml2Login {
            authenticationConverter = converter
        }
        // ...
    }

    return http.build()
}

Setting a Clock Skew

It’s not uncommon for the asserting and relying parties to have system clocks that aren’t perfectly synchronized. For that reason, you can configure OpenSaml4AuthenticationProvider's default assertion validator with some tolerance:spring-doc.cn

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidatorWithParameters(assertionToken -> {
                    Map<String, Object> params = new HashMap<>();
                    params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
                    // ... other validation parameters
                    return new ValidationContext(params);
                })
        );

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setAssertionValidator(
            OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidatorWithParameters(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
                    val params: MutableMap<String, Any> = HashMap()
                    params[CLOCK_SKEW] =
                        Duration.ofMinutes(10).toMillis()
                    ValidationContext(params)
                })
        )
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

If you are using OpenSAML 5, then we have a simpler way, using OpenSaml5AuthenticationProvider.AssertionValidator:spring-doc.cn

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
        AssertionValidator assertionValidator = AssertionValidator.builder()
                .clockSkew(Duration.ofMinutes(10)).build();
		authenticationProvider.setAssertionValidator(assertionValidator);
        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
	}
}
@Configuration @EnableWebSecurity
class SecurityConfig {
    @Bean
    @Throws(Exception::class)
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml5AuthenticationProvider()
        val assertionValidator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(10)).build()
        authenticationProvider.setAssertionValidator(assertionValidator)
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

Converting an Assertion into an Authentication

OpenSamlXAuthenticationProvider#setResponseAuthenticationConverter provides a way for you to change how it converts your assertion into an Authentication instance.spring-doc.cn

You can set a custom converter in the following way:spring-doc.cn

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    Converter<ResponseToken, Saml2Authentication> authenticationConverter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter);

        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated())
            .saml2Login((saml2) -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }

}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Autowired
    var authenticationConverter: Converter<ResponseToken, Saml2Authentication>? = null

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml5AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter)
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

The ensuing examples all build off of this common construct to show you different ways this converter comes in handy.spring-doc.cn

Coordinating with a UserDetailsService

Or, perhaps you would like to include user details from a legacy UserDetailsService. In that case, the response authentication converter can come in handy, as can be seen below:spring-doc.cn

@Component
class MyUserDetailsResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
	private final ResponseAuthenticationConverter delegate = new ResponseAuthenticationConverter();
	private final UserDetailsService userDetailsService;

	MyUserDetailsResponseAuthenticationConverter(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	@Override
    public Saml2Authentication convert(ResponseToken responseToken) {
	    Saml2Authentication authentication = this.delegate.convert(responseToken); (1)
		UserDetails principal = this.userDetailsService.loadByUsername(username); (2)
		String saml2Response = authentication.getSaml2Response();
		Collection<GrantedAuthority> authorities = principal.getAuthorities();
		return new Saml2Authentication((AuthenticatedPrincipal) userDetails, saml2Response, authorities); (3)
    }

}
@Component
open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAuthenticationConverter,
        UserDetailsService userDetailsService): Converter<ResponseToken, Saml2Authentication> {

	@Override
    open fun convert(responseToken: ResponseToken): Saml2Authentication {
	    val authentication = this.delegate.convert(responseToken) (1)
		val principal = this.userDetailsService.loadByUsername(username) (2)
		val saml2Response = authentication.getSaml2Response()
		val authorities = principal.getAuthorities()
		return Saml2Authentication(userDetails as AuthenticatedPrincipal, saml2Response, authorities) (3)
    }

}
1 First, call the default converter, which extracts attributes and authorities from the response
2 Second, call the UserDetailsService using the relevant information
3 Third, return an authentication that includes the user details

If your UserDetailsService returns a value that also implements AuthenticatedPrincipal, then you don’t need a custom authentication implementation.spring-doc.cn

Or, if you are using OpenSaml 4, then you can achieve something similar as follows:spring-doc.cn

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
            Saml2Authentication authentication = OpenSaml4AuthenticationProvider
                    .createDefaultResponseAuthenticationConverter() (1)
                    .convert(responseToken);
            Assertion assertion = responseToken.getResponse().getAssertions().get(0);
            String username = assertion.getSubject().getNameID().getValue();
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); (2)
            return MySaml2Authentication(userDetails, authentication); (3)
        });

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Autowired
    var userDetailsService: UserDetailsService? = null

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
            val authentication = OpenSaml4AuthenticationProvider
                .createDefaultResponseAuthenticationConverter() (1)
                .convert(responseToken)
            val assertion: Assertion = responseToken.response.assertions[0]
            val username: String = assertion.subject.nameID.value
            val userDetails = userDetailsService!!.loadUserByUsername(username) (2)
            MySaml2Authentication(userDetails, authentication) (3)
        }
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}
1 First, call the default converter, which extracts attributes and authorities from the response
2 Second, call the UserDetailsService using the relevant information
3 Third, return a custom authentication that includes the user details
It’s not required to call OpenSaml4AuthenticationProvider's default authentication converter. It returns a Saml2AuthenticatedPrincipal containing the attributes it extracted from AttributeStatements as well as the single ROLE_USER authority.

Configuring the Principal Name

Sometimes, the principal name is not in the <saml2:NameID> element. In that case, you can configure the ResponseAuthenticationConverter with a custom strategy like so:spring-doc.cn

@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 = ResponseAuthenticationConverter()
    authenticationConverter.setPrincipalNameConverter { assertion ->
		// ... work with OpenSAML's Assertion object to extract the principal
    }
    return authenticationConverter
}

Configuring a Principal’s Granted Authorities

Spring Security automatically grants ROLE_USER when using OpenSamlXAuhenticationProvider. With OpenSaml5AuthenticationProvider, you can configure a different set of granted authorities like so:spring-doc.cn

@Bean
ResponseAuthenticationConverter authenticationConverter() {
	ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
	authenticationConverter.setPrincipalNameConverter((assertion) -> {
		// ... grant the needed authorities based on attributes in the assertion
	});
	return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
    val authenticationConverter = ResponseAuthenticationConverter()
    authenticationConverter.setPrincipalNameConverter{ assertion ->
		// ... grant the needed authorities based on attributes in the assertion
    }
    return authenticationConverter
}

Performing Additional Response Validation

OpenSaml4AuthenticationProvider validates the Issuer and Destination values right after decrypting the Response. You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.spring-doc.cn

For example, you can throw a custom exception with any additional information available in the Response object, like so:spring-doc.cn

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseValidator((responseToken) -> {
	Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
		.createDefaultResponseValidator()
		.convert(responseToken)
		.concat(myCustomValidator.convert(responseToken));
	if (!result.getErrors().isEmpty()) {
		String inResponseTo = responseToken.getInResponseTo();
		throw new CustomSaml2AuthenticationException(result, inResponseTo);
	}
	return result;
});

When using OpenSaml5AuthenticationProvider, you can do the same with less boilerplate:spring-doc.cn

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = ResponseValidator.withDefaults(myCustomValidator);
provider.setResponseValidator(responseValidator);

You can also customize which validation steps Spring Security should do. For example, if you want to skip Response#InResponseTo validation, you can call ResponseValidator's constructor, excluding InResponseToValidator from the list:spring-doc.cn

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = new ResponseValidator(new DestinationValidator(), new IssuerValidator());
provider.setResponseValidator(responseValidator);

OpenSAML performs Asssertion#InResponseTo validation in its BearerSubjectConfirmationValidator class, which is configurable using setAssertionValidator.spring-doc.cn

Performing Additional Assertion Validation

OpenSaml4AuthenticationProvider performs minimal validation on SAML 2.0 Assertions. After verifying the signature, it will:spring-doc.cn

  1. Validate <AudienceRestriction> and <DelegationRestriction> conditionsspring-doc.cn

  2. Validate <SubjectConfirmation>s, expect for any IP address informationspring-doc.cn

To perform additional validation, you can configure your own assertion validator that delegates to OpenSaml4AuthenticationProvider's default and then performs its own.spring-doc.cn

For example, you can use OpenSAML’s OneTimeUseConditionValidator to also validate a <OneTimeUse> condition, like so:spring-doc.cn

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
provider.setAssertionValidator(assertionToken -> {
    Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
            .createDefaultAssertionValidator()
            .convert(assertionToken);
    Assertion assertion = assertionToken.getAssertion();
    OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
    ValidationContext context = new ValidationContext();
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            return result;
        }
    } catch (Exception e) {
        return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
    }
    return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
var provider = OpenSaml4AuthenticationProvider()
var validator: OneTimeUseConditionValidator = ...
provider.setAssertionValidator { assertionToken ->
    val result = OpenSaml4AuthenticationProvider
        .createDefaultAssertionValidator()
        .convert(assertionToken)
    val assertion: Assertion = assertionToken.assertion
    val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
    val context = ValidationContext()
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            return@setAssertionValidator result
        }
    } catch (e: Exception) {
        return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
    }
    result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
While recommended, it’s not necessary to call OpenSaml4AuthenticationProvider's default assertion validator. A circumstance where you would skip it would be if you don’t need it to check the <AudienceRestriction> or the <SubjectConfirmation> since you are doing those yourself.

If you are using OpenSAML 5, then we have a simpler way using OpenSaml5AuthenticationProvider.AssertionValidator:spring-doc.cn

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
AssertionValidator assertionValidator = AssertionValidator.builder()
        .conditionValidators((c) -> c.add(validator)).build();
provider.setAssertionValidator(assertionValidator);
val provider = OpenSaml5AuthenticationProvider()
val validator: OneTimeUseConditionValidator = ...;
val assertionValidator = AssertionValidator.builder()
        .conditionValidators { add(validator) }.build()
provider.setAssertionValidator(assertionValidator)

You can use this same builder to remove validators that you don’t want to use like so:spring-doc.cn

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
AssertionValidator assertionValidator = AssertionValidator.builder()
        .conditionValidators((c) -> c.removeIf(AudienceRestrictionValidator.class::isInstance)).build();
provider.setAssertionValidator(assertionValidator);
val provider = new OpenSaml5AuthenticationProvider()
val assertionValidator = AssertionValidator.builder()
        .conditionValidators {
			c: List<ConditionValidator> -> c.removeIf { it is AudienceRestrictionValidator }
        }.build()
provider.setAssertionValidator(assertionValidator)

Customizing Decryption

Spring Security decrypts <saml2:EncryptedAssertion>, <saml2:EncryptedAttribute>, and <saml2:EncryptedID> elements automatically by using the decryption Saml2X509Credential instances registered in the RelyingPartyRegistration.spring-doc.cn

OpenSaml4AuthenticationProvider exposes two decryption strategies. The response decrypter is for decrypting encrypted elements of the <saml2:Response>, like <saml2:EncryptedAssertion>. The assertion decrypter is for decrypting encrypted elements of the <saml2:Assertion>, like <saml2:EncryptedAttribute> and <saml2:EncryptedID>.spring-doc.cn

You can replace OpenSaml4AuthenticationProvider's default decryption strategy with your own. For example, if you have a separate service that decrypts the assertions in a <saml2:Response>, you can use it instead like so:spring-doc.cn

MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }

If you are also decrypting individual elements in a <saml2:Assertion>, you can customize the assertion decrypter, too:spring-doc.cn

provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
There are two separate decrypters since assertions can be signed separately from responses. Trying to decrypt a signed assertion’s elements before signature verification may invalidate the signature. If your asserting party signs the response only, then it’s safe to decrypt all elements using only the response decrypter.

Using a Custom Authentication Manager

Of course, the authenticationManager DSL method can be also used to perform a completely custom SAML 2.0 authentication. This authentication manager should expect a Saml2AuthenticationToken object containing the SAML 2.0 Response XML data.spring-doc.cn

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(authenticationManager)
            )
        ;
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = customAuthenticationManager
            }
        }
        return http.build()
    }
}

Using Saml2AuthenticatedPrincipal

With the relying party correctly configured for a given asserting party, it’s ready to accept assertions. Once the relying party validates an assertion, the result is a Saml2Authentication with a Saml2AuthenticatedPrincipal.spring-doc.cn

This means that you can access the principal in your controller like so:spring-doc.cn

@Controller
public class MainController {
	@GetMapping("/")
	public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
		String email = principal.getFirstAttribute("email");
		model.setAttribute("email", email);
		return "index";
	}
}
@Controller
class MainController {
    @GetMapping("/")
    fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
        val email = principal.getFirstAttribute<String>("email")
        model.setAttribute("email", email)
        return "index"
    }
}
Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call getAttribute to get the list of attributes or getFirstAttribute to get the first in the list. getFirstAttribute is quite handy when you know that there is only one value.