JWT Token Authentication in Spring Boot Microservices

JWT Token Authentication in Spring Boot Microservices

0 Comments

Spring Boot Microservices requires authentication of users, and one way is through JSON Web Token (JWT). JWT is an open standard (RFC 7519) that defines a compact mechanism for securely transmitting information between parties.

In this post, I will explain how to implement JWT authentication in Spring Microservices.

JWT Token Overview

JWT is of relatively small size. Therefore it can be sent through a URL, :

  • Through a POST parameter, or
  • Inside an HTTP header.

However, take note that sending tokens through HTTP headers is the most common approach.

A JWT token contains all the required information about an entity, which can be a user or a service.

This figure shows a typical use case of JWT authentication.

The Example Application

For this post, I have created two services:

  • AuthenticatorService: Responsible for authenticating user name and password. On successful authentication, this service generates and returns a JWT token.
  • BlogService : A protected service. This service contains a filter that verifies the JWT token that a client sends. On successful verification, this service returns a secured message.

This figure shows the interaction between the client and the preceding services.

The AuthenticatorService

Maven POM

To generate JWT token, you need the jjwt artifact as a dependency of your module.

The following code snippet shows the Maven POM dependency.

<dependencies>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
. . . . .

The Entity

AuthenticatorService contains a User entity to represent user credentials. The code of the User entity is this.
User.java

package com.stackroute.AuthenticatorService.model;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="users")
public class User {

@Id
private String userName;
private String password;

public User() {
}

public User(String userName, String password) {
    this.userName = userName;
    this.password = password;
}

public String getUserName() {
    return userName;
}

public void setUserName(String userName) {
    this.userName = userName;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}
}

The Token Generator

Then comes the JwtGeneratorInterface. This interface contains a single generateToken() method that accepts a User object.

JwtGeneratorInterface.java

package com.stackroute.AuthenticatorService.config;

import com.stackroute.AuthenticatorService.model.User;
import java.util.Map;

public interface JwtGeneratorInterface {

Map<String, String> generateToken(User user);
}

JwtGeneratorImpl is the implementation of JwtGeneratorInterface.
JwtGeneratorImpl.java

package com.stackroute.AuthenticatorService.config;

import com.stackroute.AuthenticatorService.model.User;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import io.jsonwebtoken.Jwts;

@Service
public class JwtGeneratorImpl implements JwtGeneratorInterface{

  @Value("${jwt.secret}")
  private String secret;

  @Value("${app.jwttoken.message}")
  private String message;

  @Override
  public Map<String, String> generateToken(User user) {
    String jwtToken="";
    jwtToken = Jwts.builder().setSubject(user.getUserName()).setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, "secret").compact();
    Map<String, String> jwtTokenGen = new HashMap<>();
    jwtTokenGen.put("token", jwtToken);
    jwtTokenGen.put("message", message);
    return jwtTokenGen;
  }
}

The Repository

I am using the Repository programming model of Spring Data JPA by implementingJpaRepository.

The code of the UserRepository repository interface is this.

UserRepository.java

package com.stackroute.AuthenticatorService.repository;

import com.stackroute.AuthenticatorService.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, String> {

public User findByUserNameAndPassword(String userName, String password);
}

The Service Layer

The service interface in this module is UserService. This interface declares two methods: saveUser() to store a User object in the database. The second method is getUserByNameAndPassword() to retrieve a user with the given user name and password.

Here is the code of theUserService interface .
UserService.java

package com.stackroute.AuthenticatorService.service;

import com.stackroute.AuthenticatorService.exception.UserNotFoundException;
import com.stackroute.AuthenticatorService.model.User;
import org.springframework.stereotype.Service;

@Service
public interface UserService {
    public void saveUser(User user);
    public User getUserByNameAndPassword(String name, String password) throws UserNotFoundException;
}

The implementation class of UserService is UserServiceImpl.
The code of UserServiceImpl is this.
UserServiceImpl.java

package com.stackroute.AuthenticatorService.service;

import com.stackroute.AuthenticatorService.exception.UserNotFoundException;
import com.stackroute.AuthenticatorService.model.User;
import com.stackroute.AuthenticatorService.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

  private UserRepository userRepository;

  @Autowired
  public UserServiceImpl(UserRepository userRepository){
    this.userRepository=userRepository;
  }
  @Override
  public void saveUser(User user) {
    userRepository.save(user);
  }

  @Override
  public User getUserByNameAndPassword(String name, String password) throws UserNotFoundException {
    User user = userRepository.findByUserNameAndPassword(name, password);
    if(user == null){
       throw new UserNotFoundException("Invalid id and password");
    }
    return user;
  }
}

The Controller

The controller has two endpoints: /register and /login. The first one is responsible to save a new user. The latter endpoint authenticates a user. On successful authentication, the latter endpoint returns a JWT token.

The code of the controller is this.

UserController.java

package com.stackroute.AuthenticatorService.controller;

