Tuesday, August 9, 2016

[DevNote] JWT Token Base Authentication in Spring MVC

JWT Token Base Authentication in Spring MVC


Keywords: Spring MVC, Spring Security, Jwt, MongoDB

Session based authentication requires server to keep session information of client logins which is making server not stateless and raises problems of scalability. Token based authentication has several advantages since server is freed from all the bookkeeping for sessions. Moreover token based authentication is immune to cross site request forgery(CSRF) by design and can be shared within the HTTP request header to other domains. 

This article stated many more advantages of token based authentication with comparison to cookie based authentication:

https://auth0.com/blog/angularjs-authentication-with-cookies-vs-token/

So how we make our Spring MVC backend serve the token based authentication? Here I have my own example shared with JSON web token which is "an open, industry standard RFC 7519 method for representing claims securely between two parties" according to its official site. And JWT has multiple libraries available in many languages, for Java we are going to use this JWT library as our tool to generate, sign and validate our JWT token.

All the source code in this blog is available from Github: https://github.com/yhuang69/PoormanPaperSearch


First thing first, the Jwt library we are using:

1
2
3
4
5
6
  <!-- JWT -->
  <dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.4</version>
  </dependency>

In Spring, we can usually assign the token handling task to one of our customized filter, this middleware can perform tasks like extracting and parse/decoding the token and verify token. In the following example, I just created a JwtTokenAuthenticationFilter extending the GenericFilterBean.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.hackinghorse.mockPaperSearch.secure;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.GenericFilterBean;

import com.hackinghorse.mockPaperSearch.secure.service.SimpleTokenStrategy;

import io.jsonwebtoken.Claims;

public class JwtTokenAuthenticationFilter extends GenericFilterBean {
 
 static Logger log = Logger.getLogger(JwtTokenAuthenticationFilter.class.getName());
 
 @Autowired
 private SimpleTokenStrategy jwtStrategy;
 
 
 @Autowired
 private UserDetailsService userDetailsService;
 
 @Override
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
   throws IOException, ServletException {
  final HttpServletRequest request = (HttpServletRequest) req;
  
  String tokenStr = jwtStrategy.getTokenFromRequest(request);
  
  /* 
   * TODO: Change this filter to authentication provider and JwtAuthenticationToken strategy 
   * so that we can throw an AuthenticationException directly.
   * 
   * Use AuthenticationProvider and Authentication Interfaces instead!
   * 
   */
  if (tokenStr != null && SecurityContextHolder.getContext().getAuthentication() == null) {
   try {
    final Claims claims = jwtStrategy.parseToken(tokenStr);
    String username = claims.getSubject();
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    if (jwtStrategy.validateToken(claims, userDetails)) {
     UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
       userDetails, null, userDetails.getAuthorities());
     authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
     SecurityContextHolder.getContext().setAuthentication(authentication);
    }
   } catch (Exception e) {
    log.debug("mockPaperSearchDev: JWT token is invalid");
   }
  }
  
  filterChain.doFilter(req, res);
 }

}

In the doFilter we first find the token from the incoming HttpServletRequest then we use a helper class to parse the token into claims. The Claims object which contains the payload of the JWT token will have all information we need for this user. In my token the subject ("sub" field) is the username, and it also contains token creation time, expiration time, and roles granted to this user. Upon parsing any malformed token will cause the parse() function to throw exception.

If the token is found, the next step is to check out the user document from our database and verify if the user does exist and the token has not expired. If token is valid, we will create an UsernamePasswordAuthenticationToken object and set to our security context so that our AuthenticationManager can work on this AuthenticationToken.

In fact, I think it's better to use AuthenticationManager interfaces and let AuthenticationManager delegate an authentication to AuthenticationProvider and perform the authentication so we can throw an AuthenticationException directly when we have a malformed or missing token. (I may follow up with some example on that)

Now here's how to setup the filter in our filter chain middleware:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
@EnableWebSecurity
@ComponentScan(basePackages="com.hackinghorse.mockPaperSearch")
public class AuthenConfiguration extends WebSecurityConfigurerAdapter {

 @Autowired
 private UnauthorizedEntryPoint unauthorizedHandler;

 @Autowired
 private UserDetailsService userDetailsService;
 
 @Bean
 public JwtTokenAuthenticationFilter authenticationTokenFilterBean() {
  return new JwtTokenAuthenticationFilter();
 }
 
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(this.userDetailsService);
    }
    
    @Bean(name="authenticationManager")
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
 @Override
 protected void configure(HttpSecurity http) throws Exception {
  http.csrf().disable()
  .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        .authorizeRequests().antMatchers("/login", "/signUp", "/static/**", "/search", "/search/**").permitAll()
        .antMatchers("/user**", "/user/**", "/login/refresh", "/uploadFile", "/userFiles/**").access("hasRole('SUBSCRIBER')")
        .antMatchers("/staff**", "/staff/**", "/signUpStaff").access("hasRole('STAFF')")
        .antMatchers("/db**", "/db/**").access("hasRole('ADMIN')");
  
  http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
  
        // disable page caching
        http.headers().cacheControl();
 }
}

We first need to configure our UserDetailsService to our AuthenticationManager. The we also need to add our JwtTokenAuthenticationFilter to our filter chain inside the configure() function. In configure() we also make sure our server is stateless by using SessionCreationPolicy.STATELESS and disable CSRF since Jwt tokens are immune to that. Also we need add permission to our URLs to restrict them to permitted users.

If user is unauthorized, we need to send back a 401 response, I did this by registering an AuthenticationEntryPoint of the following, so all 401 request will be handled by this class.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {

 @Override
 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
   throws IOException, ServletException {
  response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
 }

}

So that's all the additional setup we need for token based authentication in Spring, every URL request which requires authentication we be filtered by our JwtTokenAuthenticationFilter.

Of course we need database support for the User models, there's no magic to it, so I will skip that part.

For logging, we need a controller to handle the user credential sent from client and generate valid token and return it back to client. Here's the very straightforward example:


1
2
3
4
5
6
7
 @RequestMapping(value="/login", method=RequestMethod.POST)
 public ResponseEntity<TokenResponse> authenticateUser(@RequestBody TokenRequest tokenRequest) 
   throws AuthenticationException {
  String token = authenticateAndSignToken(tokenRequest.getUsername(), tokenRequest.getPassword());
  subscriberDao.update(tokenRequest.getUsername(), "lastLogin", TimeString.now());
  return new ResponseEntity<TokenResponse>(new TokenResponse(token), HttpStatus.OK);
 }

All the Jwt token related functions are packaged in the ITokenStrategy classes, and here's some code snippets for those functions (full code is available on Github repo of this project).

Token creation and signing:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 public String createToken(final UserDetails user) {
  Map<String, Object> claims = new HashMap<>();
  claims.put(claimUseranme, user.getUsername());
  claims.put(claimAdmin, user.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_" + UserRole.ADMIN.getRole())));
  claims.put(claimCreated, new Date(System.currentTimeMillis()));
  return generateToken(claims);
 }
 
    protected String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secretKey)
                .compact();
    }


Token parsing:


1
2
3
4
5
6
7
8
 public String getTokenFromRequest(HttpServletRequest request) {
  return request.getHeader(headerKey);
 }
 
 public Claims parseToken(final String tokenStr) 
   throws ExpiredJwtException, MalformedJwtException, SignatureException {
  return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(tokenStr).getBody();
 }






No comments:

Post a Comment