Sign Up
Hero

Java Microservices with Spring Boot and Spring Cloud

This tutorial shows you how to build a microservices architecture with Spring Boot and Spring Cloud.

Adopting a microservices architecture provides unique opportunities to add failover and resiliency to your systems so your components can gracefully handle load spikes and errors. Microservices make change less expensive, too. They can also be a good idea when you have a large team working on a single product. You can break up your project into components that can function independently. Once components can operate independently, they can be built, tested, and deployed separately. This gives an organization and its teams the agility to develop and deploy quickly.

Java is an excellent language with a vast open source ecosystem for developing a microservice architecture. In fact, some of the biggest names in our industry use Java and contribute to its ecosystem. Have you ever heard of Netflix, Amazon, or Google? What about eBay, Twitter, and LinkedIn? Yes, web-scale companies handling incredible traffic are doing it with Java.

Implementing a microservices architecture in Java isn't for everyone. For that matter, implementing microservices, in general, isn't often needed. Most companies do it to scale their people, not their systems. Even Martin Fowler's original blog post on Microservices recommends against it:

One reasonable argument we've heard is that you shouldn't start with a microservices architecture. Instead begin with a monolith, keep it modular, and split it into microservices once the monolith becomes a problem.

The Java ecosystem has some well-established patterns for developing microservice architectures. If you're familiar with Spring, you'll feel right at home developing with Spring Boot and Spring Cloud. Since that's one of the quickest ways to get started, I figured I'd walk you through a quick example.

This example contains a microservice with a REST API that returns a list of cool cars. It uses Netflix Eureka for service discovery, WebClient for remote communication, and Spring Cloud Gateway to route requests to the microservice. It integrates Spring Security and OAuth 2.0, so only authenticated users can access the API gateway and microservice. It also uses Resilience4j to add fault tolerance to the gateway.

Here is a diagram showing the overall infrastructure:

Create Java Microservices with Spring Boot and Spring Cloud

I like to show developers how to build everything from scratch. Today, I'm going to take a different approach. First, I'll show you how to get the completed example working. Then, I'll explain how I created everything and the trials and tribulations I encountered along the way.

You can start by cloning the @oktadev/auth0-java-microservices-examples repository.

git clone https://github.com/oktadev/auth0-java-microservices-examples

There are two directories in this repository that pertain to this tutorial:

  • spring-boot-gateway-webflux: a Spring Boot microservice architecture with Spring Cloud Gateway and Spring WebFlux.
  • spring-boot-gateway-mvc: a Spring Boot microservice architecture with Spring Cloud Gateway and Spring MVC.

Each directory contains three projects:

  • discovery-service: a Netflix Eureka server used for service discovery.
  • car-service: a simple Car Service that uses Spring Data REST to serve up a REST API of cars.
  • api-gateway: an API gateway with a /cool-cars endpoint that talks to the car service and filters out cars that aren't cool (in my opinion, of course).

The configuration for the WebFlux and MVC implementations is the same, so choose one and follow along.

You can also watch a demo of the WebFlux example in the screencast below.

Run a Secure Spring Boot Microservice Architecture

To run the example, you must install the Auth0 CLI and create an Auth0 account. If you don't have an Auth0 account, sign up for free. I recommend using SDKMAN! to install Java 17+ and HTTPie for making HTTP requests.

First, start the discovery service:

cd discovery-service
./gradlew bootRun

Before you can start the API gateway project, you'll need to configure the API gateway to use your Auth0 account.

Open a terminal and run auth0 login to configure the Auth0 CLI to get an API key for your tenant. Then, run auth0 apps create to register an OpenID Connect (OIDC) app with the appropriate URLs:

auth0 apps create \
  --name "Kick-Ass Cars" \
  --description "Microservices for Cool Cars" \
  --type regular \
  --callbacks http://localhost:8080/login/oauth2/code/okta \
  --logout-urls http://localhost:8080 \
  --reveal-secrets

Copy api-gateway/.env.example to .env and edit it to contain the values from the command above.

OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
OKTA_OAUTH2_CLIENT_ID=
OKTA_OAUTH2_CLIENT_SECRET=
OKTA_OAUTH2_AUDIENCE=https://<your-auth0-domain>/api/v2/

At startup, these properties will be read using spring-dotenv.

Run ./gradlew bootRun to start the API gateway, or use your IDE to run it.

Copy car-service/.env.example to .env and update its values.

OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
OKTA_OAUTH2_AUDIENCE=https://<your-auth0-domain>/api/v2/

Start it with ./gradlew bootRun and open http://localhost:8080 in your favorite browser. You'll be redirected to Auth0 to log in:

After authenticating, you'll see your name in lights! ✨

You can navigate to the following URLs in your browser for different results:

  • http://localhost:8080/print-token: prints access token to the terminal
  • http://localhost:8080/cool-cars: returns a list of cool cars
  • http://localhost:8080/home: proxies request to the car service and prints JWT claims in this application's terminal

You can see the access token's contents by copying/pasting it into jwt.io. You can also access the car service directly using it.

TOKEN=<access-token>
http :8090/cars Authorization:"Bearer $TOKEN"

Pretty cool, eh? 😎

My Developer Story with Spring Boot and Spring Cloud

A few years ago, I created an example similar to this one with Spring Boot 2.2. It used Feign for remote connectivity, Zuul for routing, Hystrix for failover, and Spring Security for OAuth. The September 2023 version of Spring Cloud has Spring Cloud OpenFeign for remote connectivity, Spring Cloud Gateway for routing, and Resilience4j for fault tolerance.

Okta also now has an Okta Spring Boot starter. I didn't use it in my first experiment, but I'm a big fan of it after the last few years! It dramatically simplifies configuration and makes securing your apps with OAuth 2.0 and OIDC easy. It's a thin wrapper around Spring Security's resource server, OAuth client, and OIDC features. Not only that, but it works with Okta Workforce Identity, Okta Customer Identity (aka Auth0), and even Keycloak.

I created all of these applications using start.spring.io's REST API and HTTPie.

https start.spring.io/starter.tgz bootVersion==3.2.0 \
  artifactId==discovery-service name==eureka-service \
  dependencies==cloud-eureka-server baseDir==discovery-service | tar -xzvf -

https start.spring.io/starter.tgz bootVersion==3.2.0 \
  artifactId==car-service name==car-service baseDir==car-service \
  dependencies==actuator,cloud-eureka,data-jpa,data-rest,postgresql,web,validation,devtools,docker-compose,okta | tar -xzvf -

https start.spring.io/starter.tgz bootVersion==3.2.0 \
  artifactId==api-gateway name==api-gateway baseDir==api-gateway \
  dependencies==cloud-eureka,cloud-feign,data-rest,web,okta | tar -xzvf -

You might notice the api-gateway project doesn't have cloud-gateway as a dependency. That's because I started without it and didn't add it until I needed to proxy requests by path.

In the code listings below, all package and import statements have been removed for brevity. You can find the complete source code in the auth0-java-microservices-examples repository.

Service Discovery with Netflix Eureka

The discovery-service is configured the same as you would most Eureka servers. It has an @EnableEurekaServer annotation on its main class and properties that set its port and turn off discovery.

server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

The car-service and api-gateway projects are configured similarly. Both have a unique name defined, and car-service is configured to run on port 8090 so it doesn't conflict with 8080:

# car-service/src/main/resources/application.properties
server.port=8090
spring.application.name=car-service
# api-gateway/src/main/resources/application.properties
spring.application.name=api-gateway

@EnableDiscoveryClient annotates the main class in both car service and API gateway.

Build a Java Microservice with Spring Data REST

The car-service provides a REST API that lets you CRUD (Create, Read, Update, and Delete) cars. It creates a default set of cars when the application loads using an ApplicationRunner bean:

// car-service/src/main/java/com/example/carservice/CarServiceApplication.java
@EnableDiscoveryClient
@SpringBootApplication
public class CarServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CarServiceApplication.class, args);
    }

    @Bean
    ApplicationRunner init(CarRepository repository) {
        repository.deleteAll();
        return args -> {
            Stream.of("Ferrari", "Jaguar", "Porsche", "Lamborghini", "Bugatti",
                "AMC Gremlin", "Triumph Stag", "Ford Pinto", "Yugo GV").forEach(name -> {
                repository.save(new Car(name));
            });
            repository.findAll().forEach(System.out::println);
        };
    }
}

