springboot:spring_simple_jwt_authentication

Spring Simple JWT Authentication

  1. Add dependencies in build.gradle, we use auth0 library here. (compile “com.auth0:java-jwt:3.4.0”)
  2. Create our Authentication Token (JwtAuthToken extends AbstractAuthenticationToken)
  3. Create our Authentication Filter (JwtAuthFilter extends AbstractAuthenticationProcessingFilter)
  4. Create our Authentication Provider (JwtAuthenticationProvider implements AuthenticationProvider)
  5. Config WebMvcConfig extends WebSecurityConfigurerAdapter, override protected void configure(AuthenticationManagerBuilder auth) throws Exception {…}, and protected void configure(HttpSecurity http) throws Exception {…} to add our Authentication Provider , and Authentication Filter in spring authentication chain.

Just add compile “com.auth0:java-jwt:3.4.0” in the dependencies section. There are other library like compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1', but in this example we use the one from Auth0.

dependencies {
    ...
    compile "com.auth0:java-jwt:3.4.0"

Here is out token, which store username, the type of token isAccessToke (Access/Refresh), the token expire date time expireDate, and the GrantedAuthority in the parent class.

public class JwtAuthToken extends AbstractAuthenticationToken {
    private String username;
    private Date expireDate;
    private boolean isAccessToke;

    public JwtAuthToken(String username, Date expireDate, boolean isAccessToke, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.username = username;
        this.expireDate = expireDate;
        this.isAccessToke = isAccessToke;
    }
    @Override
    public Object getCredentials() {
        return null;
    }
    @Override
    public Object getPrincipal() { return this.username; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public Date getExpireDate() { return expireDate; }
    public void setExpireDate(Date expireDate) { this.expireDate = expireDate; }
    public boolean isAccessToke() { return isAccessToke; }
    public void setAccessToke(boolean accessToke) { isAccessToke = accessToke; }
}

To make the code look better, we first setup some constants in a class.

public class SecurityConstants {
    public static final String SECRET = "SecretKeyToGenJWTs";
    public static final long ACCESS_TOKEN_EXPIRATION_TIME = 900 * 1000; // 15-min
    public static final long REFRESH_TOKEN_EXPIRATION_TIME = 7200 * 1000; // 2-hrs
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String ACCESS_HEADER_STRING = "token";
    public static final String REFRESH_HEADER_STRING = "token-refresh";
    public static final String ROLE = "ROLE";
}

When a request come, if the URL matches the given pattern, this filter's attemptAuthentication(…) will be called. It should prepare the authentication token, and call the authentication manager with the token to attempt the authentication. It can return a fully authenticated token if success, null if the filter do not know what to do and let the next filter to figure out, or throws an AuthenticationException (classes that extends it) on error.

On successfully authentication, the call-back method successfulAuthentication(…){} will be called, or if authentication failed, unsuccessfulAuthentication(…){} will be called if a type of AuthenticationException has been throw.

After successfully authenticated, we need to setup the security context holder by putting a security context into it. This part is simple, because the call-back has already provided us the authenticated token. For JWT auth, we need to do it basically on every request.

   SecurityContext context = SecurityContextHolder.createEmptyContext();
   context.setAuthentication(authResult);
   SecurityContextHolder.setContext(context);

You might also see that we put new access, and refresh token on the response header on receiving JWT refresh token. Normally, the front-end will use the access token to access our site, but it will expire. When it expired, the back-end will give the front-end an 401 error, the front-end should be smart enough to retry the connection with the refresh token. Upon receiving the responses with the access, and refresh tokens, the front-end should update its values (by using interceptor in Angular, for example).

Full code:

public class JwtAuthFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthFilter(RequestMatcher matcher,  AuthenticationManager authenticationManager) {
        super(matcher);
        this.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        JwtAuthToken jwtAuthToken = null;
        try {
            String token = request.getHeader(SecurityConstants.ACCESS_HEADER_STRING);
            if (token != null && token.startsWith(SecurityConstants.TOKEN_PREFIX)) {
                jwtAuthToken = getJwtAuthToken(token, true);
            } else {
                String refreshToken = request.getHeader(SecurityConstants.REFRESH_HEADER_STRING);
                if (refreshToken != null && refreshToken.startsWith(SecurityConstants.TOKEN_PREFIX)) {
                    jwtAuthToken = getJwtAuthToken(refreshToken, false);
                }
            }
            if (jwtAuthToken != null)
                return getAuthenticationManager().authenticate(jwtAuthToken);
        } catch (Exception e) {
            throw new BadCredentialsException("Error on jwt attemptAuthentication");
        }
        return null;
    }

    private JwtAuthToken getJwtAuthToken(String token, boolean isAccessToken) {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes())).build();

        DecodedJWT decodedJWT = jwtVerifier.verify(token.replace(SecurityConstants.TOKEN_PREFIX, ""));
        String user = decodedJWT.getSubject();
        Date expireDate = decodedJWT.getExpiresAt();
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        grantedAuthorities.add(new SimpleGrantedAuthority(decodedJWT.getClaim(SecurityConstants.ROLE).asString()));

        if (user != null) {
            JwtAuthToken unAuthToken = new JwtAuthToken(user, expireDate, isAccessToken, grantedAuthorities);
            return unAuthToken;
        }
        return null;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authResult);
        SecurityContextHolder.setContext(context);
        JwtAuthToken jwtAuthToken = (JwtAuthToken) authResult;
        if (!jwtAuthToken.isAccessToke()) {
            String role = jwtAuthToken.getAuthorities().iterator().next().getAuthority();
            String token = JWT.create()
                    .withSubject(jwtAuthToken.getUsername())
                    .withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.ACCESS_TOKEN_EXPIRATION_TIME))
                    .withClaim(SecurityConstants.ROLE, role)
                    .sign(HMAC512(SecurityConstants.SECRET.getBytes()));

            String refreshToken = JWT.create()
                    .withSubject(jwtAuthToken.getUsername())
                    .withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.REFRESH_TOKEN_EXPIRATION_TIME))
                    .withClaim(SecurityConstants.ROLE, role)
                    .sign(HMAC512(SecurityConstants.SECRET.getBytes()));
            response.addHeader(SecurityConstants.ACCESS_HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
            response.addHeader(SecurityConstants.REFRESH_HEADER_STRING, SecurityConstants.TOKEN_PREFIX + refreshToken);
        }
        chain.doFilter(request, response);
    }
}

