web-dev-qa-db-fra.com

Sécurité Spring: Custom UserDetailsService n'étant pas appelé (à l'aide de l'authentification Auth0)

Je suis nouveau dans le framework Spring, alors je m'excuse par avance pour les lacunes dans ma compréhension.

J'utilise Auth0 pour sécuriser mon API, qui fonctionne parfaitement. Ma configuration et configuration sont les mêmes que la configuration suggérée de la documentation Auth0:

// SecurityConfig.Java
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // auth0 config vars here

    @Override
    protected void configure(HttpSecurity http) {
        JwtWebSecurityConfigurer
                .forRS256(apiAudience, issuer)
                .configure(http)
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/api/public").permitAll()
                .antMatchers(HttpMethod.GET, "/api/private").authenticated();
    }
}

Avec cette configuration, le principal de sécurité du ressort est défini sur l'ID utilisateur (sub) du jeton jwt: auth0|5b2b.... Cependant, au lieu de simplement userId, je veux qu'il soit défini sur l'utilisateur correspondant (à partir de ma base de données). Ma question est de savoir comment faire cela.

Ce que j'ai essayé

J'ai essayé d'implémenter un UserDetailsService personnalisé basé sur une base de données que j'ai copié à partir de ce tutoriel . Cependant, il n'est pas appelé, peu importe la façon dont j'ai essayé de l'ajouter à ma conf. J'ai essayé de l'ajouter de différentes manières sans aucun effet:

// SecurityConfig.Java (changes only)

    // My custom userDetailsService, overriding the loadUserByUsername
    // method from Spring Framework's UserDetailsService.
    @Autowired
    private MyUserDetailsService userDetailsService;

    protected void configure(HttpSecurity http) {
        http.userDetailsService(userDetailsService);  // Option 1
        http.authenticationProvider(authenticationProvider());  // Option 2
        JwtWebSecurityConfigurer
                [...]  // The rest unchanged from above
    }

    @Override  // Option 3 & 4: Override the following method
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider());  // Option 3
        auth.userDetailsService(userDetailsService);  // Option 4
    }

    @Bean  // Needed for Options 2 or 4
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        return authProvider;
    }

Malheureusement, aucune des questions similaires "utilisateurDétails non appelés" ne m’a aidé, car j’ai besoin de l’associer à l’authentification Auth0. 

Je ne suis pas sûr d'être sur la bonne voie avec cela. Il me semble étrange que je ne trouve aucune documentation d'Auth0 sur ce cas d'utilisation extrêmement courant, alors il me manque peut-être quelque chose d'évident.

PS: Je ne sais pas si cela est pertinent, mais ce qui suit est toujours enregistré lors de l’initialisation.

Jun 27, 2018 11:25:22 AM com.test.UserRepository initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.

EDIT 1:

Sur la base de la réponse de Ashish451, j'ai essayé de copier son CustomUserDetailsService et ajouté ce qui suit à mon SecurityConfig:

@Autowired
private CustomUserDetailsService userService;

@Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

@Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
    auth.userDetailsService( userService );
}

Malheureusement, avec ces modifications, CustomUserDetailsService n'est toujours pas appelé.

EDIT 2:

Sortie lors de l'ajout de la méthode de journalisation suggérée par @Norberto Ritzmann:

Jul 04, 2018 3:49:22 PM com.test.repositories.UserRepositoryImpl initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
Jul 04, 2018 3:49:22 PM com.test.runner.JettyRunner testUserDetailsImpl
INFO: UserDetailsService implementation: com.test.services.CustomUserDetailsService
7
Joakim

J'ai fini par demander à l'assistance d'Auth0 à ce sujet et ils m'ont répondu qu'il n'était pas possible de modifier le principal sans modifier la source de la bibliothèque.

Cependant, ils proposent une approche alternative consistant à utiliser une bibliothèque de validation JWT (par exemple, https://github.com/auth0/Java-jwt ) au lieu de leur SDK Spring Security API.

Ma solution sera de modifier mon code pour qu'il fonctionne avec uniquement le jeton en tant que principal.

2
Joakim

Il s’agit peut-être d’un problème d’initialisation du contexte spring-boot, ce qui signifie que l’annotation @Autowired ne peut pas être résolue lors de l’initialisation de la classe de configuration. 

Vous pouvez essayer l'annotation @ComponentScan() au-dessus de votre classe de configuration et charger explicitement votre MyUserDetailsService. (voir: https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html#using-boot-importing-configuration ). Ceci fait, je recommanderais ce qui suit dans votre classe de configuration:

@Autowired
private MyUserDetailsService userService;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}

J'espère que cela peut vous aider.

2
git-flo

En examinant le code de votre adaptateur, vous générez un jeton JWT dans la configuration elle-même. Je ne sais pas ce qui est apiAudience, émetteur mais il a généré sous de JWT je suppose . Votre problème est que vous voulez changer de sous JWT selon votre base de données.

J'ai récemment implémenté la sécurité JWT dans l'application d'amorçage Spring. 

Et je configure UserName après l'avoir récupéré depuis la base de données.

J'ai ajouté du code avec pkg info pour plus de clarté.

// Ma classe d'adaptateur . C'est la même chose que la votre sauf une chose quej'y ai ajouté un filtre. Dans ce filtre, j'authentifie le jeton JWT. Ce filtre sera appelé chaque fois qu'une URL de repos sécurisé est déclenchée. 

import Java.nio.charset.StandardCharsets;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;

