web-dev-qa-db-fra.com

plusieurs mécanismes d'authentification dans une seule application utilisant Java config

Actuellement, mon application utilise un mécanisme d'authentification unique qui consiste à utiliser LDAP pour l'authentification et l'autorisation. Ma configuration de sécurité ressemble à ceci

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .authorizeRequests()
            .anyRequest().fullyAuthenticated()
            .and()
            .httpBasic();
}

@Configuration
protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {

    @Value("${ldap-${env}.manager.dn}")
    private String managerDn;

    @Value("${ldap-${env}.manager.pass}")
    private String managerPass;

    @Value("${ldap-${env}.server.url}")
    private String url;

    @Value("${ldap.password.attribute:userPassword}")
    private String passwordAttr;

    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication().userDnPatterns("uid={0},ou=people").groupSearchBase("ou=groups")
                .groupSearchFilter("(member={0})").userSearchBase("ou=people").userSearchFilter("(uid={0})")
                .userDetailsContextMapper(new CustomLdapPersonContextMapper())
                // .passwordCompare()
                // .passwordAttribute(passwordAttr)
                // .passwordEncoder(new PlaintextPasswordEncoder())
                // .and()
                .contextSource().managerDn(managerDn).managerPassword(managerPass).url(url);
    }
}
}

Il existe cependant des situations dans lesquelles les utilisateurs peuvent entrer avec un jeton de session qui peut s'authentifier à partir d'un serveur de clés de session et un jeton valide renvoie un nom d'utilisateur qui peut ensuite être utilisé pour charger les informations d'authentification à partir de LDAP pour cet utilisateur. Donc, mon deuxième mécanisme d'authentification devrait se produire en premier. Si un jeton de session est présent dans les en-têtes http, il devrait procéder à l'authentification du jeton, puis à la recherche LDAP. Comment puis-je ajouter cette deuxième couche d'authentification.

20
adeelmahmood

J'ai passé pas mal de temps à me préoccuper de la sécurité printanière lors de l'utilisation d'une configuration Java pure. Pour que cela fonctionne, il faut quelques étapes. Ce devrait être quelque chose dans ce sens. Le processus de base est le suivant:

  • Créer des filtres personnalisés pour vérifier les demandes d'informations d'autorisation spécifiques

  • Chaque filtre renvoie la valeur null (si aucune autorisation de ce type n'est trouvée), ou un résumé AbstractAuthenticationToken

  • Si un filtre retourne un jeton, chaque méthode de prise en charge (classe) de AuthenticationProvider sera invoquée avec ce jeton renvoyant true | false s'il essaye l'authentification.

  • tentativeAuthentication sera ensuite appelée sur AuthenticationProvider qui prend en charge le jeton. Ici, vous effectuez des appels de service pour authentifier l'utilisateur. Vous pouvez ensuite lancer LoginException ou appeler authentication.setAuthenticated (true) et renvoyer le jeton pour une authentification réussie.

J'utilise cette configuration depuis un certain temps, prenant en charge diverses méthodes d'authentification (demande signée, nom d'utilisateur/mot de passe, oauth, etc.) et cela fonctionne assez bien. 

Vous pouvez également passer AuthenticationSuccessHandler's et AuthenticationFailuersHandler's aux filtres de sécurité personnalisés pour fournir des stratégies de redirection personnalisées et une gestion des défaillances.

Veillez également à configurer les correspondants ant dans les constructeurs du filtre pour contrôler les modèles d'URL appliqués par les filtres. Par exemple, un filtre de requête LDAP devrait probablement être vérifié avec toute requête "/ *", alors qu'un filtre de nom d'utilisateur/mot de passe peut uniquement être vérifié sur les POST vers/login ou quelque chose de similaire.

Exemple de code:

1) Créez des AuthenticationToken personnalisés pour chaque type d'authentification que vous souhaitez prendre en charge.

public class LDAPAuthorizationToken extends AbstractAuthenticationToken {
    private String token;

    public LDAPAuthorizationToken( String token ) {
        super( null );
        this.token = token;
    }

    public Object getCredentials() {
        return token;
    }

    public Object getPrincipal() {
        return null;
    }
}

public class OTPAuthorizationToken extends UsernamePasswordAuthenticationToken {
    private String otp;

    public OTPAuthorizationToken( String username, String password, String otp ) {
        super( username, password );
        this.otp = otp;
    }

    public String getOTP() {
        return otp;
    }
}

2) Créer des filtres de sécurité personnalisés pour chaque type

public class LDAPAuthorizationFilter extends AbstractAuthenticationProcessingFilter {
    @Autowired
    private UserDetailsService userDetailsService;

    public LDAPAuthorizationFilter() {
        super( "/*" ); // allow any request to contain an authorization header
    }

    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException
    {

        if ( request.getHeader( "Authorization" ) == null ) {
            return null; // no header found, continue on to other security filters
        }

        // return a new authentication token to be processed by the authentication provider
        return new LDAPAuthorizationToken( request.getHeader( "Authorization" ) );
    }
}

public class OTPAuthorizationFilter extends AbstractAuthenticationProcessingFilter {
    @Autowired
    private UserDetailsService userDetailsService;

    public OTPAuthorizationFilter() {
        super( "/otp_login" );
    }

    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException
    {

        if ( request.getParameter( "username" ) == null || request.getParameter( "password" ) == null || request.getParameter( "otp" ) == null ) {
            return null;
        }

        // return a new authentication token to be processed by the authentication provider
        return new OTPAuthorizationToken( request.getParameter( "username" ), request.getParameter( "password" ), request.getParameter( "otp" ) );
    }
}

