Secure your Spring Boot API with JSON Web Tokens

Secure your Spring Boot API with JSON Web Tokens

Overview

If you are reading this article I assume you are a bit familiar with Spring Boot and building API using it. Because the main purpose of this article is to show you a simple way how to make your API more secured.

But before we get started I want to be sure that all of you have some basic knowledge of JSON Web Tokens (JWT). So here is the link to the official website where you can dive deeper into JWT theory.

The official definition of JWT sounds like this:

From history

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.

Sources

Let’s proceed to a practice part. I’ve created a new maven project and my pom.xml file contains the following dependencies:

        11
    

    
        org.springframework.boot
        spring-boot-starter-parent
        2.7.17
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-data-mongodb
        
        
            io.jsonwebtoken
            jjwt-api
            0.12.3
        
        
            io.jsonwebtoken
            jjwt-impl
            0.12.3
        
        
            io.jsonwebtoken
            jjwt-jackson
            0.12.3
        
        
            org.projectlombok
            lombok
            1.18.30
            provided
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.15.3
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            

        
        

As you noticed we will be using MongoDB as database; jackson-databing for binding request and response data into JSON format; lombok for generating getters, setters etc., and a few dependencies from io.jsonwebtoken group to work with JWT creation and decoding.

Let’s create application.yml in the resources folder and add the following content:

spring:
  data:
    mongodb:
      database: springjwt
      host: localhost
      port: 27017
      repositories:
        enabled: true
 

Next we need to create some default package, I will call it com.jwt.example and add the main class which will be responsible for starting our Spring Boot application. I will call it Application, but you can give it any name you like.

@SpringBootApplication
public class Application {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
}

If you did everything right then our application should be started successfully. But it does not do anything yet, so let’s continue and add some more packages into our default package: configuration, controller, model, repository, service.

In the model package add simple User class with following content:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    
    private ObjectId id;
    private String name;
    private String email;
    private String password;
}

All annotations that you can see are added from the lombok package that was mentioned before. Users need to be saved and fetched from the database, so let’s add a UserRepository interface which extends MongoRepository. We will not add any methods to this interface because MongoRepository already has built-in CRUD operations.

@Repository
public interface UserRepository extends MongoRepository<User, ObjectId> {
}

Next thing we need to do is to add UserService class with two methods saveUser and getUser.

@Service
public class UserService {

     private UserRepository userRepository;
     private TokenService tokenService;

     @Autowired
     UserService(UserRepository userRepository, TokenService tokenService) {
          this.userRepository = userRepository;
          this.tokenService = tokenService;
     }

     public User getUser(ObjectId userId) {
          return userRepository.findById(userId).orElse(null);
     }

     public String saveUser(User user) {
          User savedUser = userRepository.save(user);
          return tokenService.createToken(savedUser.getId());
     }
}

These two methods are pretty simple. GetUser() just finds users in the database using injected userRepository class. SaveUser() saves the user to the database. NOTE: saving user’s password without hashing it is a bad practice, I just skipped it because it’s not a scope of this topic. After saving a user we create a token by calling tokenSerice.createToken() method. TokenService is not in our project yet, so I will add it into service package:

@Service
public class TokenService {

    public static final String SECRET_KEY = "MY_SECRET_KEY_1234556789_SHOULD_BE_LONG_ENOUGH";
    private static final long EXPIRATION_TIME = 3600000; // 1 hour in milliseconds

    public String createToken(ObjectId userId) {
        return Jwts.builder()
                .claim("userId", userId.toHexString())
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(getSigningKey())
                .compact();
    }

    public String getUserIdFromToken(String token) {
        return (String) extractClaims(token).getPayload().get("userId");
    }

    public boolean isTokenValid(String token) {
        String userId = this.getUserIdFromToken(token);
        return userId != null && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractClaims(token).getPayload().getExpiration().before(new Date());
    }

    private Jws extractClaims(String token) {
        return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token);
    }

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
    }

}

TokenService is a class where all stuff related to JWT is happening. Let me explain some parts of this code.

Get a line on how to increase app performance with Hazelcast – our article is ready for reading.

SECRET_KEY is a random generated string, you can use any string you like.

EXPIRATION_TIME is a time in milliseconds how long our token will live. In our case it’s 1 hour. createToken() is the main method which generates our token signed with a secret key. As you can see I added userId into the claim. You can add any info into the token and this data can be fetched from the token after decoding it. We have also added issuedAt date to the token which indicates the time when the token was generated and also expiration time which is the current time + 1 hour.

isTokenValid() method just returns true or false if the token is valid or not. It calls getUserIdFromToken() method which performs decoding of the token. If userId is available in the token and the token is not expired then we assume that it’s valid.

We are almost done. One thing left to do is to tell our Spring Application which routes (request mappings) should be used with a token(secured routes) and which should be available for all users(public routes). To do this we need to create a JWTFilter class that extends the GenericFilterBean class and override method doFilter(). Also we need to add @Configuration annotation to this class. Let’s look into the code and I will explain what is going on here.

