Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.
like image 873
arunan Avatar asked Feb 01 '17 08:02

arunan


People also ask

What is JWT authentication filter?

The JSON Web Token (JWT) Authentication filter checks if the incoming request has a valid JSON Web Token (JWT). It checks the validity of the JWT by verifying the JWT signature, audiences and issuer based on the HTTP filter configuration.

Is JWT best for authentication?

JWT is a particularly useful technology for API authentication and server-to-server authorization.

What is the best way to store JWT in client?

Like local storage, session storage is accessible by any javascript code running on the same domain that the web application is hosted. So the only thing that changes, is that when a user closes their browser, the JWT will disappear and the user will have to login again in their next visit to your web application.


2 Answers

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)
like image 57
Matthieu Saleta Avatar answered Sep 21 '22 11:09

Matthieu Saleta


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)

like image 28
pedrofb Avatar answered Sep 22 '22 11:09

pedrofb