3) Créer des fournisseurs d’authentification personnalisés

public class LDAPAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private MyAuthenticationService sampleService;

    @Override
    public Authentication authenticate( Authentication authentication ) throws AuthenticationException {
        LDAPAuthorizationToken auth = (LDAPAuthorizationToken)authentication;

        String username = sampleService.verifyToken( auth.getCredentials() );
        if ( username == null ) {
            throw new LoginException( "Invalid Token" );
        }

        auth.setAuthenticated( true );

        return auth;
    }

    @Override
    public boolean supports( Class<?> authentication ) {
        if ( authentication.isAssignableFrom( LDAPAuthorizationToken.class ) ) {
            return true;
        }
        return false;
    }
}

public class OTPAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private MyAuthenticationService sampleService;

    @Override
    public Authentication authenticate( Authentication authentication ) throws AuthenticationException {
        OTPAuthorizationToken auth = (OTPAuthorizationToken)authentication;

        String error = sampleService.loginWithOTP( auth.getPrincipal(), auth.getCredentials(), auth.getOTP() );
        if ( error != null ) {
            throw new LoginException( error );
        }

        auth.setAuthenticated( true );

        return auth;
    }

    @Override
    public boolean supports( Class<?> authentication ) {
        if ( authentication.isAssignableFrom( OTPAuthorizationToken.class ) ) {
            return true;
        }
        return false;
    }
}

4) Configurer la sécurité de printemps

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure( HttpSecurity http ) throws Exception {
        // configure filters
        http.addFilterBefore( new LDAPAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class );
        http.addFilterBefore( new OTPAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class );

        // configure authentication providers
        http.authenticationProvider( new LDAPAuthenticationProvider() );
        http.authenticationProvider( new OTPAuthenticationProvider() );

        // disable csrf
        http.csrf().disable();

        // setup security
        http.authorizeRequests()
            .anyRequest()
                .fullyAuthenticated()
                .and().httpBasic();
    }
}

J'espère que cela pourra aider!

41
Matt MacLean

Une autre option consiste à ajouter un deuxième fournisseur d’authentification: spécifiez simplement un autre fournisseur dans la variable AuthenticationManagerBuilder. Étant donné que l'annotation @EnableWebSecurity est annotée avec EnableGlobalAuthentication, vous pouvez configurer l'instance globale de AuthenticationManagerBuilder. (Voir le javadocs pour plus de détails.) 

Par exemple, nous avons ici un fournisseur d’authentification LDAP ainsi qu’un fournisseur d’authentification en mémoire (codé en dur) (c’est quelque chose que nous faisons en développement pour pouvoir tester les utilisateurs locaux):

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

      @Value("${user.role}")
      private String userRole; // i.e. ROLE_APP_USER

      @Value("${include.test.users}")
      private boolean includeTestUsers;

      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
          .antMatchers("/**/js/**").permitAll()
          .antMatchers("/**/images/**").permitAll()
          .antMatchers("/**/favicon.ico").permitAll()
          .antMatchers("/**/css/**").permitAll()
          .antMatchers("/**/fonts/**").permitAll()
          .antMatchers("/**").hasAnyRole(userRole)
          .and().formLogin().loginPage("/login").permitAll().and().logout().permitAll();

        http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
      }

      @Autowired
      public void configureGlobal(AuthenticationManagerBuilder auth, LdapContextSource contextSource) throws Exception {
        auth.ldapAuthentication()
          .userSearchBase("OU=Users OU")
          .userSearchFilter("sAMAccountName={0}")
          .groupSearchBase("OU=Groups OU")
          .groupSearchFilter("member={0}")
          .contextSource(contextSource);

        if (includeTestUsers) {
          auth.inMemoryAuthentication().withUser("user").password("u").authorities(userRole);
        }
      }
    }
9
Brice Roncace

Je veux juste ajouter à la réponse de mclema. Vous devrez peut-être ajouter un remplacement pour une authentification réussie et continuer la chaîne de filtrage, sinon l'utilisateur sera redirigé vers l'URL par défaut ("/") au lieu de l'originale (par exemple:/myrest/server/somemethod)

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult) throws IOException, ServletException {
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    chain.doFilter(request, response);
}
0
Mir3

La réponse acceptée pose le problème suivant: la demande actuelle n'est pas acceptée. la session est établie uniquement pour les demandes suivantes! Par conséquent, je devais configurer au point 2

public class MyAuthorizationFilter extends AbstractAuthenticationProcessingFilter {

    public MyAuthorizationFilter() {
        super( "/*" ); // allow any request to contain an authorization header
    }

    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException
    {

        if ( request.getHeader( "Authorization" ) == null ) {
            return null; // no header found, continue on to other security filters
        }

        // required to use the token 
        myNewToken = new MyAuthorizationToken( request.getHeader( "Authorization" ) );
        // and set in the current context ==> the current request is as well authorized
        SecurityContextHolder.getContext().setAuthentication(myNewToken);
        // return a new authentication token to be processed by the authentication provider
        return myNewToken;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        // try to authenticate the current request
        attemptAuthentication((HttpServletRequest) req, (HttpServletResponse) res);
        super.doFilter(req, res, chain);
    }
}

sinon la demande en cours n'est pas encore authentifiée bien qu'une session soit déjà créée !!! (Et les fournisseurs dont je n’ai pas besoin, c’est-à-dire qu’un filtre suffit.)

0
LeO