@Configuration
public class JWTFilter extends GenericFilterBean {

    private final TokenService tokenService;

    JWTFilter() {
        this.tokenService = new TokenService();
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        String token = request.getHeader("Authorization");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.sendError(HttpServletResponse.SC_OK, "success");
            return;
        }

        if (allowRequestWithoutToken(request)) {
            response.setStatus(HttpServletResponse.SC_OK);
            filterChain.doFilter(req, res);
        } else {
            if (token == null || !tokenService.isTokenValid(token)) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            } else {
                ObjectId userId = new ObjectId(tokenService.getUserIdFromToken(token));
                request.setAttribute("userId", userId);
                filterChain.doFilter(req, res);

            }
        }
    }

    public boolean allowRequestWithoutToken(HttpServletRequest request) {
        return request.getRequestURI().contains("/register");
    }

}

Let’s go step by step through the doFilter() method. First of all we are casting request and response to HttpServletRequest and HttpServletResponse to have access to some http methods. Then we get a token from the “Authorization” header. As you may guess we will pass our token as Authorization header with each request that requires a token. After that we check if the request method is “OPTIONS” and if so we send a success response. This is needed because before sending a request to some endpoint some browsers send OPTIONS requests to ensure that the request being done is trusted by the server.

By default all requests will be checked for Authorization header, so if we want to skip some routes we should explicitly declare these routes. For this purpose we have added a method allowRequestWithoutToken() where we added “/register” request mapping. So if we’re sending a ‘register’ new user request then the method filterChain.doFilter(req, res) will be called. This method is proceeding to the next filter and the last filter in the chain will be our destination request mapping. As we don’t have any other filter then the doFilter() method will directly go to our controller which we will add in a minute.

Next step in our doFilter method is checking if the token is null or invalid. If so, then Unauthorized response will be sent. If token is valid we’re retrieving userId from token and adding it as an Attribute to the request object. After this we can get a userId from an attribute in any method in our controller.

Last step is adding our UserController class

@RestController
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @Autowired
    UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/register")
    public String registerUser(@RequestBody User user) {
        return userService.saveUser(user);
    }

    @GetMapping("/get")
    public User getUser(@RequestAttribute(value = "userId") ObjectId userId) {
        return userService.getUser(userId);
    }
    
}

Nothing special here, just calling userService methods, except getting userId from request attribute in getUser()method.

Congratulations! We are done. It’s time to test our API. Restart your Application and open Postman to send some request. Let’s create new user by sending POST request to https://localhost:8080/user/register with the following request body:

{
“name”: “Ihor Sokolyk”,
“email”: “ihor@gmail.com”,
“password”: “password”
}
 

We should get a token in the response body:

“eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjcmVhdGVkQXQiOjE1MTA0OT
gwNDIsInVzZXJJZCI6IjVhMDg1ZWY5OTZlZTE3MWE4NDcwMmU1NiJ9.Q_kKBHy5A-pKp-NjaottM6QybwnTZ4QD2XBzOdDSVcs”

Now copy this token and let’s try to get just created user by making GET request to https://localhost:8080/user/get, but do not forget to add our token as Authorization header:

Authorization header image

If you don’t pass token or remove some character from it then you should get 401 Unauthorized error:

Unauthorized error image

Conclusion

Conclusion. Now you know how to use JWT tokens and how to secure your API. But you can play around with it and make it more complicated, for example save all tokens to the database and implement logging from different devices’ functionality.

Thank you for reading this article. If you have any questions or notes please feel free to leave a comment. You can check all sources on Oril Software GitHub.

#API

#Java

#Security

#SpringBoot

Spring Boot with Liquibase Migrations

Liquibase is a DB migration and change operation tool developed specifically to help software developers change their databases and move seamlessly through the development and testing stages. The role of this tool is to hold all the changes made to a particular database in a single file named changelog. It also allows for loading information […]

Ihor Kosandyak Avatar
Ihor Kosandyak

31 Jul, 2023 · 5 min read

Spring Cloud Gateway security with JWT

There is a clear understanding that everything that is exposed to the Internet should be secured. Especially when you create software and work with sensitive user data, such as emails, phone numbers, addresses, credit cards, etc. Here we will go through securing API Gateway with Json Web Tokens(JWT). As far as you probably know Spring […]

Ihor Kosandyak Avatar
Ihor Kosandyak

26 Feb, 2021 · 4 min read

Uploading files to AWS S3 Bucket using Spring Boot

Intro Hi guys! Today we are going to talk about uploading files to Amazon S3 Bucket from your Spring Boot application. As you may notice almost each application, mobile or web, gives users an ability to upload their images, photos, avatars etc. So you, as a developer, should choose the best way how to save and where to store […]

Ihor Sokolyk Avatar
Ihor Sokolyk

3 Dec, 2017 · 6 min read