30 votes

Comment concevoir un bon filtre d'authentification JWT

Je suis nouveau chez JWT. Il n'y a pas beaucoup d'informations disponibles sur le web, puisque je suis venu ici en dernier recours. J'ai déjà développé une application spring boot en utilisant spring security en utilisant spring session. Maintenant, au lieu de spring session, nous passons à JWT. J'ai trouvé quelques liens et maintenant je suis capable d'authentifier un utilisateur et de générer un jeton. Maintenant, la partie difficile est, je veux créer un filtre qui sera authentifier chaque demande au serveur,

  1. Comment le filtre validera-t-il le jeton ? (La validation de la signature suffit-elle ?)
  2. Si quelqu'un d'autre a volé le jeton et effectue un appel de repos, comment puis-je le vérifier ?
  3. Comment contourner la demande de connexion dans le filtre ? Puisqu'elle n'a pas d'en-tête d'autorisation.

24voto

Matthieu Saleta Points 986

Voici un filtre qui peut faire ce dont vous avez besoin :

public class JWTFilter extends GenericFilterBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class);

    private final TokenProvider tokenProvider;

    public JWTFilter(TokenProvider tokenProvider) {

        this.tokenProvider = tokenProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
        ServletException {

        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String jwt = this.resolveToken(httpServletRequest);
            if (StringUtils.hasText(jwt)) {
                if (this.tokenProvider.validateToken(jwt)) {
                    Authentication authentication = this.tokenProvider.getAuthentication(jwt);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
            filterChain.doFilter(servletRequest, servletResponse);

            this.resetAuthenticationAfterRequest();
        } catch (ExpiredJwtException eje) {
            LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
            ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            LOGGER.debug("Exception " + eje.getMessage(), eje);
        }
    }

    private void resetAuthenticationAfterRequest() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }

    private String resolveToken(HttpServletRequest request) {

        String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            String jwt = bearerToken.substring(7, bearerToken.length());
            return jwt;
        }
        return null;
    }
}

Et l'inclusion du filtre dans la chaîne de filtration :

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    public final static String AUTHORIZATION_HEADER = "Authorization";

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private AuthenticationProvider authenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(this.authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        JWTFilter customFilter = new JWTFilter(this.tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);

        // @formatter:off
        http.authorizeRequests().antMatchers("/css/**").permitAll()
        .antMatchers("/images/**").permitAll()
        .antMatchers("/js/**").permitAll()
        .antMatchers("/authenticate").permitAll()
        .anyRequest().fullyAuthenticated()
        .and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
        .and().logout().permitAll();
        // @formatter:on
        http.csrf().disable();

    }
}

La classe TokenProvider :

public class TokenProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class);

    private static final String AUTHORITIES_KEY = "auth";

    @Value("${spring.security.authentication.jwt.validity}")
    private long tokenValidityInMilliSeconds;

    @Value("${spring.security.authentication.jwt.secret}")
    private String secretKey;

    public String createToken(Authentication authentication) {

        String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(","));

        ZonedDateTime now = ZonedDateTime.now();
        ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS);

        Date issueDate = Date.from(now.toInstant());
        Date expirationDate = Date.from(expirationDateTime.toInstant());

        return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
                    .signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
    }

    public Authentication getAuthentication(String token) {

        Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();

        Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
                    .map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());

        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    public boolean validateToken(String authToken) {

        try {
            Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException e) {
            LOGGER.info("Invalid JWT signature: " + e.getMessage());
            LOGGER.debug("Exception " + e.getMessage(), e);
            return false;
        }
    }
}

Maintenant, pour répondre à vos questions :

  1. Fait dans ce filtre
  2. Protégez votre demande HTTP, utilisez HTTPS.
  3. Il suffit de permettre à tous sur le /login URI ( /authenticate dans mon code)

9voto

pedrofb Points 19206

Je me concentrerai sur les conseils généraux concernant JWT, sans me préoccuper de l'implémentation du code (voir les autres réponses).

Comment le filtre validera-t-il le jeton ? (La validation de la signature suffit-elle ?)

