Secure a Spring Microservices Architecture with Spring Security and OAuth 2.0

avatar-matt_raible.jpg Matt Raible

Building a microservices architecture with Spring Boot and Spring Cloud can allow your team to scale and develop software faster. It can add resilience and elasticity to your architecture that will enable it to fail gracefully and scale infinitely. All this is great, but you need continuous deployment and excellent security to ensure your system stays up-to-date, healthy, and safe for years to come.

With Spring Security and its OAuth 2.0 support, you have everything you need to lock down your API gateway, as well as your backend servers. You can set it up to automatically propagate your access tokens from one app to the other, ensuring that everything stays secure and encrypted along the way.

This tutorial shows you how to use Spring Security with OAuth and Okta to lock down your microservices architecture. You might remember a similar post I wrote back in August: Secure a Spring Microservices Architecture with Spring Security, JWTs, Juiser, and Okta. The difference in this post is you won’t be using any Okta SDKs; Spring Security OAuth has everything you need!

Microservices Architectures with Spring Boot + Spring Cloud

This tutorial shows you how to add security to a previous tutorial I wrote, Build a Microservices Architecture for Microbrews with Spring Boot. A basic microservices architecture with Spring Boot and Spring Cloud looks like the graphic below.

Spring Boot + Cloud Microservices Architecture

Once you’ve completed this tutorial, you’ll have Spring Security locking things down, and Okta providing authorization with OAuth. Your Edge Service (a.k.a., API Gateway) will have a Feign client that passes along your access token, and Hystrix that handles graceful failover.

Spring Microservices with OAuth

To begin, you’ll need to clone the aforementioned article’s completed project.

git clone https://github.com/oktadeveloper/spring-boot-microservices-example.git

Create a Web Application in Okta

If you don’t have one yet, create a forever-free Okta Developer account. After you’ve completed the setup process, log in to your account and navigate to Applications > Add Application. Click Web and Next. On the next page, enter the following values and click Done.

  • Application Name: Spring OAuth
  • Base URIs: http://localhost:8081
  • Login redirect URIs: http://localhost:8081/login

Take note of the clientId and client secret values as you’ll need these to configure your Spring Boot apps.

Add Spring Security OAuth to the Edge Service Application

The edge-service application handles the communication with the beer-catalog-service, so it’s the best place to start integrating OAuth. In edge-service/pom.xml, add dependencies for Spring Security, its OAuth support, and its JWT support.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>

Add the following Zuul routes to edge-service/src/main/resources/application.properties.

zuul.routes.beer-catalog-service.path=/beers
zuul.routes.beer-catalog-service.url=http://localhost:8080

zuul.routes.home.path=/home
zuul.routes.home.url=http://localhost:8080

Open edge-service/src/main/java/com/example/EdgeServiceApplication.java and add @EnableOAuth2Sso to enable authentication with OAuth.

import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
...
@EnableOAuth2Sso
@SpringBootApplication
public class EdgeServiceApplication {

Adding @EnableOAuth2Sso causes Spring Security to look for a number of properties. Add the following properties to edge-service/src/main/resources/application.properties.

security.oauth2.client.client-id={yourClientId}
security.oauth2.client.client-secret={yourClientSecret}
security.oauth2.client.access-token-uri=https://{yourOktaDomain}.com/oauth2/default/v1/token
security.oauth2.client.user-authorization-uri=https://{yourOktaDomain}.com/oauth2/default/v1/authorize
security.oauth2.client.scope=openid profile email
security.oauth2.resource.filter-order=3
security.oauth2.resource.user-info-uri=https://{yourOktaDomain}.com/oauth2/default/v1/userinfo
security.oauth2.resource.token-info-uri=https://{yourOktaDomain}.com/oauth2/default/v1/introspect
security.oauth2.resource.prefer-token-info=false

TIP: If you see {yourOktaDomain} in the above code snippet, log in to your Okta account and refresh this page. It will replace this value with your domain.

Add a ResourceServerConfig.java class to the same package as EdgeServiceApplication.

package com.example.edgeservice;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
            .authorizeRequests()
            .antMatchers("/**").authenticated();
    }
}

At this point, you’ve configured enough to sign-in to your Edge Service application, but it won’t be able to communicate with the downstream beer-catalog-service.

Add Spring Security OAuth to the Beer Catalog Service

In beer-catalog-service/pom.xml, add the same dependencies you added to the Edge Service, as well as one for Thymeleaf.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Add the same properties to beer-catalog-service/src/main/resources/application.properties.

