web-dev-qa-db-fra.com

La sécurisation d'une application REST avec un JWT et une authentification de base a-t-elle un sens?

J'ai une application Spring REST qui a d'abord été sécurisée avec l'authentification de base.

J'ai ensuite ajouté un contrôleur de connexion qui crée un jeton Web JWT JSON qui est utilisé dans les demandes suivantes.

Puis-je déplacer le code suivant hors du contrôleur de connexion et dans le filtre de sécurité? Ensuite, je n'aurais plus besoin du contrôleur de connexion.

tokenAuthenticationService.addTokenToResponseHeader(responseHeaders, credentialsResource.getEmail());

Ou pourrais-je supprimer l'authentification de base?

Est-ce une bonne conception de mélanger l'authentification de base avec un JWT?

Bien que tout fonctionne bien, je suis un peu dans le noir ici pour mieux concevoir cette sécurité.

36
Stephane

En supposant 100% TLS pour toutes les communications - à la fois pendant et à tout moment après la connexion - l'authentification avec nom d'utilisateur/mot de passe via l'authentification de base et la réception d'un JWT en échange est un cas d'utilisation valide. C'est presque exactement comment fonctionne l'un des flux de OAuth 2 ('mot de passe')).

L'idée est que l'utilisateur final est authentifié via un point final, par ex. /login/token en utilisant le mécanisme de votre choix, et la réponse doit contenir le JWT qui doit être renvoyé pour toutes les demandes suivantes. Le JWT doit être un JWS (c'est-à-dire un JWT signé cryptographiquement) avec un champ d'expiration JWT approprié (exp): cela garantit que le client ne peut pas manipuler le JWT ou le faire vivre plus longtemps qu'il ne le devrait.

Vous n'avez pas besoin d'un X-Auth-Token en-tête soit: le schéma d'authentification HTTP Bearer a été créé pour ce cas d'utilisation exact: fondamentalement, tout bit d'information qui suit le nom du schéma Bearer est une information 'porteuse' qui doit être validée. Vous venez de définir l'en-tête Authorization:

Authorization: Bearer <JWT value here>

Mais cela étant dit, si votre REST n'est pas fiable (par exemple, un navigateur compatible JavaScript), je ne le ferais même pas: aucune valeur de la réponse HTTP accessible via JavaScript - essentiellement n'importe quelle valeur d'en-tête ou valeur de corps de réponse - pourrait être reniflé et intercepté via des attaques MITM XSS.

Il est préférable de stocker la valeur JWT dans un cookie http uniquement sécurisé (configuration de cookie: setSecure (true), setHttpOnly (true)). Cela garantit que le navigateur:

  1. ne transmettez le cookie que via une connexion TLS et,
  2. ne jamais rendre la valeur du cookie disponible pour le code JavaScript.

Cette approche est presque tout ce que vous devez faire pour la sécurité des meilleures pratiques. La dernière chose est de vous assurer que vous avez une protection CSRF sur chaque requête HTTP pour vous assurer que les domaines externes initiant des requêtes vers votre site ne peuvent pas fonctionner.

La façon la plus simple de le faire est de définir un cookie sécurisé uniquement (mais PAS http uniquement) avec une valeur aléatoire, par exemple un UUID.

Ensuite, à chaque demande sur votre serveur, assurez-vous que votre propre code JavaScript lit la valeur du cookie et la définit dans un en-tête personnalisé, par ex. X-CSRF-Token et vérifiez cette valeur à chaque demande du serveur. Les clients de domaine externe ne peuvent pas définir d'en-têtes personnalisés pour les demandes de votre domaine, sauf si le client externe obtient une autorisation via une demande d'options HTTP, de sorte que toute tentative d'attaque CSRF (par exemple dans une IFrame, peu importe) échouera pour eux.

Il s'agit de la meilleure sécurité disponible actuellement pour les clients JavaScript non fiables sur le Web. Stormpath a également écrit un article sur ces techniques si vous êtes curieux.

Enfin, le Stormpath Java Servlet Plugin fait déjà tout cela pour vous (et beaucoup plus cool, y compris des vérifications de sécurité automatisées supplémentaires), de sorte que vous n'avez jamais à l'écrire - ou pire - à le maintenir vous-même. Consultez la section Authentification de la demande HTTP et le formulaire/Ajax exemple pour voir comment l'utiliser HTH!

74
Les Hazlewood

Voici du code pour sauvegarder la réponse acceptée sur la façon de faire cela dans Spring .... simplement étendre UsernamePasswordAuthenticationFilter et l'ajouter à Spring Security ... cela fonctionne bien avec HTTP Basic Authentication + Spring Security

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {

        this.authenticationManager = authenticationManager;

    }

    @Override

    public Authentication attemptAuthentication(HttpServletRequest req,

                                                HttpServletResponse res) throws AuthenticationException {

        try {

            ApplicationUser creds = new ObjectMapper()

                    .readValue(req.getInputStream(), ApplicationUser.class);

            return authenticationManager.authenticate(

                    new UsernamePasswordAuthenticationToken(

                            creds.getUsername(),

                            creds.getPassword(),

                            new ArrayList<>())

            );

        } catch (IOException e) {

            throw new RuntimeException(e);

        }

    }

    @Override

    protected void successfulAuthentication(HttpServletRequest req,

                                            HttpServletResponse res,

                                            FilterChain chain,

                                            Authentication auth) throws IOException, ServletException {

        String token = Jwts.builder()

                .setSubject(((User) auth.getPrincipal()).getUsername())

                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))

                .signWith(SignatureAlgorithm.HS512, SECRET)

                .compact();

        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);

    }

}

en utilisant JWT lib .:

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

classe de configuration de démarrage de printemps

package com.vanitysoft.payit.security.web.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

    import com.vanitysoft.payit.util.SecurityConstants;

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
         @Autowired
           private UserDetailsService userDetailsService;

            @Autowired
            private  BCryptPasswordEncoder bCryptPasswordEncoder;

         @Override
           protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService)
                      .passwordEncoder(bCryptPasswordEncoder);
           }

         @Override
            protected void configure(HttpSecurity http) throws Exception {
             http.cors().and().csrf().disable()
                    .authorizeRequests()                             
                        .antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll()
                        .antMatchers("/user/**").authenticated()
                        .and()
                        .httpBasic()
                        .and()
                        .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                        .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        .and()
                        .logout()
                        .permitAll();

            }
    }
1
Jeryl Cook