La RFC7519 spécifie comment valider un JWT (voir 7.2. Validation d'un JWT ), essentiellement un validation syntaxique et vérification des signatures .

Si JWT est utilisé dans un flux d'authentification, nous pouvons nous pencher sur la validation proposée par la spécification OpenID connect. 3.1.3.4 Validation des jetons d'identification . Résumer :

  • iss contient l'identifiant de l'émetteur (et aud contient client_id si vous utilisez oauth)

  • le temps actuel entre iat et exp

  • Valider la signature du jeton en utilisant la clé secrète

  • sub identifie un utilisateur valide

Si quelqu'un d'autre a volé le jeton et effectue un appel de repos, comment puis-je le vérifier ?

La possession d'un JWT est la preuve de l'authentification. Un attaquant qui vole un jeton peut se faire passer pour l'utilisateur. Gardez donc les jetons en sécurité

  • Crypter le canal de communication en utilisant TLS

  • Utilisez un stockage sécurisé pour vos jetons. Si vous utilisez un frontal web, pensez à ajouter des mesures de sécurité supplémentaires pour protéger localStorage/cookies contre les attaques XSS ou CSRF.

  • set courte durée d'expiration sur les jetons d'authentification et demande des informations d'identification si le jeton est expiré

Comment contourner la demande de connexion dans le filtre ? Puisqu'elle n'a pas d'en-tête d'autorisation.

Le formulaire de connexion ne nécessite pas de jeton JWT car vous allez valider l'identité de l'utilisateur. Gardez le formulaire hors de la portée du filtre. Émettez le JWT après une authentification réussie et appliquez le filtre d'authentification au reste des services.

Ensuite, le filtre devrait intercepter toutes les demandes sauf le formulaire de connexion, et vérifiez :

  1. si l'utilisateur est authentifié ? Si non, lancez 401-Unauthorized

  2. si l'utilisateur est autorisé à accéder à la ressource demandée ? Si non, lancer 403-Forbidden

  3. Accès autorisé. Placez les données de l'utilisateur dans le contexte de la demande (par exemple, en utilisant un ThreadLocal).

1voto

Lazar Lazarov Points 1067

Jetez un coup d'œil à ce le projet est très bien mis en œuvre et dispose de la documentation nécessaire.

1 . Dans le projet ci-dessus, c'est la seule chose dont vous avez besoin pour valider le jeton et c'est suffisant. Où se trouve token est la valeur de la Bearer dans l'en-tête de la demande.

try {
    final Claims claims = Jwts.parser().setSigningKey("secretkey")
        .parseClaimsJws(token).getBody();
    request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
    throw new ServletException("Invalid token.");
}

2 . Voler le jeton n'est pas si facile mais, d'après mon expérience, vous pouvez vous protéger en créant manuellement une session Spring pour chaque connexion réussie. Mettez également en correspondance l'ID unique de la session et la valeur du porteur (le jeton) dans un fichier de type Carte (création d'un Bean par exemple avec une portée API).

@Component
public class SessionMapBean {
    private Map<String, String> jwtSessionMap;
    private Map<String, Boolean> sessionsForInvalidation;
    public SessionMapBean() {
        this.jwtSessionMap = new HashMap<String, String>();
        this.sessionsForInvalidation = new HashMap<String, Boolean>();
    }
    public Map<String, String> getJwtSessionMap() {
        return jwtSessionMap;
    }
    public void setJwtSessionMap(Map<String, String> jwtSessionMap) {
        this.jwtSessionMap = jwtSessionMap;
    }
    public Map<String, Boolean> getSessionsForInvalidation() {
        return sessionsForInvalidation;
    }
    public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) {
        this.sessionsForInvalidation = sessionsForInvalidation;
    }
}

Ce site SessionMapBean sera disponible pour toutes les sessions. Maintenant, à chaque demande, vous ne vérifierez pas seulement le jeton, mais vous vérifierez également s'il mathématise la session (en vérifiant que l'identifiant de session de la demande correspond bien à celui stocké dans le fichier SessionMapBean ). Bien entendu, l'identifiant de session peut également être volé. Il faut donc sécuriser la communication. Les moyens les plus courants de voler l'ID de session sont les suivants Reniflage de session (ou les Hommes au milieu) et Attaque intersites par script. . Je ne vais pas entrer dans les détails, vous pouvez lire comment vous protéger de ce genre d'attaques.

3. Vous pouvez le voir dans le projet que j'ai lié. Plus simplement, le filtre validera tous les /api/* et vous vous connecterez à un /user/login par exemple.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X