web-dev-qa-db-fra.com

Spring Security LDAP et Remember Me

Je crée une application avec Spring Boot qui a une intégration avec LDAP. J'ai pu me connecter avec succès au serveur LDAP et authentifier l'utilisateur. Maintenant, je dois ajouter une fonctionnalité de souvenir de moi. J'ai essayé de parcourir différents messages ( this ) mais je n'ai pas pu trouver de réponse à mon problème. Sécurité officielle du printemps document déclare que

Si vous utilisez un fournisseur d'authentification qui n'utilise pas un UserDetailsService (par exemple, le fournisseur LDAP), cela ne fonctionnera que si vous disposez également d'un bean UserDetailsService dans votre contexte d'application.

Voici mon code de travail avec quelques réflexions initiales pour ajouter la fonctionnalité de souvenir de moi:

WebSecurityConfig

import com.ui.security.CustomUserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.event.LoggerListener;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    String DOMAIN = "ldap-server.com";
    String URL = "ldap://ds.ldap-server.com:389";


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/ui/**").authenticated()
                .antMatchers("/", "/home", "/UIDL/**", "/ui/**").permitAll()
                .anyRequest().authenticated()
        ;
        http
                .formLogin()
                .loginPage("/login").failureUrl("/login?error=true").permitAll()
                .and().logout().permitAll()
        ;

        // Not sure how to implement this
        http.rememberMe().rememberMeServices(rememberMeServices()).key("password");

    }

    @Override
    protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {

        authManagerBuilder
                .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
                .userDetailsService(userDetailsService())
        ;
    }

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {

        ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL);
        provider.setConvertSubErrorCodesToExceptions(true);
        provider.setUseAuthenticationRequestCredentials(true);
        provider.setUserDetailsContextMapper(userDetailsContextMapper());
        return provider;
    }

    @Bean
    public UserDetailsContextMapper userDetailsContextMapper() {
        UserDetailsContextMapper contextMapper = new CustomUserDetailsServiceImpl();
        return contextMapper;
    }

    /**
     * Impl of remember me service
     * @return
     */
    @Bean
    public RememberMeServices rememberMeServices() {
//        TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", userService);
//        rememberMeServices.setCookieName("cookieName");
//        rememberMeServices.setParameter("rememberMe");
        return rememberMeServices;
    }

    @Bean
    public LoggerListener loggerListener() {
        return new LoggerListener();
    }
}

CustomUserDetailsServiceImpl

public class CustomUserDetailsServiceImpl implements UserDetailsContextMapper {

    @Autowired
    SecurityHelper securityHelper;
    Log ___log = LogFactory.getLog(this.getClass());

    @Override
    public LoggedInUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> grantedAuthorities) {

        LoggedInUserDetails userDetails = null;
        try {
            userDetails = securityHelper.authenticateUser(ctx, username, grantedAuthorities);
        } catch (NamingException e) {
            e.printStackTrace();
        }

        return userDetails;
    }

    @Override
    public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {

    }
}

Je sais que je dois implémenter UserService d'une manière ou d'une autre, mais je ne sais pas comment cela peut être réalisé.

29
Maksim

La configuration des fonctionnalités RememberMe avec LDAP pose deux problèmes:

  • sélection de l'implémentation RememberMe correcte (jetons vs jetons persistants)
  • sa configuration à l'aide de Spring Java Configuration

Je vais les prendre étape par étape.

La fonction Se souvenir de moi basée sur les jetons (TokenBasedRememberMeServices) fonctionne de la manière suivante lors de l'authentification:

  • l'utilisateur est authentifié (contre AD) et nous connaissons actuellement l'ID et le mot de passe de l'utilisateur
  • nous construisons la valeur nom d'utilisateur + expirationTime + mot de passe + staticKey et créons un hachage MD5
  • nous créons un cookie qui contient le nom d'utilisateur + l'expiration + le hachage calculé

Lorsque l'utilisateur souhaite revenir au service et être authentifié à l'aide de la fonctionnalité Se souvenir de moi, nous:

  • vérifier si le cookie existe et n'est pas expiré
  • remplir l'ID utilisateur à partir du cookie et appeler le UserDetailsService fourni qui devrait renvoyer des informations liées à l'ID utilisateur, y compris le mot de passe
  • nous calculons ensuite le hachage à partir des données renvoyées et vérifions que le hachage dans le cookie correspond à la valeur que nous avons calculée
  • s'il correspond, nous renvoyons l'objet d'authentification de l'utilisateur

Le processus de vérification du hachage est requis afin de s'assurer que personne ne peut créer un "faux" cookie de souvenir de moi, qui leur permettrait de se faire passer pour un autre utilisateur. Le problème est que ce processus repose sur la possibilité de charger le mot de passe à partir de notre référentiel - mais cela est impossible avec Active Directory - nous ne pouvons pas charger le mot de passe en clair basé sur le nom d'utilisateur.

