Json Web Token: How to Secure a Spring Boot REST API

In this post, I will show how to secure your spring boot based REST API. It has been more of a trend to secure REST APIs to avoid any unnecessary calls to public APIs. We will be using some Spring Boot features for Spring Security, along with JSON WebTokens for authorization. 

User flow in this case is

  1. User logs in.
  2. We validate user credentials.
  3. A token is sent back to user agent.
  4. User tries to access a protected resource.
  5. User sends JWT when accessing the protected resource. We validate JWT.
  6. If JWT is valid, we allow the user to access the resource.

JSON Web Tokens, known as JWTs are used for forming authorization for users. This helps us to build secure APIs and it is also easy to scale. During authentication, a JWT is returned. Whenever the user wants to access a protected resource, the browser must send JWTs in the Authorization header along with the request. One thing to understand here is that it is a good security practice to secure REST API. 

Basically, we will show how to:

  1. Verify JSON WebToken
  2. Validate the signature
  3. Check the client permissions

What You Will Need?

  1. Java 8.
  2. MySQL Database.
  3. IntelliJ Editor.
  4. Gradle.

Note – This won’t be a full-fledged app, but REST APIs based on Spring boot, Spring security.

Spring Boot Based REST API

I will be securing REST API for company that I created in this blog post on REST APIs. This API also includes caching. A user will try to access /cachedemo/v1/companies/ and since APIs are protected, the user will get a response like below:

Response from protected API

Now we will implement how to protect this API and how to access it when it is protected. 

Adding User and User Registration

Since we want to add authorization for APIs, we will need to know where the user is able to log in and send credentials. These credentials will be validated, and a token will be generated. This token then will be transmitted in a request to an API call. The token will be validated in the Spring Security authorization filter that we will add. If the token is valid, the user will be able to access the API.

Create a User Model

Java
 




x
54


1
package com.betterjavacode.models;
2
 
          
3
import javax.persistence.*;
4
import java.io.Serializable;
5
 
          
6
@Entity(name = "User")
7
@Table(name = "user")
8
public class User implements Serializable
9
{
10
    public User()
11
    {
12
 
          
13
    }
14
 
          
15
    @Id
16
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
17
    private long id;
18
 
          
19
    @Column(name = "username")
20
    private String username;
21
 
          
22
    @Column(name = "password")
23
    private String password;
24
 
          
25
    public long getId()
26
    {
27
        return id;
28
    }
29
 
          
30
    public void setId(long id)
31
    {
32
        this.id = id;
33
    }
34
 
          
35
    public String getUsername()
36
    {
37
        return username;
38
    }
39
 
          
40
    public void setUsername(String username)
41
    {
42
        this.username = username;
43
    }
44
 
          
45
    public String getPassword()
46
    {
47
        return password;
48
    }
49
 
          
50
    public void setPassword(String password)
51
    {
52
        this.password = password;
53
    }
54
}



We will add a controller where a user can register with its details for username and password

Java
 




xxxxxxxxxx
1
31


 
1
package com.betterjavacode.resources;
2
 
          
3
import com.betterjavacode.models.User;
4
import com.betterjavacode.repositories.UserRepository;
5
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6
import org.springframework.web.bind.annotation.PostMapping;
7
import org.springframework.web.bind.annotation.RequestBody;
8
import org.springframework.web.bind.annotation.RequestMapping;
9
import org.springframework.web.bind.annotation.RestController;
10
 
          
11
@RestController
12
@RequestMapping(value = "/cachedemo/v1/users")
13
public class UserController
14
{
15
    private UserRepository userRepository;
16
    private BCryptPasswordEncoder bCryptPasswordEncoder;
17
 
          
18
    public UserController(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder)
19
    {
20
        this.userRepository = userRepository;
21
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
22
    }
23
 
          
24
    @PostMapping("/signup")
25
    public void signUp(@RequestBody User user)
26
    {
27
        user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
28
        userRepository.save(user);
29
    }
30
 
          
31
}



Now, when we POST a request to /cachedemo/v1/users/signup is sent, a user will be saved in the database. Passwords for the user will be saved in an encrypted format, as we are using BCryptPasswordEncoder. Now, we will show how a user can log in to create a token.

