Add ResponseAuthenticationConverter

Aside from simplifying configuration, this commit also makes it possible
to provide a response authentication converter that doesn't need the
NameID element to be present.

Closes gh-12136
This commit is contained in:
Josh Cummings 2025-04-07 16:35:28 -06:00
parent 3e686abf50
commit 3869b13e68
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
7 changed files with 470 additions and 15 deletions

View File

@ -58,3 +58,108 @@ Xml::
<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)
----
======

View File

@ -250,12 +250,135 @@ class SecurityConfig {
----
======
== Converting an `Assertion` into an `Authentication`
`OpenSamlXAuthenticationProvider#setResponseAuthenticationConverter` provides a way for you to change how it converts your assertion into an `Authentication` instance.
You can set a custom converter in the following way:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@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();
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@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.
[[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
== 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:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@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>
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@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 xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] using the relevant information
<3> Third, return an authentication that includes the user details
[TIP]
====
If your `UserDetailsService` returns a value that also implements `AuthenticatedPrincipal`, then you don't need a custom authentication implementation.
====
Or, if you are using OpenSaml 4, then you can achieve something similar as follows:
[tabs]
======
Java::
@ -336,6 +459,78 @@ open class SecurityConfig {
It's not required to call ``OpenSaml4AuthenticationProvider``'s default authentication converter.
It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s 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:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ResponseAuthenticationConverter authenticationConverter() {
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
authenticationConverter.setPrincipalNameConverter((assertion) -> {
// ... work with OpenSAML's Assertion object to extract the principal
});
return authenticationConverter;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@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:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ResponseAuthenticationConverter authenticationConverter() {
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
authenticationConverter.setPrincipalNameConverter((assertion) -> {
// ... grant the needed authorities based on attributes in the assertion
});
return authenticationConverter;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
val authenticationConverter = ResponseAuthenticationConverter()
authenticationConverter.setPrincipalNameConverter{ assertion ->
// ... grant the needed authorities based on attributes in the assertion
}
return authenticationConverter
}
----
======
[[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
== Performing Additional Response Validation

View File

@ -339,7 +339,7 @@ It's common to need to set other values in the `<saml2:LogoutRequest>` than the
By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation`
* The `DestinationValidator` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation`
* The `ID` attribute - a GUID
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
* The `<NameID>` element - from `Authentication#getName`
@ -424,7 +424,7 @@ It's common to need to set other values in the `<saml2:LogoutResponse>` than the
By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation`
* The `DestinationValidator` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation`
* The `ID` attribute - a GUID
* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
* The `<Status>` element - `SUCCESS`

View File

@ -110,6 +110,8 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
private Converter<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter = createDefaultResponseAuthenticationConverter();
private boolean validateResponseAfterAssertions = false;
private static final Set<String> includeChildStatusCodes = new HashSet<>(
Arrays.asList(StatusCode.REQUESTER, StatusCode.RESPONDER, StatusCode.VERSION_MISMATCH));
@ -143,6 +145,10 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
this.responseAuthenticationConverter = responseAuthenticationConverter;
}
void setValidateResponseAfterAssertions(boolean validateResponseAfterAssertions) {
this.validateResponseAfterAssertions = validateResponseAfterAssertions;
}
static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
return (responseToken) -> {
Response response = responseToken.getResponse();
@ -321,7 +327,9 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
"Did not decrypt response [" + response.getID() + "] since it is not signed"));
}
if (!this.validateResponseAfterAssertions) {
result = result.concat(this.responseValidator.convert(responseToken));
}
boolean allAssertionsSigned = true;
for (Assertion assertion : response.getAssertions()) {
AssertionToken assertionToken = new AssertionToken(assertion, token);
@ -337,12 +345,17 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
+ "Please either sign the response or all of the assertions.";
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, description));
}
if (this.validateResponseAfterAssertions) {
result = result.concat(this.responseValidator.convert(responseToken));
}
else {
Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions());
if (firstAssertion != null && !hasName(firstAssertion)) {
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
"Assertion [" + firstAssertion.getID() + "] is missing a subject");
result = result.concat(error);
}
}
if (result.hasErrors()) {
Collection<Saml2Error> errors = result.getErrors();
@ -422,7 +435,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
};
}
private boolean hasName(Assertion assertion) {
static boolean hasName(Assertion assertion) {
if (assertion == null) {
return false;
}
@ -435,7 +448,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
return assertion.getSubject().getNameID().getValue() != null;
}
private static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
MultiValueMap<String, Object> attributeMap = new LinkedMultiValueMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
@ -452,7 +465,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
return new LinkedHashMap<>(attributeMap); // gh-11785
}
private static List<String> getSessionIndexes(Assertion assertion) {
static List<String> getSessionIndexes(Assertion assertion) {
List<String> sessionIndexes = new ArrayList<>();
for (AuthnStatement statement : assertion.getAuthnStatements()) {
sessionIndexes.add(statement.getSessionIndex());

View File

@ -85,6 +85,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
*/
public OpenSaml4AuthenticationProvider() {
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml4Template());
this.delegate.setValidateResponseAfterAssertions(false);
}
/**

View File

@ -58,12 +58,15 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@ -118,6 +121,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
setResponseValidator(ResponseValidator.withDefaults());
setAssertionValidator(AssertionValidator.withDefaults());
setResponseAuthenticationConverter(new ResponseAuthenticationConverter());
}
/**
@ -300,6 +304,21 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
(token) -> responseAuthenticationConverter.convert(new ResponseToken(token)));
}
/**
* Indicate when to validate response attributes, like {@code Destination} and
* {@code Issuer}. By default, this value is set to false, meaning that response
* attributes are validated first. Setting this value to {@code true} allows you to
* use a response authentication converter that doesn't rely on the {@code NameID}
* element in the {@link Response}'s assertion.
* @param validateResponseAfterAssertions when to validate response attributes
* @since 6.5
* @see #setResponseAuthenticationConverter
* @see ResponseAuthenticationConverter
*/
public void setValidateResponseAfterAssertions(boolean validateResponseAfterAssertions) {
this.delegate.setValidateResponseAfterAssertions(validateResponseAfterAssertions);
}
/**
* Construct a default strategy for validating the SAML 2.0 Response
* @return the default response validator strategy
@ -373,12 +392,11 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
* Construct a default strategy for converting a SAML 2.0 Response and
* {@link Authentication} token into a {@link Saml2Authentication}
* @return the default response authentication converter strategy
* @deprecated please use {@link ResponseAuthenticationConverter} instead
*/
@Deprecated
public static Converter<ResponseToken, Saml2Authentication> createDefaultResponseAuthenticationConverter() {
Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2Authentication> delegate = BaseOpenSamlAuthenticationProvider
.createDefaultResponseAuthenticationConverter();
return (token) -> delegate
.convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken()));
return new ResponseAuthenticationConverter();
}
/**
@ -852,4 +870,81 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
}
/**
* A default implementation of {@link OpenSaml5AuthenticationProvider}'s response
* authentication converter. It will take the principal name from the
* {@link org.opensaml.saml.saml2.core.NameID} element. It will also extract the
* assertion attributes and session indexes. You can either configure the principal
* name converter and granted authorities converter in this class or you can
* post-process this class's result through delegation.
*
* @author Josh Cummings
* @since 6.5
*/
public static final class ResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
private Converter<Assertion, String> principalNameConverter = ResponseAuthenticationConverter::authenticatedPrincipal;
private Converter<Assertion, Collection<GrantedAuthority>> grantedAuthoritiesConverter = ResponseAuthenticationConverter::grantedAuthorities;
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
Response response = responseToken.response;
Saml2AuthenticationToken token = responseToken.token;
Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
String username = this.principalNameConverter.convert(assertion);
Map<String, List<Object>> attributes = BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion);
List<String> sessionIndexes = BaseOpenSamlAuthenticationProvider.getSessionIndexes(assertion);
DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, attributes,
sessionIndexes);
String registrationId = responseToken.token.getRelyingPartyRegistration().getRegistrationId();
principal.setRelyingPartyRegistrationId(registrationId);
return new Saml2Authentication(principal, token.getSaml2Response(),
this.grantedAuthoritiesConverter.convert(assertion));
}
/**
* Use this strategy to extract the principal name from the {@link Assertion}. By
* default, this will retrieve it from the
* {@link org.opensaml.saml.saml2.core.Subject}'s
* {@link org.opensaml.saml.saml2.core.NameID} value.
*
* <p>
* Note that because of this, if there is no
* {@link org.opensaml.saml.saml2.core.NameID} present, then the default throws an
* exception.
* </p>
* @param principalNameConverter the conversion strategy to use
*/
public void setPrincipalNameConverter(Converter<Assertion, String> principalNameConverter) {
Assert.notNull(principalNameConverter, "principalNameConverter cannot be null");
this.principalNameConverter = principalNameConverter;
}
/**
* Use this strategy to grant authorities to a principal given the first
* {@link Assertion} in the response. By default, this will grant
* {@code ROLE_USER}.
* @param grantedAuthoritiesConverter the conversion strategy to use
*/
public void setGrantedAuthoritiesConverter(
Converter<Assertion, Collection<GrantedAuthority>> grantedAuthoritiesConverter) {
Assert.notNull(grantedAuthoritiesConverter, "grantedAuthoritiesConverter cannot be null");
this.grantedAuthoritiesConverter = grantedAuthoritiesConverter;
}
private static String authenticatedPrincipal(Assertion assertion) {
if (!BaseOpenSamlAuthenticationProvider.hasName(assertion)) {
throw new Saml2AuthenticationException(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
"Assertion [" + assertion.getID() + "] is missing a subject"));
}
return assertion.getSubject().getNameID().getValue();
}
private static Collection<GrantedAuthority> grantedAuthorities(Assertion assertion) {
return AuthorityUtils.createAuthorityList("ROLE_USER");
}
}
}

