49

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.
arunan
  • 922
  • 1
  • 17
  • 25
  • Are you asking about code or about generalities of how a JWT filter should work? – pedrofb Feb 01 '17 at 09:37
  • Could you share the code used to generate the JWT? I configured spring to generate JWT using OAuth2, but I can't see any tokens exchanged between Auth Server and the web-app... – Teo Dec 19 '17 at 07:55

4 Answers4

45

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)
Matthieu Saleta
  • 1,388
  • 1
  • 11
  • 17
  • Thanks for your answer, but will you please explain these two lines, Authentication authentication = this.tokenProvider.getAuthentication(jwt); SecurityContextHolder.getContext().setAuthentication(authentication); – arunan Feb 01 '17 at 12:43
  • Are you storing the password in the JWT token? – arunan Feb 01 '17 at 12:43
  • 2
    No the password is not stored in the JWT. `this.tokenProvider.getAuthentication(jwt)` decrypt the jwt using the secret key and return a new Spring Security `UsernamePasswordAuthenticationToken` without password - it extracts the username and the authorities from the claims. Then I put this AuthenticationToken inside the `SecurityContextHolder` so Spring Security consider that the user is logged. – Matthieu Saleta Feb 01 '17 at 12:54
  • I've added the TokenProvider class to show you. It will be better than my explanations :) – Matthieu Saleta Feb 01 '17 at 12:58
  • 2
    Note that this example is sessionless. The AuthenticationToken is put inside the SecurityContextHolder, the filterChain continue to execute the call to the Rest API and then it is reseted. – Matthieu Saleta Feb 01 '17 at 13:02
  • This is not a good solution. The filter returns unauthorized if your request does not contain a JWT token. In practice you may want multiple auth tokens (for example Bearer Token + Basic for API access). The filters should work in tandem with authentication providers. – tggm Oct 25 '19 at 21:54
  • Well... The subject is about the design of a JWT authentication filter so it's quite logical that it returns unauthorized if a JWT token is not provided. Nevertheless, some filters can be added to manage the other authentication types. The goal of the filter is just to intercept a header (here the JWT one) to authenticate the user inside spring security. This can be done with all authentication headers and if none of the filters can authenticate the user, Spring Security will at last return unauthorized. – Matthieu Saleta Oct 27 '19 at 09:59
  • Pretty good example that shows all the important bits and how they tie in together. One thing you would want to do differently in a real application, is each time you generate a new token, fetch the latest user grants, etc from the "source" of user data. It could be that the user is disabled or their roles may have changed. – neesh Apr 18 '20 at 19:13
  • can you tell me about `auth.authenticationProvider(this.authenticationProvider);` and where it furthur related with code, or `.userDetailsService(userDetailsService)` second approch @MatthieuSaleta ? – Ashish Kamble May 13 '20 at 06:53
  • what mean of this line code this.resetAuthenticationAfterRequest(); – Long tran Aug 22 '23 at 13:41
  • @Longtran This post is quite old so I don't remember well but we need to clean the Authentication object that we put inside the SecurityContext during the process so if the thread is reused, no Authentication is present inside the SecurityContext. – Matthieu Saleta Aug 23 '23 at 13:09
13

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)

Community
  • 1
  • 1
pedrofb
  • 37,271
  • 5
  • 94
  • 142
1

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.

Community
  • 1
  • 1
Lazar Lazarov
  • 2,412
  • 4
  • 26
  • 35
0

I have used a simple approach to handle JWT exceptions in AuthFilter.

RequestFiler.java

@Component
public class RequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtUtil;

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
        
        final String token = getAccessToken(request);
        JwtTokenValidity tokenValidity = jwtUtil.validateAccessToken(token);
        
        if (tokenValidity.isValid()) {
            setAuthenticationContext(token, request);
            filterChain.doFilter(request, response);
        } else {
            setUnauthorizedResponse(response, tokenValidity.getMessage());
        }
    }

    private void setUnauthorizedResponse(HttpServletResponse response, String reason) {
        try {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write(JsonUtils.toJson(ApiResponse.generateErrorResponse(HttpStatus.UNAUTHORIZED, reason)));
        } catch (IOException e) {
            logger.error(String.format(e.getMessage()));
        }
}

JwtUtils.java

@Component
public class JwtTokenUtil {
    
    public JwtTokenValidity validateAccessToken(final String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            logger.info(String.format("Token is valid/verified."));
            return new JwtTokenValidity(true, "Token is valid/verified.");
        } catch (ExpiredJwtException ex) {
            logger.error("JWT expired.", ex.getMessage());
            return new JwtTokenValidity(false, "Token is expired.");
        } catch (IllegalArgumentException ex) {
            logger.error("Token is null, empty or only whitespace.", ex.getMessage());
            return new JwtTokenValidity(false, "Token is null, empty or only whitespace.");
        } catch (MalformedJwtException ex) {
            logger.error("Token is invalid.", ex);
            return new JwtTokenValidity(false, "Token is invalid.");
        } catch (UnsupportedJwtException ex) {
            logger.error("JWT is not supported.", ex);
            return new JwtTokenValidity(false, "JWT is not supported.");
        } catch (SignatureException ex) {
            logger.error("Signature validation failed.");
            return new JwtTokenValidity(false, "JWT Signature validation failed.");
        }
    }
    
    public static class JwtTokenValidity {
    
    private boolean isValid;
    private String message;
    
    public JwtTokenValidity(boolean isValid, String message) {
        this.isValid = isValid;
        this.message = message;
    }
    
    public boolean isValid() {
        return isValid;
    }
    public void setValid(boolean isValid) {
        this.isValid = isValid;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}
}
AmeeQ
  • 117
  • 1
  • 3
  • 12