[Learn Microservices With Me] Securing Microservice Communication Using An Auth Server


Hi everyone! Welcome back to a new blog post. Today, we'll continue with our student microservice API and extend it with an Auth server to secure our microservice communication.
Prerequisites
To follow along with this guide, you should:
Be able to follow along with the previous microservice tutorials in this blog (and ideally have read them)
Understand the basics of microservices
Understand the concept of configuration servers in a microservice architecture
Know how JWT works, including its structure and the theory behind it
Know about HTTP basic authentication
For this guide, we’ll be starting from the code we left off at the previous blog post. If you need a refresher on the code we’ll be working with, check it out on GitHub.
Background
If you remember back to my first post, we had implemented some communication between microservices to sync replicated data between services:
Course Service
@RestController
@RequestMapping("student")
@AllArgsConstructor
public class StudentController {
private StudentService studentService;
@PostMapping
public ResponseEntity<Void> addStudent(@RequestBody SaveStudentRequest student) {
studentService.saveStudent(student);
return ResponseEntity.noContent().build();
}
}
Single endpoint for the Student Service microservice to sync its student records with the Course Service
Student Service
@RestController
@RequestMapping("/student")
@AllArgsConstructor
public class StudentController {
// Fields and other endpoints up here...
@PostMapping("")
public ResponseEntity<Void> saveStudent(@RequestBody StudentDTO studentDTO) throws URISyntaxException {
var studentID = studentService.saveStudent(studentDTO);
courseServiceClient.saveNewStudent(new CourseSaveStudentRequest(studentID))
.then()
.subscribe(); // Do nothing when the request goes through.
return ResponseEntity.created(new URI("/student/" + studentID)).build();
}
}
Student Service sends the request to the Course Service after saving a new student
One issue with this code is security; how can we enforce that only our Student Service can call this endpoint? To do this, we need to implement some form of authentication and authorization to verify that the client is our Student Service. This is where an authentication server will come into play.
Our Authentication Server
In our updated application, we'll make use of the functionality Spring provides to create an authentication server. Using the spring-boot-starter-oauth2-authorization-server
dependency, we can set up a microservice for managing JWT-based authorization. This includes:
configuring registered clients, their passwords (secrets), authorities, and more;
generating JWTs with a private key;
issuing JWTs; and
providing public keys for microservices to verify issued JWTs.
Much of this functionality would be implemented for us by Spring, and all we'd need to do is write configurations. After it’s all set up, we can then configure our Course Service to require authentication for the /student
endpoint. When our Student Service wants to send a request to this endpoint, it would first need to send a request to the Auth Service to obtain a JWT it will add to the authorization header.
Implementing the Auth Service
Now that you understand how our Auth Service will work, let's dive into the implementation! First, let's add a new Maven module to our application. The pom.xml
for the new module should look like so:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.john.amiscaray</groupId>
<artifactId>MicroservicesClassroomExample</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>AuthService</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
From there, we can add the basic properties we need for our microservice to register itself with Consul (our service registry) and connect to the config server:
spring.application.name=auth-service
server.port=8083
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.config.import=configserver:
spring.cloud.config.discovery.service-id=config-service
spring.cloud.config.discovery.enabled=true
and add our main method:
package io.john.amiscaray;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
With that, we need to configure this microservice as an authentication server. We'll first create the required configuration class and set up the public/private key pair we discussed above:
@Configuration
public class AuthorizationServerConfig {
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa(); // generate a key pair
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
private RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
}
}
package io.john.amiscaray.util;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
public final class KeyGeneratorUtils {
private KeyGeneratorUtils() {}
public static KeyPair generateRsaKey() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
This code generates an RSA public/private key pair and uses it to create a JWKSource
bean. Spring will use this bean to generate JWTs using the private key and share the public key using an endpoint of this Auth Service.
Next, we need to create another bean for some configuration for our Auth Server:
@Bean
public AuthorizationServerSettings authorizationServerSettings(@Value("${auth.issuer-uri}") String issuerUri) {
return AuthorizationServerSettings.builder()
.issuer(issuerUri)
.build();
}
along with some new configuration:
# Other properties go here...
# Add the property below to our application.properties; this is the value of the issuerUri argument for our bean
auth.issuer-uri=http://localhost:8083
This sets the issuer field for our JWTs to the URI of our Auth Server. Without this configuration, you'd run into 401 unauthorized errors due to an invalid issuer.
Lastly, we need a couple more beans for a repository of registered clients and a password encoder to encode their passwords. Let's start with the easier of the two, the password encoder:
@Bean
public PasswordEncoder secretEncoder() {
return new BCryptPasswordEncoder();
}
Then, we can define a RegisteredClientRepository
bean where we can register our Student Service as a client:
@Bean
public RegisteredClientRepository registeredClientRepository(
PasswordEncoder passwordEncoder,
@Value("${client.student-service.client-secret}") String rawSecret
) {
var encodedSecret = passwordEncoder.encode(rawSecret);
var registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("student-service")
.clientSecret(encodedSecret)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("write:student")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
add auth-service.properties
in our config repository (for our config server) with the client secret:
# auth-service/auth-service.properties from the configuration git repository
client.student-service.client-secret=${CLIENT_SECRET}
and update our config server configuration with a search path for the location of the auth-service.properties
files
# Config Server application.properties
# Other properties above...
# Updated Config Server search path:
spring.cloud.config.server.git.search-paths[2]=auth-service
Above, in our registeredClientRepository
method, we register our Student Service as a client with a random UUID, password/secret (added in our configuration repository as an environment variable), and a write:student
permission/scope. Additionally, we configured it so that the Student Service can authenticate with this service using HTTP basic authentication to retrieve the JWT as client credentials.
With that configuration, our Student Service would have to send a POST request to this Auth Service to a generated /oauth2/token
endpoint to request the JWT token. In the body, the request will have information that the Student service wants client credentials (a JWT) and to request the write:student
permission for the /student
endpoint. To authenticate the request, the Student Service would add an Authorization header (following the HTTP basic authentication scheme) with the above client ID and client secret as the username and password, respectively.
For your reference, the final configuration class should look like so:
package io.john.amiscaray.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import io.john.amiscaray.util.KeyGeneratorUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
public class AuthorizationServerConfig {
@Bean
public RegisteredClientRepository registeredClientRepository(
PasswordEncoder passwordEncoder,
@Value("${client.student-service.client-secret}") String rawSecret
) {
var encodedSecret = passwordEncoder.encode(rawSecret);
var registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("student-service")
.clientSecret(encodedSecret)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("write:student")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa(); // generate a key pair
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
private RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
}
@Bean
public AuthorizationServerSettings authorizationServerSettings(@Value("${auth.issuer-uri}") String issuerUri) {
return AuthorizationServerSettings.builder()
.issuer(issuerUri)
.build();
}
@Bean
public PasswordEncoder secretEncoder() {
return new BCryptPasswordEncoder();
}
}
Note above that with the CLIENT_SECRET environment variable, the property would be retrieved from the machine that the Auth Service is running on, not from the config server.
Updating the Course Service
Now that our Auth Service is complete, we need to make some changes to our Course Service to secure the /student
endpoint. To begin, we need to add the following dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
As you'd expect, we now need Spring Security to secure our /student
endpoint. However, what's more interesting here is the spring-boot-starter-oauth2-resource-server
dependency. This is required for us to use the Auth Service.
Next, we can configure Spring Security for our service to require authentication for our /student
endpoint:
package io.john.amiscaray.cfg;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(ex -> ex
.pathMatchers("/student").authenticated()
.anyExchange().permitAll()
).oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
This configuration will require clients calling the /student
endpoint to be authenticated using a JWT in their Authorization header while permitting all other requests. Afterward, we need to update our application.properties
to configure the URI of our Auth Server:
# Other properties up here...
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8083
Here, we need to use the actual hostname and port of our Auth Server since the JWT decoder functionality can't resolve the hostname and port by service name, as we've done in the previous blog post.
Finally, we can update our /student
endpoint to require the write:student
permission that our Student Service is allowed to have:
@PreAuthorize("hasAuthority('SCOPE_write:student')") // Require the write:student permission
@PostMapping
public ResponseEntity<Void> addStudent(@RequestBody SaveStudentRequest student) {
studentService.saveStudent(student);
return ResponseEntity.noContent().build();
}
Updating Our Student Service
Lastly, we need to update our Student Service to retrieve a JWT and apply it as an Authorization header for the /student
request. First, we need to create a new AuthServiceClient
class to call the Auth Service:
package io.john.amiscaray.http;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Map;
@Service
public class AuthServiceClient {
private WebClient.Builder webClientBuilder;
@Value("${auth.client-secret}")
private String clientSecret;
public AuthServiceClient(WebClient.Builder webClientBuilder) {
this.webClientBuilder = webClientBuilder;
}
public Mono<String> getAuthToken() {
return webClientBuilder.build().post()
.uri("http://auth-service/oauth2/token")
.headers(headers -> headers.setBasicAuth("student-service", clientSecret))
.body(BodyInserters.fromFormData("grant_type", "client_credentials").with("scope", "write:student"))
.retrieve()
.bodyToMono(Map.class)
.map(response -> (String) response.get("access_token"));
}
}
In the getAuthToken
method, we send the post request to the /oauth2/token
. Here the http://auth-service
URI gets resolved as the host and port of the Auth Service using service discovery. Then, in the headers, we have an HTTP basic authorization header with our service name as the username and client secret as the password. In the body, we have form data specifying that we want to get client credentials (the JWT) with the write:student
scope. Finally, we retrieve the body as a Mono
(async container) and map it to a Mono<String>
containing the JWT from the response body. For the auth.client-secret
property, we add it to the git repository that our Config Server fetches from:
auth.client-secret=${CLIENT_SECRET}
Again, like last time, the
CLIENT_SECRET
environment variable gets fetched from the machine that the Student Service is running on.
Lastly, using this new method, we can update the CourseServiceClient
class to add a JWT to the header of the /student
request:
package io.john.amiscaray.http;
import io.john.amiscaray.dto.CourseSaveStudentRequest;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.*;
import reactor.core.publisher.Mono;
import java.util.function.Consumer;
@Service
@AllArgsConstructor
public class CourseServiceClient {
private WebClient.Builder webClientBuilder;
private AuthServiceClient authServiceClient;
public Mono<Void> saveNewStudent(CourseSaveStudentRequest saveStudentRequest) {
return authServiceClient.getAuthToken()
.flatMap(authToken -> webClientBuilder
.build()
.post()
.uri("http://course-service/student")
.headers(headers -> headers.setBearerAuth(authToken))
.bodyValue(saveStudentRequest)
.retrieve()
.bodyToMono(Void.class));
}
}
First, we are calling the AuthServiceClient#getAuthToken
method to retrieve the Mono
containing the JWT. Then we call the Mono#flatMap
method to retrieve this JWT and call the /student
endpoint with the JWT in the authorization header, returning a new Mono
as the result of the API call.
Conclusion
With that, we have successfully implemented authentication and authorization in our microservices application to secure our inter-service communication! I hope you found this interesting and can use this to develop new skills or awesome projects in the future. For future reference, check out the final code here on GitHub.
Subscribe to my newsletter
Read articles from John Amiscaray directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