The CarRepository interface makes persisting and fetching cars from the database easy:

// car-service/src/main/java/com/example/carservice/data/CarRepository.java
@RepositoryRestResource
public interface CarRepository extends JpaRepository<Car, Long> {
}

The Car class is a simple JPA entity with an id and name property. Spring Boot will see PostgreSQL on its classpath and autoconfigure connectivity. A compose.yaml file exists in the root directory to start a PostgreSQL instance using Docker Compose:

services:
  postgres:
    image: 'postgres:latest'
    environment:
      - 'POSTGRES_DB=mydatabase'
      - 'POSTGRES_PASSWORD=secret'
      - 'POSTGRES_USER=myuser'
    ports:
      - '5432'

Spring Boot added Docker Compose support in version 3.1. This means that if you add the following dependency to your build.gradle, it'll look for a compose.yaml (or docker-compose.yaml) file in the root directory and start it when you run ./gradlew bootRun:

developmentOnly 'org.springframework.boot:spring-boot-docker-compose'

Finally, the application.properties has a setting to create the database automatically:

spring.jpa.hibernate.ddl-auto=update

Connect to Java Microservices with Spring Cloud OpenFeign

Next, I configured OpenFeign in the api-gateway project to connect to the car service and its /cars endpoint. Then, I mapped a Car record to the JSON that's returned. I exposed it as a /cool-cars endpoint:

// api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

record Car(String name) {
}

@FeignClient("car-service")
interface CarClient {

    @GetMapping("/cars")
    CollectionModel<Car> readCars();
}

@RestController
class CoolCarController {

    private final CarClient carClient;

    public CoolCarController(CarClient carClient) {
        this.carClient = carClient;
    }

    @GetMapping("/cool-cars")
    public Collection<Car> coolCars() {
        return carClient.readCars()
                .getContent()
                .stream()
                .filter(this::isCool)
                .collect(Collectors.toList());
    }

    private boolean isCool(Car car) {
        return !car.name().equals("AMC Gremlin") &&
                !car.name().equals("Triumph Stag") &&
                !car.name().equals("Ford Pinto") &&
                !car.name().equals("Yugo GV");
    }
}

This worked great, but I still wanted to proxy /home to the downstream car service.

Add Routing with Spring Cloud Gateway

When I first wrote this tutorial with Spring Boot 3.1 and Spring Cloud 2022.0.4, Spring Cloud Gateway only had a WebFlux API. Since Spring Cloud 2023.0.0, it has a Spring MVC API too! This means you can use it with Spring MVC or Spring WebFlux.

Proxy Requests by Path with Spring MVC

