Search code examples
springspring-securitysaml-2.0spring-saml

How to override the NameID value in SAMLAuthenticationProvider?


I'm using Spring Security SAML 1.0.1. My application uses this XML to configure the SAMLAuthenticationProvider bean :

<!-- SAML Authentication Provider responsible for validating of received SAML messages -->
<b:bean id="samlAuthenticationProvider" class="org.springframework.security.saml.SAMLAuthenticationProvider">
    <!-- OPTIONAL property: can be used to store/load user data after login -->
    <b:property name="userDetails">
        <b:bean class="eu.ueb.acem.services.auth.SamlAuthenticationUserDetailsService"/>
    </b:property>
    <b:property name="forcePrincipalAsString" value="false"/>
</b:bean>

The class "SamlAuthenticationUserDetailsService" implements the loadUserBySAML(SAMLCredential) method. If the user doesn't exist in the database, it is created.

@Override
public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException {
    logger.info("entering loadUserBySAML({})", credential);
    String userID = credential.getNameID().getValue();
    logger.info("{} is logged in", userID);
    Map<String, String> mapOfAttributesFriendlyNamesAndValues = new HashMap<String, String>();
    mapOfAttributesFriendlyNamesAndValues.put("eduPersonAffiliation", null);
    mapOfAttributesFriendlyNamesAndValues.put("eduPersonPrincipalName", null);
    mapOfAttributesFriendlyNamesAndValues.put("eduPersonPrimaryAffiliation", null);
    mapOfAttributesFriendlyNamesAndValues.put("supannEtablissement", null);
    mapOfAttributesFriendlyNamesAndValues.put("supannEntiteAffectationPrincipale", null);
    mapOfAttributesFriendlyNamesAndValues.put("supannOrganisme", null);
    mapOfAttributesFriendlyNamesAndValues.put("displayName", null);
    mapOfAttributesFriendlyNamesAndValues.put("mail", null);
    mapOfAttributesFriendlyNamesAndValues.put("givenName", null);
    mapOfAttributesFriendlyNamesAndValues.put("sn", null);
    mapOfAttributesFriendlyNamesAndValues.put("uid", null);

    for (Attribute attribute : credential.getAttributes()) {
        logger.info("attribute friendly name={}", attribute.getFriendlyName());
        if (mapOfAttributesFriendlyNamesAndValues.containsKey(attribute.getFriendlyName())) {
            // We set the values of the property
            for (XMLObject attributeValueXMLObject : credential.getAttribute(attribute.getName()).getAttributeValues()) {
                logger.info("We care about this attribute, getAttributeValue={}", getAttributeValue(attributeValueXMLObject));
                mapOfAttributesFriendlyNamesAndValues.put(attribute.getFriendlyName(), getAttributeValue(attributeValueXMLObject));
            }
        }
        else {
            logger.info("We don't care about this attribute");
        }
    }
    Person user = usersService.getUser(mapOfAttributesFriendlyNamesAndValues.get("eduPersonPrincipalName"));
    user.setLogin(mapOfAttributesFriendlyNamesAndValues.get("eduPersonPrincipalName"));
    user.setEmail(mapOfAttributesFriendlyNamesAndValues.get("mail"));
    user.setName(mapOfAttributesFriendlyNamesAndValues.get("displayName"));
    user.setAdministrator(true);
    user = usersService.updatePerson(user);
    logger.info("leaving loadUserBySAML");
    return loadUserByUser(user);
}

private UserDetails loadUserByUser(Person targetUser) {
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

    // Roles
    if (targetUser.isAdministrator()) {
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
    }
    else {
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
    }

    return new User(targetUser.getLogin(), targetUser.getPassword(),
            true, // enabled
            true, // account not expired
            true, // credentials not expired
            true, // account not locked
            authorities);
}

I have the following behaviour :

  • I request a page on my SP which requires authentication
  • I'm redirected to the IDP (would be better to the Discovery Service)
  • The IDP correctly returns the credential
  • My loadUserBySAML method is called
  • If the user doesn't exist, it is created in the database with a login equal to its email address
  • The requested page loads and the created User is not reused, instead, a new User is created with a login equal to the value of credential.getNameID().getValue() (e.g. "_246558c0d7c514447292d750df577b6b").

Question : how can I set the "NameID" attribute of the credential to be the e-mail address?

I have read multiple times the section "9.4 Authentication object" of the documentation but I still don't understand how to tell Spring Security that my UserDetails object should be referenced with the email address and not with the NameID value.


Solution

  • I think that the problem might be that SAMLAuthenticationProvider by default always uses NameID, even when UserDetails gets returned from the SAMLUserDetailsService. It's done so for backwards compatibility with previous versions, although I'm considering changing it to avoid confusion.

    In order to use values returned from your UserDetails set property forcePrincipalAsString to false on bean SAMLAuthenticationProvider.