import com.stackroute.AuthenticatorService.config.JwtGeneratorImpl;
import com.stackroute.AuthenticatorService.config.JwtGeneratorInterface;
import com.stackroute.AuthenticatorService.exception.UserNotFoundException;
import com.stackroute.AuthenticatorService.model.User;
import com.stackroute.AuthenticatorService.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/user")
public class UserController {
private UserService userService;
private JwtGeneratorInterface jwtGenerator;

  @Autowired
  public UserController(UserService userService, JwtGeneratorInterface jwtGenerator){
    this.userService=userService;
    this.jwtGenerator=jwtGenerator;
  }

  @PostMapping("/register")
  public ResponseEntity<?> postUser(@RequestBody User user){
  try{
     userService.saveUser(user);
     return new ResponseEntity<>(user, HttpStatus.CREATED);
   } catch (Exception e){
     return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
   }
  }

  @PostMapping("/login")
  public ResponseEntity<?> loginUser(@RequestBody User user) {
    try {
      if(user.getUserName() == null || user.getPassword() == null) {
      throw new UserNotFoundException("UserName or Password is Empty");
    }
    User userData = userService.getUserByNameAndPassword(user.getUserName(), user.getPassword());
    if(userData == null){
       throw new UserNotFoundException("UserName or Password is Invalid");
    }
       return new ResponseEntity<>(jwtGenerator.generateToken(user), HttpStatus.OK);
    } catch (UserNotFoundException e) {
       return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
    }
  }
}

Verifying Tokens in the BlogService

BlogService is the one to be protected through JWT. It’s a simple service with the following components:

• The controller that exposes endpoints
• The configuration file that registers the filter
• Filter is the component to do the token verification

Also, to perform verification, ensure you have this dependency in pom.xml.

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

 

The Controller

The controller has two endpoints. The first is an unrestricted endpoint that simply returns a message. The second one is a JWT-restricted endpoint.

The code of the controller is this.
BlogControlleer.java

package guru.springframework.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api/v1/blog")
public class BlogController {

    @GetMapping("/unrestricted")
        public ResponseEntity<?> getMessage() {
        return new ResponseEntity<>("Hai this is a normal message..", HttpStatus.OK);
    }

    @GetMapping("/restricted")
    public ResponseEntity<?> getRestrictedMessage() {
        return new ResponseEntity<>("This is a restricted message", HttpStatus.OK);
    }
}

The Configuration

The configuration is responsible for registering the authentication filter.

The code of the configuration class is this.

FilterConfig.java

package guru.springframework.config;

import guru.springframework.filter.JwtFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean jwtFilter() {
        FilterRegistrationBean filter= new FilterRegistrationBean();
        filter.setFilter(new JwtFilter());

        // provide endpoints which needs to be restricted.
        // All Endpoints would be restricted if unspecified
       filter.addUrlPatterns("/api/v1/blog/restricted");
    return filter;
    }
}

Test the JWT Token in the Filter

The filter is responsible for verifying the JWT token. The filter class extends the GenericFilter class and overrides the doFiter() method.
This method accepts as input parameter:

  • ServletRequest
  • ServletResponse
  • FilterChain

The code of the filter class is this.

JwtFilter.java

package guru.springframework.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
           final HttpServletRequest request = (HttpServletRequest) servletRequest;
           final HttpServletResponse response = (HttpServletResponse) servletResponse;
           final String authHeader = request.getHeader("authorization");
           if ("OPTIONS".equals(request.getMethod())) {
               response.setStatus(HttpServletResponse.SC_OK);
               filterChain.doFilter(request, response);
           } else {
               if(authHeader == null || !authHeader.startsWith("Bearer ")){
                   throw new ServletException("An exception occurred");
               }  
           }
           final String token = authHeader.substring(7);
           Claims claims = Jwts.parser().setSigningKey("secret").parseClaimsJws(token).getBody();
           request.setAttribute("claims", claims);
           request.setAttribute("blog", servletRequest.getParameter("id"));
           filterChain.doFilter(request, response);
    }
}

Test the Application with JWT Token

To test the application:

  1. Start POSTMAN or a REST client to access the AuthenticarService
  2. Send a POST request to the login endpoint with Alice and pass123 as credentials in the request body.
  3. Notice the JSON web token returned by the Authenticator service.

  1. Next, start  BlogService and make a GET request to the restricted endpoint. Ensure, you add the token in the Authorization header.

The value Authorization header value must be Bearer, separated by a space and followed by the token
Once you send the request, you will receive a restricted response.

Summary

I have seen lots of developers verifying JWT tokens in their services. Microservices have a pattern called Gateway Offloading. This pattern enables each microservice to offload shared service functionality, such as the use of SSL certificates, Token verification, to an API gateway.

In addition, Microservices Gateways can tend to become Single Point of Failure. However, with Technology evolving fast and moving to the cloud. There are hardware load balancers, software load balancers, and cloud load balancers. And all of these have redundancy and various failover schemes to prevent a single point of failure.

You can access the AuthenicatorService here at Github, For the Blogservice, access here at Githuub.

If you want to deep-dive into Spring Security, I have a Udemy Bestseller Spring Security Core: Beginner to Guru

 

About SFG Contributor

Staff writer account for Spring Framework Guru

    You May Also Like

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.