Here we extends AuthenticationProvider. Tell it we support JwtAuthToken as Authentication token, and do the auth here. In our example, we actually has done the authentication in the filter. If you would like, you can move that part into the public Authentication authenticate(Authentication authentication) throws AuthenticationException {…} method here.

The better way to do it is to create two type of tokens. One call RawJwtToken, and one call JwtToken, and put the create the raw one in the filter, and the real one in the authentication provider, and do the token conversion in the authentication provider. We are a bit lazy here without doing it.

Full code:

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtAuthToken jwtAuthToken = (JwtAuthToken) authentication;
        Date now = new Date();
        if (jwtAuthToken.getExpireDate().before(now)){
            throw new CredentialsExpiredException("Token Expired");
        }
        JwtAuthToken authenticatedToken =  new JwtAuthToken(jwtAuthToken.getUsername(), jwtAuthToken.getExpireDate(), jwtAuthToken.isAccessToke(), jwtAuthToken.getAuthorities());
        authenticatedToken.setAuthenticated(true);
        return authenticatedToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return JwtAuthToken.class.isAssignableFrom(authentication);
    }
}

We need to config the spring security as usual. The following two lines are the key points for JWT config. For the jwt auth filter, we need to tell the filter which paths need to be authenticated, and which paths need to be skipped. Here, /login, and /error are skipped, and everything else need to be authenticated.