User Login

To handle user login, we will add an AuthenticationFilter, which will get added in FilterChain, and Spring boot will handle the execution of it appropriately. This filter will look like the following:

Java
 




xxxxxxxxxx
1
54


1
package com.betterjavacode.SpringAppCache;
2
 
          
3
 
          
4
import com.fasterxml.jackson.databind.ObjectMapper;
5
import io.jsonwebtoken.Jwts;
6
import io.jsonwebtoken.SignatureAlgorithm;
7
import org.springframework.security.authentication.AuthenticationManager;
8
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
9
import org.springframework.security.core.Authentication;
10
import org.springframework.security.core.AuthenticationException;
11
import org.springframework.security.core.userdetails.User;
12
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
13
 
          
14
import javax.servlet.FilterChain;
15
import javax.servlet.http.HttpServletRequest;
16
import javax.servlet.http.HttpServletResponse;
17
import java.io.IOException;
18
import java.util.ArrayList;
19
import java.util.Date;
20
 
          
21
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter
22
{
23
    private AuthenticationManager authenticationManager;
24
 
          
25
    public AuthenticationFilter(AuthenticationManager authenticationManager)
26
    {
27
        this.authenticationManager = authenticationManager;
28
        setFilterProcessesUrl("/login");
29
    }
30
 
          
31
    @Override
32
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException
33
    {
34
        try
35
        {
36
            com.betterjavacode.models.User creds = new ObjectMapper().readValue(request.getInputStream(), com.betterjavacode .models.User.class);
37
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword(),new ArrayList<>()));
38
        }
39
        catch(IOException e)
40
        {
41
            throw new RuntimeException("Could not read request" + e);
42
        }
43
    }
44
 
          
45
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authentication)
46
    {
47
        String token = Jwts.builder()
48
                .setSubject(((User) authentication.getPrincipal()).getUsername())
49
                .setExpiration(new Date(System.currentTimeMillis() + 864_000_000))
50
                .signWith(SignatureAlgorithm.HS512, "SecretKeyToGenJWTs".getBytes())
51
                .compact();
52
        response.addHeader("Authorization","Bearer " + token);
53
    }
54
}



Basically, a user will send credentials in a request to the URL ending with /login . This filter will help to authenticate the user. If there is successful authentication, a token will be added in response header with the key Authorization.

Token Validation and Authorization

We add another filter AuthorizationFilter to validate the token that we passed through AuthenticationFilter earlier. This filter will look like below:

Java
 




xxxxxxxxxx
1
56


 
1
package com.betterjavacode.SpringAppCache;
2
 
          
3
import io.jsonwebtoken.Jwts;
4
import org.springframework.security.authentication.AuthenticationManager;
5
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
6
import org.springframework.security.core.context.SecurityContextHolder;
7
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
8
 
          
9
import javax.servlet.FilterChain;
10
import javax.servlet.ServletException;
11
import javax.servlet.http.HttpServletRequest;
12
import javax.servlet.http.HttpServletResponse;
13
import java.io.IOException;
14
import java.util.ArrayList;
15
 
          
16
 
          
17
public class AuthorizationFilter extends BasicAuthenticationFilter
18
{
19
    public AuthorizationFilter(AuthenticationManager authenticationManager)
20
    {
21
        super(authenticationManager);
22
    }
23
 
          
24
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
25
            throws IOException, ServletException
26
    {
27
        String header = request.getHeader("Authorization");
28
        if(header == null || !header.startsWith("Bearer"))
29
        {
30
            filterChain.doFilter(request,response);
31
            return;
32
        }
33
 
          
34
        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
35
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
36
        filterChain.doFilter(request,response);
37
    }
38
 
          
39
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request)
40
    {
41
        String token = request.getHeader("Authorization");
42
        if(token != null)
43
        {
44
            String user = Jwts.parser().setSigningKey("SecretKeyToGenJWTs".getBytes())
45
                    .parseClaimsJws(token.replace("Bearer",""))
46
                    .getBody()
47
                    .getSubject();
48
            if(user != null)
49
            {
50
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
51
            }
52
            return null;
53
        }
54
        return null;
55
    }
