DEV Community

Zorian
Zorian

Posted on

How to Secure Your Spring Boot API with JSON Web Tokens (JWT)

How to Secure Your Spring Boot API with JSON Web Tokens (JWT)
In this guide, I will show you how to enhance your API's security using JSON Web Tokens (JWT). This is for those who already know Spring Boot and how to build APIs with it. Also, before we start, make sure you have a basic understanding of JWT. You can get a detailed overview on the official website. This should help you understand what we'll discuss in this guide.

What is a JSON Web Token (JWT)?

JSON Web Token (JWT) is a secure way to share information between parties in a small, self-contained format. It's an open standard (RFC 7519) that uses digital signatures to verify that the information is trustworthy. JWTs can be signed using a secret code with HMAC or with RSA's public/private key pairs.

How to Use JWT Tokens and Secure your API

To show you with practical examples, I have created a Maven project with essential dependencies. These include MongoDB for the database, Jackson for JSON data binding, Lombok for auto-generating code like getters and setters, and io.jsonwebtoken for JWT management.

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.8.8</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- Package as an executable jar/war -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

        </plugins>
    </build>
Enter fullscreen mode Exit fullscreen mode

Application Configuration

Create an application.yml file in the resources folder with the necessary configuration.

spring:
  data:
    mongodb:
      database: springjwt
      host: localhost
      port: 27017
      repositories:
        enabled: true
Enter fullscreen mode Exit fullscreen mode

Setting Up the Main Class

In the default package com.jwt.example, add the main class, Application, to initiate the Spring Boot application.

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

Creating the User Model

In the model package, add a User class using Lombok annotations for simplified code.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {

    private ObjectId id;
    private String name;
    private String email;
    private String password;
}
Enter fullscreen mode Exit fullscreen mode

User Repository Interface

Add a UserRepository interface extending MongoRepository in the repository package. This interface will utilize MongoRepository’s built-in CRUD operations, requiring no additional methods.

@Repository
public interface UserRepository extends MongoRepository<User, 
ObjectId> {
}
Enter fullscreen mode Exit fullscreen mode

The next step is to include a 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.findOne(userId);
}

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

The UserService includes getUser() for retrieving users and saveUser() for storing them in the database. A key note: the example skips password hashing for simplicity. After saving users, saveUser() uses tokenService.createToken() to generate a token.


@Service
public class TokenService {

public static final String TOKEN_SECRET = "s4T2zOIWHMM1sxq";

public String createToken(ObjectId userId) {
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
String token = JWT.create()
.withClaim("userId", userId.toString())
.withClaim("createdAt", new Date())
.sign(algorithm);
return token;
} catch (UnsupportedEncodingException exception) {
exception.printStackTrace();
//log WRONG Encoding message
} catch (JWTCreationException exception) {
exception.printStackTrace();
//log Token Signing Failed
}
return null;
}

public String getUserIdFromToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaim("userId").asString();
} catch (UnsupportedEncodingException exception) {
exception.printStackTrace();
//log WRONG Encoding message
return null;
} catch (JWTVerificationException exception) {
exception.printStackTrace();
//log Token Verification Failed
return null;
}
}

public boolean isTokenValid(String token) {
String userId = this.getUserIdFromToken(token);
return userId != null;
}
Enter fullscreen mode Exit fullscreen mode

TokenService Implementation

TokenService manages JWT operations. It uses a SECRET_KEY and sets an EXPIRATION_TIME of 1 hour. The createToken() method generates JWTs with the userId embedded. The service also includes a token validity check using isTokenValid().

@Configuration
public class JWTFilter extends GenericFilterBean {

private 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) 
if (request.getRequestURI().contains("/register")) {
return true;
}
return false;
}
}
Enter fullscreen mode Exit fullscreen mode

JWTFilter for Route Management

JWTFilter, extending GenericFilterBean, controls access to routes. It processes tokens from the "Authorization" header and bypasses "OPTIONS" requests and specified routes like “/register”. Invalid or missing tokens trigger an Unauthorized response, while valid tokens pass userId to the request for controller use.

UserController Setup

The UserController utilizes the userId from the JWTFilter for secure user management in the application.


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

private UserService userService;

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

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

@GetMapping("/get")
public User getUser(HttpServletRequest request) {
ObjectId userId = (ObjectId) request.getAttribute("userId");
return userService.getUser(userId);
}
}`
Enter fullscreen mode Exit fullscreen mode

UserController Functionality

The UserController primarily uses userService methods. A notable function is retrieving the userId from the request attribute in the getUser() method. This setup ensures secure user operations within the application.

With the setup complete, it’s time to test the API. After restarting the Application, use Postman to send a POST request to https://localhost:8080/user/register with the appropriate request body to create a new user.

Receiving and Using the Token:

A successful user registration should return a token in the response body. Copy this token for further requests. To fetch the newly created user, make a GET request to https://localhost:8080/user/get and include the token as an Authorization header.

Image description

If the token is not passed or is altered, the API should respond with a 401 Unauthorized error, indicating the security measures are functioning as intended.

Image description

Conclusion

I hope this guide has given you a good starting point on using JWT tokens to secure your API. If you're looking to delve deeper and really master this topic, I highly recommend checking out this article: Secure your Spring Boot API with JSON Web Tokens. It's a great resource for expanding your understanding and exploring more advanced features.

Top comments (0)