Adding security to micro-services — Spring boot (Gateway filter) + JWT authentication (Part 2)
This is the second part of my previous article about creating micro-services with spring boot framework-
Now we will add some security to prevent unauthorised access to out services(apis). As we are talking about micro-services which means that we should and have a stand-alone authentication service to provide authentication mechanism to each service in a single place, instead of implementing it in each service. And also we should have a common place to authorising incoming requests.
From the above scenario we can conclude that we need following to implement -
- An Authentication service — for identifying users
- An Authorisation filter — for filtering un-authorised requests
We will create each of them one-by-one, Let’s Start!
NOTE: I am using IntelliJ IDEA as IDE for this project, all the instructions and coding sample are from it.
Before start, If you still haven’t read my previous article (link at the top), you can clone this Github repo mayank042/spring_microservice_sample: Sample application demonstrating the spring boot micro-service architecture (github.com) and checkout the tag — v1_no_auth
Generating the JWT token
Authentication Service
We use this service to generate a JWT token, that can be later passed in authentication header of api requests. Will verify the token in Gateway service.
Assuming we have a User table in out database with email, password and a role. So we will create an User entity with UserRepository to fetch user details.
Start by creating a new spring module inside root module -
File → New → Module → Spring initializr
(using spring boot version — 2.7.x)
pom.xml — add JPA dependency
<!--JPA-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--Validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
User.java — entity class
@Data
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Id", nullable = false)
private Long id;
@Size(max = 255)
@Column(name = "Email")
private String email;
@Size(max = 255)
@Column(name = "Password")
private String password;
@Size(max = 255)
@Column(name = "Role")
private String role;
}
UserRepository.java — JPA repository for User entity
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email);
}
UserService.java and UserServiceImpl.java
public interface UserService {
User getUserByEmail(String email);
}
@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository repository;
@Override
public User getUserByEmail(String email) {
return Optional.ofNullable(repository.findByEmail(email))
.orElseThrow(() -> new RuntimeException("User with email is not found."));
}
}
Till here we have created an POST api /authenticate to handle authentication requests, and service and repository to find the User by email.
Now we will use a JWT library to generate the token.
Also note that we do not store passwords in database directly, instead we use some encryption and decryption techniques, to store and retrieve the password.
Now, create an controller to handle the authentication requests and return with JWT token.
UserController.java
@RestController
@AllArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping(value = "/authenticate")
public ResponseEntity<String> authenticateUser(
@Valid @RequestBody AuthenticationRequest request
) {
User user = userService.getUserByEmail(request.getEmail());
var password = CryptoUtils.decrypt(user.getPassword());
if (user.getPassword().equals(password)) {
// generate token
var token = jwtTokenUtil.generate(user, "ACCESS");
return ResponseEntity.ok(new AuthToken(token));
}
return ResponseEntity.badRequest().build();
}
}
Verifying JWT token (Applying gateway request filter)
Gateway service
Its time to put out JWT token on work, we will use that to verify and block invalid/unknown/non-authorised requests to our all services, by applying an request filter in out prev. gateway service.
pom.xml — Add some JWT dependencies first -
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
AuthenticationFilter.java — a gateway filter
@RefreshScope
@Component
public class AuthenticationFilter implements GatewayFilter {
@Autowired
private RouterValidator routerValidator;
@Autowired
private JwtUtil jwtUtil;
@Value("${jwt.prefix}")
public String TOKEN_PREFIX;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (routerValidator.isSecured.test(request)) {
if (this.isAuthMissing(request) || this.isPrefixMissing(request))
return this.onError(exchange, "Authorization header is missing in request", HttpStatus.UNAUTHORIZED);
final String token = this.getAuthHeader(request);
if (jwtUtil.isInvalid(token))
return this.onError(exchange, "Authorization header is invalid", HttpStatus.UNAUTHORIZED);
this.populateRequestWithHeaders(exchange, token);
}
return chain.filter(exchange);
}
/*PRIVATE*/
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
private String getAuthHeader(ServerHttpRequest request) {
var header = request.getHeaders().getOrEmpty("Authorization").get(0);
return header.replace(TOKEN_PREFIX,"").trim();
}
private boolean isAuthMissing(ServerHttpRequest request) {
return !request.getHeaders().containsKey("Authorization");
}
private boolean isPrefixMissing(ServerHttpRequest request) {
var header = request.getHeaders().getFirst ("Authorization");
assert header != null;
return !header.startsWith(TOKEN_PREFIX);
}
private void populateRequestWithHeaders(ServerWebExchange exchange, String token) {
Claims claims = jwtUtil.getAllClaimsFromToken(token);
exchange.getRequest().mutate()
.header("id", String.valueOf(claims.get("id")))
.header("roles", String.valueOf(claims.get("roles")))
.header("tenantId", String.valueOf(claims.get("tenantId")))
.build();
}
}
Here AuthenticationFilter implements the GateFilter which provides us with a filter method to chain and filter requests after validation.
inside filter method, we are checking that Authentication
header should present in the request and the JWT token should not expired and valid. If checks failed we can return response of 401 unauthorised
Applying the filter -
GatewayConfig.java
@Configuration
public class GatewayConfig {
@Autowired
AuthenticationFilter filter;
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.filters(f -> f.filter(filter))
.uri("lb://user-service"))
.route("auth-service", r -> r.path("/api/auth/**")
.uri("lb://auth-service"))
.build();
}
}
Note how we applied the filter on the user-service, we can do the same with all the service we have and same authentication will work for all.
Congratulation! we have successfully added some basic security to our services.