56
}



If the validation of the token is successful, a user is returned and assigned to a security context.

To enable Spring security, we will add a new class WebSecurityConfiguration with the annotation, @EnableWebSecurity. This class will extend the standard WebSecurityConfigurerAdapter. In this class, we will restrict our APIs and also add some whitelisted URLs that we will need access without any authorization token. This will look like below:

Java
 




xxxxxxxxxx
1
63


1
package com.betterjavacode.SpringAppCache;
2
 
          
3
import org.springframework.context.annotation.Bean;
4
import org.springframework.http.HttpMethod;
5
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
6
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
7
import org.springframework.security.config.annotation.web.builders.WebSecurity;
8
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
10
import org.springframework.security.config.http.SessionCreationPolicy;
11
import org.springframework.security.core.userdetails.UserDetailsService;
12
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
13
import org.springframework.web.cors.CorsConfiguration;
14
import org.springframework.web.cors.CorsConfigurationSource;
15
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
16
 
          
17
@EnableWebSecurity
18
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter
19
{
20
    private BCryptPasswordEncoder bCryptPasswordEncoder;
21
    private UserDetailsService userDetailsService;
22
 
          
23
    private static final String[] AUTH_WHITELIST = {
24
            "/v2/api-docs",
25
            "/swagger-resources",
26
            "/swagger-resources/**",
27
            "/configuration/ui",
28
            "/configuration/security",
29
            "/swagger-ui.html",
30
            "/webjars/**"
31
    };
32
 
          
33
    public WebSecurityConfiguration(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder)
34
    {
35
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
36
        this.userDetailsService = userDetailsService;
37
    }
38
 
          
39
 
          
40
    protected void configure(HttpSecurity httpSecurity) throws Exception
41
    {
42
        httpSecurity.cors().and().csrf().disable().authorizeRequests()
43
                .antMatchers(AUTH_WHITELIST).permitAll()
44
                .antMatchers(HttpMethod.POST, "/cachedemo/v1/users/signup").permitAll()
45
                .anyRequest().authenticated()
46
                .and().addFilter(new AuthenticationFilter(authenticationManager()))
47
                .addFilter(new AuthorizationFilter(authenticationManager()))
48
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
49
    }
50
 
          
51
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception
52
    {
53
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
54
    }
55
 
          
56
    @Bean
57
    CorsConfigurationSource corsConfigurationSource()
58
    {
59
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
60
        source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues());
61
        return source;
62
    }
63
}



In the method, configure, we have restricted most APIs, only allowing Swagger URLs and signup URL. We also add filters to HttpSecurity. We will add our own UserDetailsServiceImpl class to validate user credentials. 

Java
 




xxxxxxxxxx
1
32


 
1
package com.betterjavacode.services;
2
 
          
3
import com.betterjavacode.models.User;
4
import com.betterjavacode.repositories.UserRepository;
5
import org.springframework.security.core.userdetails.UserDetails;
6
import org.springframework.security.core.userdetails.UserDetailsService;
7
import org.springframework.security.core.userdetails.UsernameNotFoundException;
8
import org.springframework.stereotype.Component;
9
 
          
10
import java.util.Collections;
11
 
          
12
@Component
13
public class UserDetailsServiceImpl implements UserDetailsService
14
{
15
    private UserRepository userRepository;
16
 
          
17
    public UserDetailsServiceImpl(UserRepository userRepository)
18
    {
19
        this.userRepository = userRepository;
20
    }
21
 
          
22
    @Override
23
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
24
    {
25
        User user = userRepository.findByUsername(username);
26
        if(user == null)
27
        {
28
            throw new UsernameNotFoundException(username);
29
        }
30
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.emptyList());
31
    }
32
}



Demo

With the all the code changes, now we are ready to create a user, login and access secured REST APIs. From the image above, a user gets Access Denied error for accessing secured APIs. To demo this, I have already registered a user with username test1 and password test@123.

Securing REST API

This POST request will give us Authorization token in response as shown above. Now using this token in our GET request to retrieve companies data. This GET request will look like below: 

Securing REST API

In this way, we showed how to secure REST API using JSON web token. 

 

 

 

 

Top