security.oauth2.client.client-id={yourClientId}
security.oauth2.client.client-secret={yourClientSecret}
security.oauth2.client.access-token-uri=https://{yourOktaDomain}.com/oauth2/default/v1/token
security.oauth2.client.user-authorization-uri=https://{yourOktaDomain}.com/oauth2/default/v1/authorize
security.oauth2.client.scope=openid profile email
security.oauth2.resource.filter-order=3
security.oauth2.resource.user-info-uri=https://{yourOktaDomain}.com/oauth2/default/v1/userinfo
security.oauth2.resource.token-info-uri=https://{yourOktaDomain}.com/oauth2/default/v1/introspect
security.oauth2.resource.prefer-token-info=false

TIP: An alternative to adding these properties is to use environment variables. For example, SECURITY_OAUTH2_CLIENT_CLIENT_ID would be the environment variable to specify security.oauth2.client.client-id. Using environment variables would allow you to change the settings for both apps from one location.

Create a HomeController in beer-catalog-service/src/main/java/com/example/beercatalogservice/HomeController.java to render the user’s information so you can verify authentication is working.

package com.example.beercatalogservice;

import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.security.Principal;
import java.util.Map;

@Controller
public class HomeController {

    @GetMapping("/home")
    @SuppressWarnings("unchecked")
    public String howdy(Model model, Principal principal) {
        OAuth2Authentication authentication = (OAuth2Authentication) principal;
        Map<String, Object> user = (Map<String, Object>) authentication.getUserAuthentication().getDetails();
        model.addAttribute("user", user);
        return "home";
    }
}

Create a home.html template in beer-catalog-service/src/main/resources/templates/home.html and populate it with the following code.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <style>
        th {
            text-align: left;
        }
        td {
            white-space: nowrap;
        }
        td:first-child {
            font-family: "Courier", monospace;
            font-size: 0.9em;
            color: #343434;
        }
    </style>
</head>
<body>
<h1>Hello<span th:if="${user}" th:text="' ' + ${user.name}"> Joe</span>!</h1>
<div th:unless="${user}">
    <a th:href="@{/login}">Login</a>
</div>
<div th:if="${user}">
    <form id="logoutForm" th:action="@{/logout}" method="post">
        <input type="submit" value="Logout"/>
    </form>
</div>

<h2>User Properties</h2>
<table>
    <thead>
    <tr>
        <th>Name</th>
        <th>Value</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <td>sub</td>
        <td th:text="${user.sub}"></td>
    </tr>
    <tr>
        <td>name</td>
        <td th:text="${user.name}"></td>
    </tr>
    <tr>
        <td>given_name</td>
        <td th:text="${user.given_name}"></td>
    </tr>
    <tr>
        <td>family_name</td>
        <td th:text="${user.family_name}"></td>
    </tr>
    <tr>
        <td>preferred_username</td>
        <td th:text="${user.preferred_username}"></td>
    </tr>
    <tr>
        <td>email</td>
        <td th:text="${user.email}"></td>
    </tr>
    <tr>
        <td>roles</td>
        <td th:text="${user.roles}"></td>
    </tr>
    </tbody>
</table>
</body>
</html>

Create a ResourceServerConfig.java class in the same package as HomeController. This class configures Spring Security so it secures all endpoints, except those accessed with an Authorization header.

package com.example.beercatalogservice;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
            .authorizeRequests().anyRequest().fullyAuthenticated();
    }
}

Add RequestInterceptor for Feign

The @FeignClient used to talk to beer-catalog-service is not aware of the Authorization header. To make it aware, create a UserFeignClientInterceptor class in the same directory as EdgeServiceApplication.

package com.example.edgeservice;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.stereotype.Component;

@Component
public class UserFeignClientInterceptor implements RequestInterceptor {
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_TOKEN_TYPE = "Bearer";

    @Override
    public void apply(RequestTemplate template) {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        Authentication authentication = securityContext.getAuthentication();

        if (authentication != null && authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
            OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
            template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, details.getTokenValue()));
        }
    }
}

Register it as a @Bean inside the EdgeServiceApplication class.

import feign.RequestInterceptor;
...
public class EdgeServiceApplication {

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

    @Bean
    public RequestInterceptor getUserFeignClientInterceptor() {
        return new UserFeignClientInterceptor();
    }
}

In order to get Hystrix aware of the security context, you need to add two properties to edge-service/src/main/resources/application.properties:

feign.hystrix.enabled=true
hystrix.shareSecurityContext=true

Verify Secure Communication

You can verify communication between the edge-service and beer-catalog-service works by starting all the Spring Boot applications. First, start eureka-service:

cd eureka-service
./mvnw spring-boot:run

In a new terminal window, start beer-catalog-service:

cd beer-catalog-service
./mvnw spring-boot:run

In another terminal window, start edge-service:

cd edge-service
./mvnw spring-boot:run

Open your browser and navigate to http://localhost:8081/good-beers. You should be redirected to your Okta domain and see a login page, prompting for your credentials.

Okta Sign-In Form

Enter the credentials you created your account with, and you’ll see a list of good beers as a result.

