arunan
arunan

Reputation: 961

How to design a good JWT authentication filter

I am new to JWT. There isn't much information available in the web, since I came here as a last resort. I already developed a spring boot application using spring security using spring session. Now instead of spring session we are moving to JWT. I found few links and now I can able to authenticate a user and generate token. Now the difficult part is, I want to create a filter which will be authenticate every request to the server,

  1. How will the filter validate the token? (Just validating the signature is enough?)
  2. If someone else stolen the token and make rest call, how will I verify that.
  3. How will I by-pass the login request in the filter? Since it doesn't have authorization header.

Upvotes: 50

Views: 54296

Answers (3)

pedrofb
pedrofb

Reputation: 39241

I will focus in the general tips on JWT, without regarding code implemementation (see other answers)

How will the filter validate the token? (Just validating the signature is enough?)

RFC7519 specifies how to validate a JWT (see 7.2. Validating a JWT), basically a syntactic validation and signature verification.

If JWT is being used in an authentication flow, we can look at the validation proposed by OpenID connect specification 3.1.3.4 ID Token Validation. Summarizing:

  • iss contains the issuer identifier (and aud contains client_id if using oauth)

  • current time between iat and exp

  • Validate the signature of the token using the secret key

  • sub identifies a valid user

If someone else stolen the token and make rest call, how will I verify that.

Possesion of a JWT is the proof of authentication. An attacker who stoles a token can impersonate the user. So keep tokens secure

  • Encrypt communication channel using TLS

  • Use a secure storage for your tokens. If using a web front-end consider to add extra security measures to protect localStorage/cookies against XSS or CSRF attacks

  • set short expiration time on authentication tokens and require credentials if token is expired

How will I by-pass the login request in the filter? Since it doesn't have authorization header.

The login form does not require a JWT token because you are going to validate the user credential. Keep the form out of the scope of the filter. Issue the JWT after successful authentication and apply the authentication filter to the rest of services

Then the filter should intercept all requests except the login form, and check:

  1. if user authenticated? If not throw 401-Unauthorized

  2. if user authorized to requested resource? If not throw 403-Forbidden

  3. Access allowed. Put user data in the context of request( e.g. using a ThreadLocal)

Upvotes: 13

Matthieu Saleta
Matthieu Saleta

Reputation: 1508

Here is a filter that can do what you need :

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;
    }
}

And the inclusion of the filter in the filter chain :

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();

    }
}

The TokenProvider class :

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;
        }
    }
}

Now to answer your questions :

  1. Done in this filter
  2. Protect your HTTP request, use HTTPS
  3. Just permit all on the /login URI (/authenticate in my code)

Upvotes: 46

Lazar Lazarov
Lazar Lazarov

Reputation: 2532

Take a look at this project it is very good implemented and has the needed documentation.

1. It the above project this is the only thing you need to validate the token and it is enough. Where token is the value of the Bearer into the request header.

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. Stealing the token is not so easy but in my experience you can protect yourself by creating a Spring session manually for every successfull log in. Also mapping the session unique ID and the Bearer value(the token) into a Map (creating a Bean for example with API scope).

@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;
    }
}

This SessionMapBean will be available for all sessions. Now on every request you will not only verify the token but also you will check if he mathces the session (checking the request session id does matches the one stored into the SessionMapBean). Of course session ID can be also stolen so you need to secure the communication. Most common ways of stealing the session ID is Session Sniffing (or the Men in the middle) and Cross-site script attack. I will not go in more details about them you can read how to protect yourself from that kind of attacks.

3. You can see it into the project I linked. Most simply the filter will validated all /api/* and you will login into a /user/login for example.

Upvotes: 1

Related Questions