Cela rend l'implémentation basée sur les jetons impropre à l'utilisation avec AD (sauf si nous commençons à créer un magasin d'utilisateurs local qui contient le mot de passe ou d'autres informations d'identification basées sur l'utilisateur secrètes et je ne suggère pas cette approche car je ne connais pas d'autres détails de votre candidature, même si cela peut être une bonne façon de procéder).

L'autre implémentation de Remember me est basée sur des jetons persistants (PersistentTokenBasedRememberMeServices) et fonctionne comme ceci (de manière un peu simplifiée):

  • lorsque l'utilisateur s'authentifie, nous générons un jeton aléatoire
  • nous stockons le jeton dans le stockage avec des informations sur l'ID de l'utilisateur qui lui est associé
  • nous créons un cookie qui inclut l'ID du jeton

Lorsque l'utilisateur souhaite s'authentifier, nous:

  • vérifier si nous avons le cookie avec ID de jeton disponible
  • vérifier si l'ID de jeton existe dans la base de données
  • charger les données de l'utilisateur en fonction des informations de la base de données

Comme vous pouvez le voir, le mot de passe n'est plus requis, bien que nous ayons maintenant besoin d'un stockage de jetons (généralement une base de données, nous pouvons utiliser en mémoire pour les tests) qui est utilisé à la place de la vérification du mot de passe.

Et cela nous amène à la partie configuration. La configuration de base pour le rappel persistant basé sur des jetons ressemble à ceci:

@Override
protected void configure(HttpSecurity http) throws Exception {           
    ....
    String internalSecretKey = "internalSecretKey";
    http.rememberMe().rememberMeServices(rememberMeServices(internalSecretKey)).key(internalSecretKey);
}

 @Bean
 public RememberMeServices rememberMeServices(String internalSecretKey) {
     BasicRememberMeUserDetailsService rememberMeUserDetailsService = new BasicRememberMeUserDetailsService();
     InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();
     PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(staticKey, rememberMeUserDetailsService, rememberMeTokenRepository);
     services.setAlwaysRemember(true);
     return services;
 }

Cette implémentation utilisera un stockage de jetons en mémoire qui devrait être remplacé par JdbcTokenRepositoryImpl pour la production. Le UserDetailsService fourni est responsable du chargement des données supplémentaires pour l'utilisateur identifié par l'ID utilisateur chargé à partir du cookie Remember me. L'implémentation la plus simple peut ressembler à ceci:

public class BasicRememberMeUserDetailsService implements UserDetailsService {
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         return new User(username, "", Collections.<GrantedAuthority>emptyList());
     }
}

Vous pouvez également fournir une autre implémentation UserDetailsService qui charge des attributs supplémentaires ou des appartenances à des groupes à partir de votre base de données AD ou interne, selon vos besoins. Cela pourrait ressembler à ceci:

@Bean
public RememberMeServices rememberMeServices(String internalSecretKey) {
    LdapContextSource ldapContext = getLdapContext();

    String searchBase = "OU=Users,DC=test,DC=company,DC=com";
    String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))";
    FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(searchBase, searchFilter, ldapContext);
    search.setSearchSubtree(true);

    LdapUserDetailsService rememberMeUserDetailsService = new LdapUserDetailsService(search);
    rememberMeUserDetailsService.setUserDetailsMapper(new CustomUserDetailsServiceImpl());

    InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();

    PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(internalSecretKey, rememberMeUserDetailsService, rememberMeTokenRepository);
    services.setAlwaysRemember(true);
    return services;
}

@Bean
public LdapContextSource getLdapContext() {
    LdapContextSource source = new LdapContextSource();
    source.setUserDn("user@"+DOMAIN);
    source.setPassword("password");
    source.setUrl(URL);
    return source;
}

Cela vous permettra de vous souvenir de la fonctionnalité qui fonctionne avec LDAP et fournit les données chargées dans RememberMeAuthenticationToken qui seront disponibles dans la SecurityContextHolder.getContext().getAuthentication(). Il pourra également réutiliser votre logique existante pour l'analyse des données LDAP dans un objet utilisateur (CustomUserDetailsServiceImpl).

Comme sujet séparé, il y a aussi un problème avec le code affiché dans la question, vous devez remplacer le:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
            .userDetailsService(userDetailsService())
    ;

avec:

    authManagerBuilder
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
    ;

L'appel à userDetailsService ne doit être effectué que pour ajouter une authentification basée sur DAO (par exemple contre une base de données) et doit être appelé avec une implémentation réelle du service de détails utilisateur. Votre configuration actuelle peut conduire à des boucles infinies.

29
Vladimír Schäfer

Il semble qu'il vous manque une instance de UserService à laquelle votre RememberMeService a besoin d'une référence. Puisque vous utilisez LDAP, vous aurez besoin d'une version LDAP de UserService. Je ne connais que les implémentations JDBC/JPA, mais ressemble à org.springframework.security.ldap.userdetails.LdapUserDetailsManager est ce que vous recherchez. Votre configuration ressemblerait alors à ceci:

@Bean
public UserDetailsService getUserDetailsService() {
    return new LdapUserDetailsManager(); // TODO give it whatever constructor params it needs
}

@Bean
public RememberMeServices rememberMeServices() {
    TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", getUserDetailsService());
    rememberMeServices.setCookieName("cookieName");
    rememberMeServices.setParameter("rememberMe");
    return rememberMeServices;
}
0
SergeyB