View File

@ -22,6 +22,7 @@ import java.io.ObjectOutputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
@ -71,12 +72,15 @@ import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.AssertionValidator;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseValidator;
import org.springframework.security.saml2.provider.service.authentication.TestCustomOpenSaml5Objects.CustomOpenSamlObject;
@ -92,6 +96,7 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link OpenSaml5AuthenticationProvider}
@ -660,6 +665,47 @@ public class OpenSaml5AuthenticationProviderTests {
verify(authenticationConverter).convert(any());
}
@Test
public void authenticateWhenResponseAuthenticationConverterComponentConfiguredThenUses() {
Converter<Assertion, Collection<GrantedAuthority>> grantedAuthoritiesConverter = mock(Converter.class);
given(grantedAuthoritiesConverter.convert(any())).willReturn(AuthorityUtils.createAuthorityList("CUSTOM"));
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
authenticationConverter.setGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setResponseAuthenticationConverter(authenticationConverter);
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
Saml2AuthenticationToken token = token(response, verifying(registration()));
Authentication authentication = provider.authenticate(token);
assertThat(AuthorityUtils.authorityListToSet(authentication.getAuthorities())).containsExactly("CUSTOM");
verify(grantedAuthoritiesConverter).convert(any());
}
@Test
public void authenticateWhenValidateResponseAfterAssertionsThenCanHaveResponseAuthenticationConverterThatDoesntNeedANameID() {
Converter<ResponseToken, Saml2Authentication> responseAuthenticationConverter = mock(Converter.class);
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setValidateResponseAfterAssertions(true);
provider.setResponseAuthenticationConverter(responseAuthenticationConverter);
Response response = TestOpenSamlObjects
.signedResponseWithOneAssertion((r) -> r.getAssertions().get(0).setSubject(null));
Saml2AuthenticationToken token = token(response, verifying(registration()));
provider.authenticate(token);
verify(responseAuthenticationConverter).convert(any());
}
@Test
public void authenticateWhenValidateResponseBeforeAssertionsThenMustHaveNameID() {
Converter<ResponseToken, Saml2Authentication> responseAuthenticationConverter = mock(Converter.class);
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setValidateResponseAfterAssertions(false);
provider.setResponseAuthenticationConverter(responseAuthenticationConverter);
Response response = TestOpenSamlObjects
.signedResponseWithOneAssertion((r) -> r.getAssertions().get(0).setSubject(null));
Saml2AuthenticationToken token = token(response, verifying(registration()));
assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token));
verifyNoInteractions(responseAuthenticationConverter);
}
@Test
public void setResponseAuthenticationConverterWhenNullThenIllegalArgument() {
// @formatter:off