Hello,

I use Spring Security 5.6.1 in my project.

I adopted the SAML login example from Spring Security Sample to connect with Keycloak IDP. In principle it works, but when I configure Keycloak to deliver multiple values for the same SAML attribute it seems that Spring Security SAML doesn't return all of them.

My SecurityConfig looks like the following:

@EnableWebSecurity
public class SecurityConfig {

 @Bean
  SecurityFilterChain app(HttpSecurity http) throws Exception {
    OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
    authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
      Saml2Authentication authentication = OpenSaml4AuthenticationProvider
          .createDefaultResponseAuthenticationConverter().convert(responseToken);
      return authentication;
    });

    http.authorizeHttpRequests()
        .antMatchers(unsecuredPattern).permitAll()
        .anyRequest().authenticated()
      .and()
        .saml2Login(Customizer.withDefaults())
          .authenticationManager(new ProviderManager(authenticationProvider))
        .saml2Logout(Customizer.withDefaults());

    return http.build();
  }

}

My IDP (Keycloak) returns after successful login the following SAML Token:

<samlp:Response (...)

  <saml:Assertion (...)

    <saml:AttributeStatement>
      <saml:Attribute Name="Role"
                      NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xsi:type="xs:string">Role1</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="Role"
                      NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xsi:type="xs:string">Role2</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="Role"
                      NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xsi:type="xs:string">Role3</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

When I stop in my code into the Lambda expression I can look into the returned Saml2Authentication-object and can access the containing principal.

((org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal)authentication.getPrincipal()).getAttribute("Role") returns '[Role3]', i.e. only the last 'Role' attribute value.

I would have expected to see all of the attribute values ('[Role1, Role2, Role3}').

Is this a wanted behaviour?

From my understanding there seems to me a bug in OpenSaml4AuthenticationProvider.getAssertionAttributes(Assertion assertion) which overwrites already existing attributes with the same name in the attributeMap.

Thanks

Comment From: marcusdacoregio

Hi @mrBofrost.

The getAssertionAttributes does retrieve all the <saml:AttributeValue> from a <saml:Attribute>. But in your scenario, you have multiple <saml:Attribute> with one <saml:AttributeValue> each. It would work if the response was something like this:

<samlp:Response (...)

  <saml:Assertion (...)

    <saml:AttributeStatement>
      <saml:Attribute Name="Role"
                      NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xsi:type="xs:string">Role1</saml:AttributeValue>
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xsi:type="xs:string">Role2</saml:AttributeValue>
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xsi:type="xs:string">Role3</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

If you want to customize how Spring Security converts the Response, you can use OpenSaml4AuthenticationProvider#setResponseAuthenticationConverter, like so:

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
Converter<ResponseToken, Saml2Authentication> authenticationConverter =
       createDefaultResponseAuthenticationConverter();
provider.setResponseAuthenticationConverter(responseToken -> {
      Saml2Authentication authentication = authenticationConverter.convert(responseToken);
      User user = myUserRepository.findByUsername(authentication.getName());
      return new MyAuthentication(authentication, user);
});

I'll close this for now but feel free to discuss further.

Comment From: mrBofrost

Thanks a lot for your answer. I'll give it a try.

Comment From: shawnweeks

@marcusdacoregio This is how Red Hat Keycloak and Oracle Access Manager return Roles by default. I'm not familiar with other IDPs but this kinda feels like something we should support OOB.