auth.authenticationProvider(jwtAuthenticationProvider);
addFilterBefore(new JwtAuthFilter(new SkipPathRequestMatcher(jwtSkipPaths, "/**"), this.authenticationManager()), UsernamePasswordAuthenticationFilter.class)

The full config:

@Configuration
public class WebMvcConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsServiceImpl userDetailsService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    private JwtAuthenticationProvider jwtAuthenticationProvider;

    public WebMvcConfig(UserDetailsServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
        auth.authenticationProvider(jwtAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> jwtSkipPaths = new ArrayList<>();
        jwtSkipPaths.add("/login");
        jwtSkipPaths.add("/error");

        http.cors().and().csrf().disable().authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .addFilterBefore(new AjaxAuthFilter(this.authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtAuthFilter(new SkipPathRequestMatcher(jwtSkipPaths, "/**"), this.authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
  1. Get the token by using other login method, such as Ajax auth.
  2. Use the access token
  3. Use the refresh token if the access token expired.
(base) D:\>curl --noproxy "*" -v  http://127.0.0.1:8080/login  -d "username=user&password=password"
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /login HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.0
> Accept: */*
> Content-Length: 31
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 31 out of 31 bytes
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyNzQ0MDZ9.eD5JKRveeyQAn03EikZurdlGfn35Op--F7Czw6qcN8FvHv80qRUQZoE_JUdBqJrXzrgx2LU54eZZ-2o5S9DGpQ
< token-refresh: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyODA3MDZ9.I43CCtTTmjvqakUollzaRf0b5DCHt5LlJ-if_0Ci4JXHWJEQ-L_TyJ1pqZHTotKFXk3OMn3NZnY9AQUuyQTgkQ
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Tue, 16 Jun 2020 02:11:47 GMT
<
* Connection #0 to host 127.0.0.1 left intact
(base) D:\>curl --noproxy "*" -v  http://127.0.0.1:8080/hello  -H "token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyNzQ0ODV9.a-Qb1Put98e9rQw1E_Am0ybJkrH1BYgX4MuiFam8CoLZej_1RWQhWpfmXdp4WnIprW9GFUOPmm3S6BHJwZQc9A"
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.0
> Accept: */*
> token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyNzQ0ODV9.a-Qb1Put98e9rQw1E_Am0ybJkrH1BYgX4MuiFam8CoLZej_1RWQhWpfmXdp4WnIprW9GFUOPmm3S6BHJwZQc9A
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 16 Jun 2020 02:13:33 GMT
<
{"response":"Hello"}* Connection #0 to host 127.0.0.1 left intact
(base) D:\>curl --noproxy "*" -v  http://127.0.0.1:8080/hello -H "token-refresh: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyODA3ODV9.uGdHsQVLDJaakJLru6wAIVw74ymYaULnVtTerfg_FSquJZoY0-71tkcy4gYGogoR6CUM5K1IxrZ-Jjme0KZ36Q"
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.0
> Accept: */*
> token-refresh: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyODA3ODV9.uGdHsQVLDJaakJLru6wAIVw74ymYaULnVtTerfg_FSquJZoY0-71tkcy4gYGogoR6CUM5K1IxrZ-Jjme0KZ36Q
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyNzQ1ODd9._laxbwGZkLl5RE4EZ1eJOs8d0dB3ci0uhU1aL8-2sSL6fdQdMFvk4qAvwfSlrhdQXG3TTMoW74K7d8AsJY35GA
< token-refresh: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTIyODA4ODd9.BcF13z9OZ1O1VnzXuJi3M95MfFGcczP2n4n7sjtek1H0EaU5ifowODeqXAQhoeVG4MtANzsNm9WW9z0_CS6fwA
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 16 Jun 2020 02:14:47 GMT
<
{"response":"Hello"}* Connection #0 to host 127.0.0.1 left intact
  • springboot/spring_simple_jwt_authentication.txt
  • Last modified: 2020/06/16 10:16
  • by chongtin