web-dev-qa-db-fra.com

Comment concevoir un bon filtre d'authentification JWT

Je suis nouveau à JWT. Il n’ya pas beaucoup d’informations disponibles sur le Web, puisque j’y suis arrivé en dernier recours. J'ai déjà développé une application de démarrage à ressort utilisant la sécurité de printemps utilisant la session de printemps. Maintenant, au lieu de la session de printemps, nous passons à JWT. J'ai trouvé peu de liens et je peux maintenant authentifier un utilisateur et générer un jeton. Maintenant la partie difficile est, je veux créer un filtre qui authentifiera chaque demande au serveur,

  1. Comment le filtre va-t-il valider le jeton? (Il suffit de valider la signature?)
  2. Si quelqu'un d'autre a volé le jeton et a fait une pause, comment vais-je le vérifier?.
  3. Comment vais-je contourner la demande de connexion dans le filtre? Comme il n'a pas d'en-tête d'autorisation.
30
arunan

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 filtres:

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 requête HTTP, utilisez HTTPS
  3. Permettez simplement à tous sur le /login URI (/authenticate dans mon code)
23
Matthieu Saleta

Je vais me concentrer sur les astuces générales sur JWT, sans tenir compte de l’implémentation de code (voir les autres réponses)

Comment le filtre va-t-il valider le jeton? (Il suffit de valider la signature?)

La RFC7519 spécifie comment valider un JWT (voir 7.2. Valider un JWT ), essentiellement une validation syntaxique et une vérification de la signature .

Si JWT est utilisé dans un flux d'authentification, nous pouvons examiner la validation proposée par la spécification de connexion OpenID .1.3.4 ID Token Validation . Résumant:

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

  • heure actuelle entre iat et exp

  • Valider la signature du jeton à l'aide de la clé secrète

  • sub identifie un utilisateur valide

Si quelqu'un d'autre a volé le jeton et a fait une pause, comment vais-je le vérifier?.

La possession d'un JWT est la preuve de l'authentification. Un attaquant qui vole un jeton peut emprunter l'identité de l'utilisateur. Alors gardez les jetons en sécurité

  • Chiffrer le canal de communication à l'aide de TLS

  • Utilisez un stockage sécurisé pour vos jetons. Si vous utilisez une interface Web, envisagez d'ajouter des mesures de sécurité supplémentaires pour protéger les stockages/cookies locaux contre les attaques XSS ou CSRF.

  • définir un délai d'expiration court sur les jetons d'authentification et exiger des informations d'identification si le jeton a expiré

Comment vais-je contourner la demande de connexion dans le filtre? Comme il n'a pas d'en-tête d'autorisation.

Le formulaire de connexion ne nécessite pas de jeton JWT car vous allez valider les informations d'identification de l'utilisateur. Gardez la forme 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érifier:

  1. si utilisateur authentifié? Si non jeter 401-Unauthorized

  2. si utilisateur autorisé à demander la ressource? Si non jeter 403-Forbidden

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

7
pedrofb

Jetez un oeil à this le projet est très bien implémenté et dispose de la documentation nécessaire.

1 . Si le projet ci-dessus est la seule chose dont vous avez besoin pour valider le jeton, cela suffit. Où token est la valeur de Bearer dans l'en-tête de la requête.

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 une session Spring manuellement pour chaque connexion réussie. Mappez également l'ID unique de la session et la valeur du porteur (le jeton) dans un Carte (création d’un bean par exemple avec une portée d’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 SessionMapBean sera disponible pour toutes les sessions. Désormais, à chaque demande, vous vérifierez non seulement le jeton, mais également s'il vérifiera si la session est en cours (vérifier que l'identifiant de session de la demande correspond à celui stocké dans le SessionMapBean). Bien sûr, l'ID de session peut également être volé, vous devez donc sécuriser la communication. Les moyens les plus courants de voler l'ID de session sont Session Sniffing (ou les hommes au milieu) et entre sites attaque de script . Je n'entrerai pas 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é. Le plus simplement, le filtre validera tous les /api/* et vous vous connecterez à un /user/login par exemple.

1
Lazar Lazarov