import com.dev.myapp.jwt.model.CustomUserDetailsService;
import com.dev.myapp.security.RestAuthenticationEntryPoint;
import com.dev.myapp.security.TokenAuthenticationFilter;
import com.dev.myapp.security.TokenHelper;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {




    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private CustomUserDetailsService jwtUserDetailsService; // Get UserDetail bu UserName

    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint; // Handle any exception during Authentication

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //  Binds User service for User and Password Query from Database with Password Encryption
    @Autowired
    public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
        auth.userDetailsService( jwtUserDetailsService )
            .passwordEncoder( passwordEncoder() );
    }

    @Autowired
    TokenHelper tokenHelper;  // Contains method for JWT key Generation, Validation and many more...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
        .exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
        .authorizeRequests()
        .antMatchers("/auth/**").permitAll()
        .anyRequest().authenticated().and()
        .addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);

        http.csrf().disable();
    }


    //  Patterns to ignore from JWT security check
    @Override
    public void configure(WebSecurity web) throws Exception {
        // TokenAuthenticationFilter will ignore below paths
        web.ignoring().antMatchers(
                HttpMethod.POST,
                "/auth/login"
        );
        web.ignoring().antMatchers(
                HttpMethod.GET,
                "/",
                "/assets/**",
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js"
            );

    }
}

// Service utilisateur pour obtenir les détails de l'utilisateur

@Transactional
@Repository
public class CustomUserDetailsService implements UserDetailsService {

    protected final Log LOGGER = LogFactory.getLog(getClass());

    @Autowired
    private UserRepo userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User uu = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
        } else {
            return user;
        }
    }

}

// Gestionnaire d'accès non autorisé

@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

// Chaîne de filtres pour la validation du jeton JWT

import Java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.Apache.commons.logging.Log;
import org.Apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    protected final Log logger = LogFactory.getLog(getClass());

    private TokenHelper tokenHelper;

    private UserDetailsService userDetailsService;

    public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
        this.tokenHelper = tokenHelper;
        this.userDetailsService = userDetailsService;
    }


    @Override
    public void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {

        String username;
        String authToken = tokenHelper.getToken(request);

        logger.info("AuthToken: "+authToken);

        if (authToken != null) {
            // get username from token
            username = tokenHelper.getUsernameFromToken(authToken);
            logger.info("UserName: "+username);
            if (username != null) {
                // get user
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (tokenHelper.validateToken(authToken, userDetails)) {
                    // create authentication
                    TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
                    authentication.setToken(authToken);
                    SecurityContextHolder.getContext().setAuthentication(authentication); // Adding Token in Security COntext
                }
            }else{
                logger.error("Something is wrong with Token.");
            }
        }
        chain.doFilter(request, response);
    }
}

// classe TokenBasedAuthentication

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;


public class TokenBasedAuthentication extends AbstractAuthenticationToken {

    private static final long serialVersionUID = -8448265604081678951L;
    private String token;
    private final UserDetails principle;

    public TokenBasedAuthentication( UserDetails principle ) {
        super( principle.getAuthorities() );
        this.principle = principle;
    }

    public String getToken() {
        return token;
    }

    public void setToken( String token ) {
        this.token = token;
    }

    @Override
    public boolean isAuthenticated() {
        return true;
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    @Override
    public UserDetails getPrincipal() {
        return principle;
    }

}

// Classe d'assistance pour la génération de JWT et la logique de validation

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import Java.util.Date;

import javax.servlet.http.HttpServletRequest;

import org.Apache.commons.logging.Log;
import org.Apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.dev.myapp.common.TimeProvider;
import com.dev.myapp.entity.User;


@Component
public class TokenHelper {

    protected final Log LOGGER = LogFactory.getLog(getClass());

    @Value("${app.name}") // reading details from property file added in Class path
    private String APP_NAME;

    @Value("${jwt.secret}")
    public String SECRET;

    @Value("${jwt.licenseSecret}")
    public String LICENSE_SECRET;

    @Value("${jwt.expires_in}")
    private int EXPIRES_IN;

    @Value("${jwt.mobile_expires_in}")
    private int MOBILE_EXPIRES_IN;

    @Value("${jwt.header}")
    private String AUTH_HEADER;

    @Autowired
    TimeProvider timeProvider;  // return current time. Basically Deployment time.

    private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;


    //  Generate Token based on UserName. You can Customize this 
    public String generateToken(String username) {
        String audience = generateAudience();
        return Jwts.builder()
                .setIssuer( APP_NAME )
                .setSubject(username)
                .setAudience(audience)
                .setIssuedAt(timeProvider.now())
                .setExpiration(generateExpirationDate())
                .signWith( SIGNATURE_ALGORITHM, SECRET )
                .compact();
    }


    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getIssuedAtDateFromToken(token);
        return (
                username != null &&
                username.equals(userDetails.getUsername())
        );
    }


   //  If Token is valid will extract all claim else throw appropriate error
    private Claims getAllClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.error("Could not get all claims Token from passed token");
            claims = null;
        }
        return claims;
    }


    private Date generateExpirationDate() {
        long expiresIn = EXPIRES_IN;
        return new Date(timeProvider.now().getTime() + expiresIn * 1000);
    }

}

Pour ce journal 

No authentication manager set. Reauthentication of users when changing passwords 

Puisque vous n'avez pas implémenté de méthodes avec Name loadUserByUsername . Vous obtenez ce journal.

Modifier 1:

J'utilise Filter Chain uniquement pour valider le jeton et ajouter un utilisateur dans un contexte de sécurité qui sera extrait du jeton .... 

J'utilise JWT et vous utilisez AuthO, seule l'implémentation est différente. Suis ajouté la mise en œuvre complète pour un flux de travail complet.

Vous vous concentrez sur la mise en œuvre de authenticationManagerBean et configureGlobal from WebSecurityConfig class pour utiliser UserService.

et TokenBasedAuthentication class implémentation. 

Vous pouvez sauter d'autres choses.

2
MyTwoCents