I added spring-cloud-starter-gateway-mvc as a dependency to the api-gateway project and added the following to a new api-gateway/src/main/resources/application.yml file:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      mvc:
        routes:
          - id: car-service
            uri: lb://car-service
            predicates:
              - Path=/home/**

With this configuration, I could access the car service directly at http://localhost:8090/cars and through the gateway at http://localhost:8080/cool-cars.

To add failover with Spring Cloud Circuit Breaker, I added it as a dependency:

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'

Then, I enabled it in application.properties:

spring.cloud.openfeign.circuitbreaker.enabled=true

I updated the CarClient interface in ApiGatewayApplication to have a fallback that returns an empty list of cars if the car service is unavailable.

@FeignClient(name = "car-service", fallback = Fallback.class)
interface CarClient {

    @GetMapping("/cars")
    CollectionModel<Car> readCars();

}

@Component
class Fallback implements CarClient {

    @Override
    public CollectionModel<Car> readCars() {
        return CollectionModel.empty();
    }
}

Proxy Requests by Path with WebFlux

Getting everything to work with Spring MVC and Spring Cloud Gateway didn't take long. Using Spring WebFlux required a bit more work.

I immediately discovered that adding spring-cloud-starter-gateway as a dependency caused issues. First, I had Spring MVC in my classpath, and Spring Cloud Gateway uses WebFlux. WebFlux recommends using WebClient over Feign. I decided to switch to WebClient.

I had to remove the following dependencies from my original api-gateway project:

implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

And add Spring Cloud Gateway with Resilience4j dependencies:

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'

Then, I moved CoolCarController to its own class and re-implemented it with WebClient:

// api-gateway/src/main/java/com/example/apigateway/web/CoolCarController.java
@RestController
class CoolCarController {

    Logger log = LoggerFactory.getLogger(CoolCarController.class);

    private final WebClient.Builder webClientBuilder;
    private final ReactiveCircuitBreaker circuitBreaker;

    public CoolCarController(WebClient.Builder webClientBuilder,
                             ReactiveCircuitBreakerFactory circuitBreakerFactory) {
        this.webClientBuilder = webClientBuilder;
        this.circuitBreaker = circuitBreakerFactory.create("circuit-breaker");
    }

    record Car(String name) {
    }

    @GetMapping("/cool-cars")
    public Flux<Car> coolCars() {
        return circuitBreaker.run(
            webClientBuilder.build()
                .get().uri("http://car-service/cars")
                .retrieve().bodyToFlux(Car.class)
                .filter(this::isCool),
            throwable -> {
                log.warn("Error making request to car service", throwable);
                return Flux.empty();
            });
    }

    private boolean isCool(Car car) {
        return !car.name().equals("AMC Gremlin") &&
            !car.name().equals("Triumph Stag") &&
            !car.name().equals("Ford Pinto") &&
            !car.name().equals("Yugo GV");
    }
}

In order to inject the WebClient.Builder, I had to create a WebClientConfiguration class:

// api-gateway/src/main/java/com/example/apigateway/config/WebClientConfiguration.java
@Configuration
public class WebClientConfiguration {

    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

In the car-service project, I had to switch from using Spring Data REST to handling it with a @RestController and @GetMapping annotation. I removed the @RepositoryRestResource annotation from CarRepository and added a CarController class:

// car-service/src/main/java/com/example/carservice/web/CarController.java
@RestController
class CarController {

    private final CarRepository repository;

    public CarController(CarRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/cars")
    public List<Car> getCars() {
        return repository.findAll();
    }
}

NOTE: I did try to use Spring HATEOAS but ran into an issue when using it with the Okta Spring Boot starter.

To proxy /home to the downstream microservice, I added a api-gateway/src/main/resources/application.yml file to configure Spring Cloud Gateway to enable service discovery and specify routes:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: car-service
          uri: lb://car-service
          predicates:
            - Path=/home/**

At this point, I could access the car service directly at http://localhost:8090/cars and through the gateway at http://localhost:8080/cool-cars.

Secure Spring Boot Microservices with OAuth 2.0 and OIDC

To configure the Okta Spring Boot starter, there are a few properties in the api-gateway project's application.properties file:

okta.oauth2.issuer=${OKTA_OAUTH2_ISSUER}
okta.oauth2.client-id=${OKTA_OAUTH2_CLIENT_ID}
okta.oauth2.client-secret=${OKTA_OAUTH2_CLIENT_SECRET}
okta.oauth2.audience=${OKTA_OAUTH2_AUDIENCE}

The car-service is configured as an OAuth resource server and has the following properties in its application.properties file:

okta.oauth2.issuer=${OKTA_OAUTH2_ISSUER}
okta.oauth2.audience=${OKTA_OAUTH2_AUDIENCE}

The variables are read from the .env file in each project's root directory.

Fetch an Access Token as a JWT

When I first got things working, I was able to log in to the gateway, but when I tried to connect to the downstream microservice, it said the JWT was invalid. For this reason, I added a /print-token endpoint to the gateway that prints the access token to the console.

NOTE: The code in this section is for the WebFlux version. The MVC version is in the next section.

// api-gateway/src/main/java/com/example/apigateway/web/HomeController.java
@RestController
class HomeController {

    @GetMapping("/")
    public String howdy(@AuthenticationPrincipal OidcUser user) {
        return "Hello, " + user.getFullName();
    }

    @GetMapping("/print-token")
    public String printAccessToken(@RegisteredOAuth2AuthorizedClient("okta")
                                   OAuth2AuthorizedClient authorizedClient) {

        var accessToken = authorizedClient.getAccessToken();

        System.out.println("Access Token Value: " + accessToken.getTokenValue());
        System.out.println("Token Type: " + accessToken.getTokenType().getValue());
        System.out.println("Expires At: " + accessToken.getExpiresAt());

        return "Access token printed";
    }
}

Using jwt.io, I verified that it wasn't a valid JWT. I thought about trying to implement Spring Security's opaque token support, but discovered Auth0 doesn't have an /instropection endpoint. This makes it impossible to use opaque tokens with Auth0.

The good news is I figured out a workaround! If you pass a valid audience parameter to Auth0, you'll get a JWT for the access token. I logged an issue to improve the Okta Spring Boot starter and added a SecurityConfiguration class to solve the problem in the meantime.

// api-gateway/src/main/java/com/example/apigateway/config/SecurityConfiguration.java
@Configuration
public class SecurityConfiguration {

    @Value("${okta.oauth2.audience:}")
    private String audience;

    private final ReactiveClientRegistrationRepository clientRegistrationRepository;

    public SecurityConfiguration(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
        http
            .authorizeExchange(authz -> authz
                .anyExchange().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .authorizationRequestResolver(
                    authorizationRequestResolver(this.clientRegistrationRepository)
                )
            );
        return http.build();
    }

    private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver(
        ReactiveClientRegistrationRepository clientRegistrationRepository) {

        var authorizationRequestResolver =
            new DefaultServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
            authorizationRequestCustomizer());

        return authorizationRequestResolver;
    }

    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return customizer -> customizer
            .additionalParameters(params -> params.put("audience", audience));
    }
}

To make Spring Cloud Gateway pass the access token downstream, I added TokenRelay to its default filters in application.yml:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      default-filters:
        - TokenRelay
      routes: ...

I updated the WebClientConfiguration class to configure WebClient to include the access token with its requests:

// api-gateway/src/main/java/com/example/apigateway/config/WebClientConfiguration.java
@Configuration
public class WebClientConfiguration {

    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder(ReactiveClientRegistrationRepository clientRegistrations,
                                              ServerOAuth2AuthorizedClientRepository authorizedClients) {
        var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
        oauth.setDefaultClientRegistrationId("okta");
        return WebClient
            .builder()
            .filter(oauth);
    }

}

Spring Cloud Gateway MVC and OAuth 2.0

To get the Spring Cloud Gateway MVC example working with OAuth 2.0, I had to add a SecurityConfiguration class to pass an audience parameter to Auth0:

// api-gateway/src/main/java/com/example/apigateway/config/SecurityConfiguration.java
@Configuration
public class SecurityConfiguration {

    @Value("${okta.oauth2.audience:}")
    private String audience;

    private final ClientRegistrationRepository clientRegistrationRepository;

    public SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authorization -> authorization
                    .authorizationRequestResolver(
                        authorizationRequestResolver(this.clientRegistrationRepository)
                    )
                )
            );
        return http.build();
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
        ClientRegistrationRepository clientRegistrationRepository) {

        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
            new DefaultOAuth2AuthorizationRequestResolver(
                clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
            authorizationRequestCustomizer());

        return authorizationRequestResolver;
    }

    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return customizer -> customizer
            .additionalParameters(params -> params.put("audience", audience));
    }
}

Spring Cloud Gateway MVC 2023.0.0 doesn't allow you to configure a TokenRelay filter in YAML, so I added a RouterFunction bean to add it.

// api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java
public class ApiGatewayApplication {

    @Bean
    public RouterFunction<ServerResponse> gatewayRouterFunctionsLoadBalancer() {
        return route("car-service")
            .route(path("/home/**"), http())
            .filter(lb("car-service"))
            .filter(tokenRelay())
            .build();
    }

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

Thanks for the help with this code, Spencer Gibb! 🙌

The updated application.yml file looks as follows after removing its mvc configuration.

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true

The last thing I needed to configure was OAuth integration for OpenFeign. I added the following properties to application.properties:

spring.cloud.openfeign.oauth2.enabled=true
spring.cloud.openfeign.oauth2.clientRegistrationId=okta

I removed the spring.cloud.openfeign.circuitbreaker.enabled property because I could not get it to work with Spring MVC. If you know how to make it work, please let me know in the comments!

Spring Boot Microservices and Refresh Tokens

In my previous Spring Boot 2.2 example, I couldn't get refresh tokens to work. I was able to get them to work this time! I changed the default scopes in api-gateway to request a refresh token using the offline_access scope:

# .env
OKTA_OAUTH2_AUDIENCE=https://fast-expiring-api
OKTA_OAUTH2_SCOPES=openid,profile,email,offline_access

And added a property to application.properties to read it:

# src/main/resources/application.properties
okta.oauth2.scopes=${OKTA_OAUTH2_SCOPES}

Then, I created an API in Auth0 called fast-expiring-api and set it to expire in 30 seconds:

auth0 apis create --name fast-expiring --identifier https://fast-expiring-api \
  --token-lifetime 30 --offline-access --no-input

If you do the same, you can restart the API gateway and go to http://localhost:8080/print-token to see your access token. You can copy the expired time to timestamp-converter.com to see when it expires in your local timezone. Wait 30 seconds and refresh the page. You'll see a request for a new token and an updated Expires At timestamp in your terminal.

The Okta Spring Boot starter and Keycloak

If you find yourself in a situation where you don't have an internet connection, it can be handy to run Keycloak locally in a Docker container. Since the Okta Spring Boot starter is a thin wrapper around Spring Security, it works with Keycloak, too.

In my experience, Spring Security's OAuth support works with any OAuth 2.0-compliant server. The Okta Spring Boot starter does validate the issuer to ensure it's an Okta URL, so you must use Spring Security's properties instead of the okta.oauth2.* properties when using Keycloak.

An easy way to get a pre-configured Keycloak instance is to use JHipster's jhipster-sample-app-oauth2 application. It gets updated with every JHipster release. You can clone it with the following command:

git clone https://github.com/jhipster/jhipster-sample-app-oauth2.git --depth=1
cd jhipster-sample-app-oauth2

Then, start its Keycloak instance:

docker compose -f src/main/docker/keycloak.yml up -d

You can configure the api-gateway to use Keycloak by removing the okta.oauth2.* properties and using Spring Security's in application.properties:

spring.security.oauth2.client.provider.okta.issuer-uri=http://localhost:9080/realms/jhipster
spring.security.oauth2.client.registration.okta.client-id=web_app
spring.security.oauth2.client.registration.okta.client-secret=web_app
spring.security.oauth2.client.registration.okta.scope=openid,profile,email,offline_access

The car-service requires similar changes in its application.properties file:

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/jhipster
spring.security.oauth2.resourceserver.jwt.audiences=account

Restart both apps, open http://localhost:8080, and you'll be able to log in with Keycloak:

Use admin/admin for credentials, and you can access http://localhost:8080/cool-cars as you did before:

Stay secure with Spring Boot and Spring Cloud!

I hope you liked this tour of how to build Java microservice architectures with Spring Boot and Spring Cloud. You learned how to build everything with minimal code and then configure it to be secure with Spring Security, OAuth 2.0, OIDC, and Auth0 by Okta.

You can find all the code shown in this tutorial on GitHub in the @oktadev/auth0-java-microservices-examples repository. The OpenFeign example with Spring MVC is in the spring-boot-gateway-mvc directory. The Spring Cloud Gateway with WebFlux is in spring-boot-gateway-webflux. The Keycloak example is in the keycloak branch.

If you liked this post, you might enjoy these related posts:

We've also published some new labs about securing Spring Boot in our Developer Center. They're great if you like to learn by doing!

Please follow us on Twitter @oktadev and subscribe to our YouTube channel for more Spring Boot and microservices knowledge.

You can also sign up for our developer newsletter to stay updated on everything Identity and Security.