In this article we will see how to integrate a simple REST API authentication using JSON Web Token (JWT) standard and Spring Security into an existing e-commerce Spring Boot REST API application. This article is not meant to explain the JWT standard so I encourage you to read more about it first.

I will use a real world example unlike many dummy tutorials on the internet with useless code. As the base, I’ll use my private project which you can find on GitHub. I will not paste entire code here so you may need to check the source to understand the context. However I will try to include as much info as possible while keeping it clean.

This article is inspired by springboot-jwt-starter GitHub project.

So, in order to understand this article you should have a basic knowledge in following:

Java

Spring Framework (I use Spring Boot 1.5.2)

Spring Framework Security

JWT standard

These are steps we will go throughout to implement JWT into Spring Framework:

Create Spring Security package Setup login "controller" Protect endpoints

1. Create Spring Security Package

Spring allows us to filter all requests that will be made to our app.

WebSecurityConfig and AuthenticationFilter

In my example I’ve extended org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter so the code looks like this:

... ... public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public TokenAuthenticationFilter jwtAuthenticationTokenFilter() throws Exception { return new TokenAuthenticationFilter (); } protected void configure( HttpSecurity http) throws Exception { http.addFilterBefore(jwtAuthenticationTokenFilter(), BasicAuthenticationFilter . class ) ... ...

Now lets create the filter in TokenAuthenticationFilter.java file:

public class TokenAuthenticationFilter extends OncePerRequestFilter { ( "${jwt.header}" ) private String AUTH_HEADER ; ... ... private String getToken( HttpServletRequest request ) { String authHeader = request.getHeader( AUTH_HEADER ); if ( authHeader != null && authHeader.startsWith( "Bearer " )){ return authHeader.substring( 7 ); } return null ; } public void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException , ServletException { String error = "" ; String authToken = getToken( request ); if (authToken != null ) { String username = tokenHelper.getUsernameFromToken( authToken ); if ( username != null ) { UserDetails userDetails = userDetailServiceImpl.loadUserByUsername( username ); TokenBasedAuthentication authentication = new TokenBasedAuthentication ( userDetails ); authentication.setToken( authToken ); SecurityContextHolder .getContext().setAuthentication( authentication ); } else { error = "Username from token can't be found in DB." ; } } else { error = "Authentication failed - no Bearer token provided." ; } if ( ! error.equals( "" )){ System .out.println(error); SecurityContextHolder .getContext().setAuthentication( new AnonAuthentication () ); } chain.doFilter(request, response); }

So let’s check key points here.

String authToken = getToken( request ); String username = tokenHelper.getUsernameFromToken( authToken );

We will see tokenHelper later, but essentially this is all verification we need. As you will see this code checks if token is expired and if the signature is correct. So if something is wrong, username (or authToken) will be null.

Token is valid

Now that we verified JWT, we can set an authentication object in SecurityContextHolder. This is Spring Security’s way of checking authentication. It works like this:

TokenBasedAuthentication authentication = new TokenBasedAuthentication( userDetails ); authentication.setToken( authToken ); SecurityContextHolder.getContext().setAuthentication(authentication);

TokenBasedAuthentication is our custom made class that extends org.springframework.security.authentication.AbstractAuthenticationToken abstract class. In this class we‘ll define authorities from the userDetails object and these authorities we will check later in WebSecurityConfig.

TokenBasedAuthentication.java:

public class TokenBasedAuthentication extends AbstractAuthenticationToken { private String token; private final UserDetails principle; public TokenBasedAuthentication ( UserDetails principle ) { super ( principle.getAuthorities() ); this .principle = principle; } public String getToken () { return token; } public void setToken ( String token ) { this .token = token; } public boolean isAuthenticated () { return true ; } public Object getCredentials () { return token; } public UserDetails getPrincipal () { return principle; } }

The key line here is in the constructor:

super ( principle.getAuthorities() );

Behind the scene, this is Set<org.springframework.security.core.GrantedAuthority>.

After this we have the Authentication object in our “before-filter”. In next phases of the code executions, we can check this object and see what authorities it has.

In my example I have only one role “admin” and I check it in WebSecurityConfig.configure() like this:

http.anyRequest().hasAuthority("admin")

(More details on this configurations in the 3rd section.)

Token is invalid

For invalid tokes (remember TokenAuthenticationFilter above?) we will have another authentication class AnonAuthentication that extends AbstractAuthenticationToken but this one will not have any authorities. So in the AnonAuthentication class, instead of:

super ( principle.getAuthorities() );

we will have:

super ( null );

So in the TokenAuthenticationFilter , if the token is invalid, we put following:

SecurityContextHolder.getContext().setAuthentication( new AnonAuthentication() );

Therefore this object fails mentioned security verification:

http.anyRequest().hasAuthority("admin")

JWT Token Helper

As promised I will show you some key points of this helper. For full source check the project on GitHub.

So we called:

String username = tokenHelper.getUsernameFromToken( authToken );

that is:

public String getUsernameFromToken( String token) { String username; try { final Claims claims = this .getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null ; } return username; }

To get Claims we use io.jsonwebtoken.* package, that is "Java JWT: JSON Web Token for Java and Android" project which you can find on Github.

claims = Jwts.parser() .setSigningKey( this .SECRET) .parseClaimsJws(token) .getBody();

This library does all magic behind to verify signature, token expiration and contents.

UserDetailService

If you check our filter again, you will see next code:

UserDetails userDetails = userDetailServiceImpl.loadUserByUsername( username );

So we need to create this service and "inject" (see dependency injection) it into our filter. The service looks like this:

import org.springframework.security.core.userdetails.UserDetailsService; ... ... public class UserDetailServiceImpl implements UserDetailsService { ... public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { ... ...

I've used JPARepository implementation in the method, but you can implement this using your existing "user-service".

2. Setup login controller

Spring Boot has “POST /login” as default route for user login. We will actually not use any controller here but we will rather implement our org.springframework.security.web.authentication.AuthenticationSuccessHandler by extending org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler.

public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { ( "${jwt.expires_in}" ) private int EXPIRES_IN; TokenHelper tokenHelper; ObjectMapper objectMapper; public void onAuthenticationSuccess ( HttpServletRequest request, HttpServletResponse response, Authentication authentication ) { clearAuthenticationAttributes(request); User user = (User)authentication.getPrincipal(); String jwt = tokenHelper.generateToken( user.getUsername() ); UserTokenState userTokenState = new UserTokenState(jwt, EXPIRES_IN); try { String jwtResponse = objectMapper.writeValueAsString(userTokenState); response.setContentType( "application/json" ); response.getWriter().write(jwtResponse); } catch (Exception e){ System.out.println(e.getMessage()); } } private class UserTokenState { private String jws; private int expires; public UserTokenState (String jws, int expires) { this .jws = jws; this .expires = expires; } ... Getters and setters here... }

And then in WebSecurityConfig:

@ Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; ... ... http ... .and().formLogin() .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler)

This is also part of the Spring Security and in other words we say HttpSecurity object to overwrite default behavior, i.e. showing a login form on failed or redirect on succeed login. Instead we inject our handlers explained above.

3. Protect endpoints

So let me finally show configure() method in my WebSecurityConfig:

protected void configure (HttpSecurity http) throws Exception { http .addFilterBefore(jwtAuthenticationTokenFilter(), BasicAuthenticationFilter.class) .authorizeRequests() .antMatchers( "/product/image/**" ).permitAll() .antMatchers(HttpMethod.GET, "/product/**" ).permitAll() .antMatchers(HttpMethod.GET, "/group/**" ).permitAll() .antMatchers( "/cart/**" ).permitAll() .anyRequest().authenticated() .anyRequest().hasAuthority( "admin" ) .and().formLogin() .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler) .and().csrf().disable(); }

So we have:

addFilterBefore() to filter requests and set Authentication object,

antMatchers() to allow public endpoints,

authenticated() and hasAuthority() means all requests must contain Authentication object containing admin Authority (see above) and

formLogin() where we modify the default login process by adding (login) success and failure handlers.

Conclusion

Spring Security provides a lot of flexibility by simply replacing the default classes with our own. Then we only need to develop JWT management part for which we use helper which uses JsonWebTokens.io package.

That's it! Very simple, right? :)

I am looking forward to read your comments on what's wrong in here and what can be made better.