Good Beers

If you try to navigate to http://localhost:8081/home, it won’t work. This is because you need to add Spring Cloud Security to edge-service/pom.xml to relay the access token for the Zuul proxy.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-security</artifactId>
</dependency>

Without this dependency, requests to /good-beers will work (because Feign is configured), but /home will not (because Zuul needs Spring Cloud Security).

Restart your Edge Server application, navigate to http://localhost:8081/home and you’ll see your user details on the next page.

Okta User Details

NOTE: I was unable to get the logout button to work due to a CSRF error. I tried adding security.enable-csrf=false to application.properties in the Edge Service app, but it didn’t help. I sent an email to the Spring Security team asking if they had any advice.

Add Okta’s Sign-In Widget to the Angular Client

To use Okta’s Sign-In Widget, you’ll need to modify your app in Okta to enable the Implicit grant type. Log in to your account, navigate to Applications > Spring OAuth > General tab and click Edit. Enable Implicit (Hybrid) under Allowed grant types and select both checkboxes below it. Add http://localhost:4200 under Login redirect URIs and click Save.

For the Sign-In Widget to make requests to this application, you’ll also need to configure the client URL as a trusted origin. Click API > Trusted Origins > Add Origin. Enter http://localhost:4200 as the Origin URL and select both checkboxes under it.

Open a terminal, navigate to spring-boot-microservices-example/client, and install the client’s dependencies using npm.

cd client
npm install

Install Okta’s Sign-In Widget to make it possible to communicate with the secured server.

npm install @okta/okta-signin-widget --save

Add the widget’s CSS to client/src/styles.css:

@import '~@okta/okta-signin-widget/dist/css/okta-sign-in.min.css';
@import '~@okta/okta-signin-widget/dist/css/okta-theme.css';

Create client/src/app/shared/okta/okta.service.ts and use it to configure the widget to talk to your Okta tenant. Make sure to replace {yourOktaDomain} and {clientId} in the code below.

import { Injectable } from '@angular/core';
import * as OktaSignIn from '@okta/okta-signin-widget';

@Injectable()
export class OktaService {
  widget;

  constructor() {
    this.widget = new OktaSignIn({
      baseUrl: 'https://{yourOktaDomain}.com',
      clientId: '{yourClientId}',
      authParams: {
        issuer: 'default',
        responseType: ['id_token', 'token'],
        scopes: ['openid', 'email', 'profile']
      }
    });
  }

  getWidget() {
    return this.widget;
  }

  getIdToken() {
    return this.widget.tokenManager.get('idToken');
  }

  getAccessToken() {
    return this.widget.tokenManager.get('accessToken');
  }
}

Add OktaService as a provider to client/src/app/app.module.ts.

import { OktaService } from './shared/okta/okta.service';

@NgModule({
  ...
  providers: [OktaService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Modify client/src/app/shared/beer/beer.service.ts to read the access token and set it in an Authorization header when it exists.

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { OktaService } from '../okta/okta.service';

@Injectable()
export class BeerService {

  constructor(private http: HttpClient, private oktaService: OktaService) {
  }

  getAll(): Observable<any> {
    let headers: HttpHeaders = new HttpHeaders();
    if (this.oktaService.getAccessToken()) {
      const accessToken = this.oktaService.getAccessToken();
      // headers is immutable, so re-assign
      headers = headers.append('Authorization', accessToken.tokenType + ' ' + accessToken.accessToken);
    }
    return this.http.get('http://localhost:8081/good-beers', {headers: headers});
  }
}

Modify app.component.html to add a placeholder for the widget and a section to show the user’s name and a logout button.

<mat-toolbar color="primary">
  <span>Welcome to {{title}}!</span>
</mat-toolbar>

<!-- Container to inject the Sign-In Widget -->
<div id="okta-signin-container"></div>

<div *ngIf="user">
  <h2>
    Welcome {{user?.name}}!
  </h2>

  <button mat-raised-button (click)="logout()">Logout</button>

  <app-beer-list></app-beer-list>
</div>

You’ll notice the user variable in the HTML. To resolve this, you need to change your client/src/app/app.component.ts so it renders the Sign-In Widget. Angular’s ChangeDetectorRef is used to notify Angular when things have changed and rendering needs to process updated variables.

import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { OktaService } from './shared/okta/okta.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app';
  user;
  signIn;

  constructor(private oktaService: OktaService, private changeDetectorRef: ChangeDetectorRef) {
    this.signIn = oktaService.getWidget();
  }

  showLogin() {
    this.signIn.renderEl({el: '#okta-signin-container'}, (response) => {
      if (response.status === 'SUCCESS') {
        response.forEach(token => {
          if (token.idToken) {
            this.signIn.tokenManager.add('idToken', token);
            this.user = this.getUser(token);
          }
          if (token.accessToken) {
            this.signIn.tokenManager.add('accessToken', token);
          }
        });
        this.signIn.remove();
        this.changeDetectorRef.detectChanges();
      }
    });
  }

  getUser(token) {
    return {
      name: token.claims.name,
      email: token.claims.email,
      username: token.claims.preferred_username
    };
  }

  ngOnInit() {
    this.signIn.session.get((response) => {
      if (response.status !== 'INACTIVE') {
        const token = this.oktaService.getIdToken();
        this.user = this.getUser(token);
        this.changeDetectorRef.detectChanges();
      } else {
        this.showLogin();
      }
    });
  }

  logout() {
    this.signIn.signOut(() => {
      this.user = undefined;
      this.changeDetectorRef.detectChanges();
      this.showLogin();
    });
  }
}

In order for the BeerListComponent (at src/app/beer-list/beer-list.component.ts) to detect that you’ve logged in, you need to use add a constructor dependency on ChangeDetectorRef and invoke its detectChanges() method when you set the giphyUrl property on each beer.

import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { BeerService, GiphyService } from '../shared';

@Component({
  selector: 'app-beer-list',
  templateUrl: './beer-list.component.html',
  styleUrls: ['./beer-list.component.css'],
  providers: [BeerService, GiphyService]
})
export class BeerListComponent implements OnInit {
  beers: Array<any>;

  constructor(private beerService: BeerService, private giphyService: GiphyService,
              private changeDetectorRef: ChangeDetectorRef) { }

  ngOnInit() {
    this.beerService.getAll().subscribe(
      data => {
        this.beers = data;
        for (const beer of this.beers) {
          this.giphyService.get(beer.name).subscribe(url => {
            beer.giphyUrl = url;
            this.changeDetectorRef.detectChanges();
          });
        }
      },
      error => console.log(error)
    )
  }
}

Verify Authentication Works

Start the client by opening a terminal, navigating to the client directory, then running npm start. Open your browser to http://localhost:4200, and you should see a login form like the following.

Angular Login

If you want to adjust the style of the form, so it isn’t right up against the top toolbar, add the following to styles.css.

#okta-signin-container {
  margin-top: 25px;
}

Angular Login Styled

You should be able to log in, see a welcome message, as well as a logout button. However, you won’t see a beer list because of the following error in your console.

Failed to load http://localhost:8081/good-beers: Response for preflight is invalid (redirect)

This happens because Spring Security doesn’t recognize the @CrossOrigin annotation on the /good-beers endpoint. To fix this, add a simpleCorsFilter to EdgeServiceApplication.

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Collections;
...
public class EdgeServiceApplication {

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

    @Bean
    public RequestInterceptor getUserFeignClientInterceptor() {
        return new UserFeignClientInterceptor();
    }

    @Bean
    public FilterRegistrationBean simpleCorsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(Collections.singletonList("*"));
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"));
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

Restart the Edge Service application and try again. This time you should have great success!

Angular Welcome

NOTE: You should change the allowed origins from * to your client’s URLs if you’re using this configuration in production.

Deploy to Cloud Foundry

To deploy everything on Cloud Foundry with Pivotal Web Services, you’ll need to create an account, download/install the Cloud Foundry CLI, and sign-in (using cf login -a api.run.pivotal.io).

There are quite a few steps involved to deploy all the services and the Angular client for production. For that reason, I wrote a deploy.sh script that automates everything.

NOTE: After this script finishes, you’ll have to add the URL for the client to your Okta app as a Login redirect URI. You’ll also need to add it as an origin under API > Trusted Origins.

TIP: If you receive an error stating that you’re using too much memory, you may have to upgrade your Cloud Foundry subscription.

Learn More about Spring Boot, OAuth 2.0, and Microservices

This article showed you how to use Spring Security, OAuth, and Okta secure a microservices architecture. With Zuul, Feign, and Spring Cloud Security, you can ensure your backend services communicate securely.

The source code for this tutorial is available on GitHub, in the “oauth” branch.

git clone https://github.com/oktadeveloper/spring-boot-microservices-example.git
git checkout oauth

This tutorial showed you how to add security to a previous tutorial, Build a Microservices Architecture for Microbrews with Spring Boot.

If you’re interested in learning about the future of Spring Security and OAuth 2.0, see Next Generation OAuth 2.0 Support with Spring Security by our good friend Joe Grandja of the Spring Security Team.

Also, JHipster uses this same setup with its OAuth support. I hope to write a post soon that demonstrates how to create a microservices architecture with JHipster and OAuth. In the meantime, you can see how to Use Ionic for JHipster to Create Mobile Apps with OIDC Authentication.

Learn more about Okta and its APIs at developer.okta.com/product. If you have questions about this tutorial, please leave a comment below or hit me up on Twitter @mraible.